const PARTICLE_COUNT = 400; const SPAWN_DURATION = 700; const VX_RANGE = 0.5; const VY_MIN = 0.3; const VY_RANGE = 0.2; const WIDTH_MIN = 3; const WIDTH_RANGE = 4; const HEIGHT_MIN = 2; const HEIGHT_RANGE = 3; const ROTATION_SPEED_RANGE = 0.15; const DRIFT_STRENGTH = 0.03; const GRAVITY = 0.007; const VX_DAMPING = 0.995; const SWAY_AMPLITUDE = 0.3; const SWAY_SPEED_MIN = 0.02; const SWAY_SPEED_RANGE = 0.03; const OFFSCREEN_MARGIN = 10; const TOTAL_HEIGHT = 192 * 2; const COLORS = [ "#fb0018", "#0059f3", "#f3e300", "#00fb00", "#fb8afb", "#fb9200", ]; export type ConfettiParticle = { x: number; y: number; vx: number; vy: number; width: number; height: number; color: string; rotation: number; rotationSpeed: number; swayPhase: number; swaySpeed: number; }; type Spawner = { frame: number; accumulator: number; particleCount: number; duration: number; }; const particles: ConfettiParticle[] = []; const spawners: Spawner[] = []; const spawnParticle = () => { particles.push({ x: Math.random() * LOGICAL_WIDTH, y: -(Math.random() * 20), vx: (Math.random() - 0.5) * VX_RANGE, vy: VY_MIN + Math.random() * VY_RANGE, width: WIDTH_MIN + Math.random() * WIDTH_RANGE, height: HEIGHT_MIN + Math.random() * HEIGHT_RANGE, color: COLORS[Math.floor(Math.random() * COLORS.length)]!, rotation: Math.random() * Math.PI * 2, rotationSpeed: (Math.random() - 0.5) * ROTATION_SPEED_RANGE, swayPhase: Math.random() * Math.PI * 2, swaySpeed: SWAY_SPEED_MIN + Math.random() * SWAY_SPEED_RANGE, }); }; const spawn = (particleCount = PARTICLE_COUNT, duration = SPAWN_DURATION) => { spawners.push({ frame: 0, accumulator: 0, particleCount, duration }); }; const update = () => { for (let s = spawners.length - 1; s >= 0; s -= 1) { const spawner = spawners[s]!; if (spawner.frame < spawner.duration) { const progress = spawner.frame / spawner.duration; const inv = 1 - progress; const rate = inv * inv; spawner.accumulator += rate * (spawner.particleCount / spawner.duration) * 3; const count = Math.floor(spawner.accumulator); spawner.accumulator -= count; for (let i = 0; i < count; i += 1) { spawnParticle(); } spawner.frame += 1; } else { spawners.splice(s, 1); } } for (let i = particles.length - 1; i >= 0; i -= 1) { const p = particles[i]!; p.vy += GRAVITY; p.vx += (Math.random() - 0.5) * DRIFT_STRENGTH; p.vx *= VX_DAMPING; p.swayPhase += p.swaySpeed; p.x += p.vx + Math.sin(p.swayPhase) * SWAY_AMPLITUDE; p.y += p.vy; p.rotation += p.rotationSpeed; if (p.y > TOTAL_HEIGHT + OFFSCREEN_MARGIN) { particles.splice(i, 1); } } }; export const useConfetti = () => ({ particles, spawn, update });