Spaces:
Running
Running
| // 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 }; | |
| } | |