/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import React from "react"; const DEFAULT_GRADIENT_STOPS = [ { offset: "0%", color: "var(--color-orange-20)" }, { offset: "28%", color: "var(--color-orange-30)" }, { offset: "64%", color: "var(--color-pink-30)" }, { offset: "100%", color: "var(--color-pink-40)" }, ]; const DEFAULT_CONFETTI_COUNT = 42; const CONFETTI_SHAPES = [ { radius: "1px", clip: "none" }, // rectangle / streamer { radius: "50%", clip: "none" }, // circle / oval { radius: "0", clip: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)" }, // diamond { radius: "0", clip: "polygon(50% 0%, 0% 100%, 100% 100%)" }, // triangle ]; const SOCCER_POOL = [ { ball: true }, { ball: true }, { ball: true }, { ball: true }, ...CONFETTI_SHAPES, ]; // Stable within a celebration run, varied between runs. const celebrationRandom = seed => { const value = Math.sin(seed) * 10000; return value - Math.floor(value); }; const buildConfettiPieces = (run, colors, count, shapeMode) => { const pool = shapeMode === "soccer" ? SOCCER_POOL : CONFETTI_SHAPES; const spread = shapeMode === "soccer"; return Array.from({ length: count }, (_, i) => { const base = (run + 1) * 100 + i; const color = colors[i % colors.length]; const shape = pool[Math.floor(celebrationRandom(base + 6) * pool.length)]; const isBall = !!shape.ball; const width = isBall ? Math.round(12 + celebrationRandom(base + 0.5) * 5) : Math.round(6 + celebrationRandom(base + 0.5) * 4); const height = isBall ? width : Math.round(width * (1.4 + celebrationRandom(base + 5) * 0.8)); const left = spread ? `${Math.min( ((i + celebrationRandom(base + 7)) / count) * 100, 98 ).toFixed(2)}%` : `${(celebrationRandom(base) * 100).toFixed(2)}%`; const delay = spread ? `${Math.round(celebrationRandom(base + 1) * 1100)}ms` : `${Math.round(celebrationRandom(base + 1) * 350)}ms`; const duration = spread ? `${Math.round(2600 + celebrationRandom(base + 2) * 800)}ms` : `${Math.round(3000 + celebrationRandom(base + 2) * 1200)}ms`; const rotate = spread ? `${Math.round(celebrationRandom(base + 3) * 400 - 200)}deg` : `${Math.round(celebrationRandom(base + 3) * 720 - 360)}deg`; const drift = spread ? `${Math.round(celebrationRandom(base + 4) * 90 - 45)}px` : `${Math.round(celebrationRandom(base + 4) * 80 - 40)}px`; return { id: i, ball: isBall, color, left, delay, duration, rotate, drift, width: `${width}px`, height: `${height}px`, radius: isBall ? "50%" : (shape.radius ?? "50%"), clip: isBall ? "none" : (shape.clip ?? "none"), }; }); }; const FIREWORK_SPARKS = 16; const buildFireworks = (run, colors, count) => Array.from({ length: count }, (_burst, b) => { const base = (run + 1) * 1000 + b * 37; const color = colors[b % colors.length]; const left = `${(10 + celebrationRandom(base) * 80).toFixed(1)}%`; const topPct = `${(8 + celebrationRandom(base + 1) * 38).toFixed(1)}%`; const burstDelay = Math.round(celebrationRandom(base + 2) * 2200); const sizeScale = 0.7 + celebrationRandom(base + 4); const radius = (24 + celebrationRandom(base + 3) * 14) * sizeScale; const sparks = Array.from({ length: FIREWORK_SPARKS }, (_spark, s) => { const angle = (s / FIREWORK_SPARKS) * 2 * Math.PI + celebrationRandom(base + s) * 0.5; const dist = radius * (0.7 + celebrationRandom(base + s + 0.5) * 0.6); return { id: s, dx: `${Math.round(Math.cos(angle) * dist)}px`, dy: `${Math.round(Math.sin(angle) * dist)}px`, delay: `${burstDelay + Math.round(celebrationRandom(base + s + 0.7) * 40)}ms`, }; }); return { id: b, left, top: topPct, color, scale: sizeScale, sparks }; }); const confettiPieceStyle = piece => ({ "--confetti-x": piece.left, "--confetti-w": piece.width, "--confetti-h": piece.height, "--confetti-color": piece.color, "--confetti-delay": piece.delay, "--confetti-duration": piece.duration, "--confetti-rotate": piece.rotate, "--confetti-drift": piece.drift, "--confetti-radius": piece.radius, "--confetti-clip": piece.clip, }); export const WidgetCelebration = ({ classNamePrefix = "widget-celebration", celebrationFrame, celebrationId, confettiColors, confettiCount = DEFAULT_CONFETTI_COUNT, confettiShape = "mixed", fireworkBursts = 0, gradientStops = DEFAULT_GRADIENT_STOPS, headlineL10nId, illustrationSrc, onComplete, subheadL10nId, }) => { const className = suffix => suffix ? `${classNamePrefix}-${suffix}` : classNamePrefix; // Copy-less celebrations are purely decorative. const hasCopy = !!(headlineL10nId || subheadL10nId); const confettiPieces = confettiColors?.length ? buildConfettiPieces( celebrationId, confettiColors, confettiCount, confettiShape ) : []; const fireworks = fireworkBursts && confettiColors?.length ? buildFireworks(celebrationId, confettiColors, fireworkBursts) : []; const ballSymbolId = `${classNamePrefix}-ball-${celebrationId}`; const resolvedIllustrationSrc = illustrationSrc?.endsWith(".svg") ? `${illustrationSrc}?run=${celebrationId}` : illustrationSrc; const strokeSize = celebrationFrame.strokeInset * 2; const strokeWidth = celebrationFrame.width - strokeSize; const strokeHeight = celebrationFrame.height - strokeSize; return (