// Confetti burst on the celebration canvas. Runs its own animation loop; call `burst(x, y)` // (stage-relative pixels) on a correct catch. Pointer-events are disabled in CSS so it never // intercepts the game. export function createConfetti(canvas) { const ctx = canvas.getContext("2d"); let particles = []; const colors = ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93", "#ff924c", "#ff6392"]; function resize() { canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight; } resize(); window.addEventListener("resize", resize); function burst(x, y, count = 90) { for (let i = 0; i < count; i++) { const angle = Math.random() * Math.PI * 2; const speed = 3 + Math.random() * 7; particles.push({ x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 5, // bias upward size: 7 + Math.random() * 9, color: colors[Math.floor(Math.random() * colors.length)], rot: Math.random() * Math.PI, vr: (Math.random() - 0.5) * 0.5, life: 1, }); } } function tick() { ctx.clearRect(0, 0, canvas.width, canvas.height); if (particles.length) { particles = particles.filter((p) => p.life > 0 && p.y < canvas.height + 40); for (const p of particles) { p.vy += 0.28; // gravity p.vx *= 0.99; // drag p.x += p.vx; p.y += p.vy; p.rot += p.vr; p.life -= 0.012; ctx.save(); ctx.globalAlpha = Math.max(0, p.life); ctx.translate(p.x, p.y); ctx.rotate(p.rot); ctx.fillStyle = p.color; ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size * 0.6); ctx.restore(); } } requestAnimationFrame(tick); } tick(); return { burst }; }