const PARTICLE_COUNT = 400; const SPAWN_DURATION = 350; const VX_RANGE = 1.0; const VY_MIN = 0.6; const VY_RANGE = 0.4; const WIDTH_MIN = 3; const WIDTH_RANGE = 4; const HEIGHT_MIN = 2; const HEIGHT_RANGE = 3; const ROTATION_SPEED_RANGE = 0.3; const DRIFT_STRENGTH = 0.06; const GRAVITY = 0.014; const VX_DAMPING = 0.988; const SWAY_AMPLITUDE = 0.6; const SWAY_SPEED_MIN = 0.04; const SWAY_SPEED_RANGE = 0.06; const OFFSCREEN_MARGIN = 10; const TOTAL_HEIGHT = 192 * 2; const FRAME_TIME = 1000 / 60; 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 = { elapsed: 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({ elapsed: 0, accumulator: 0, particleCount, duration: duration * FRAME_TIME, }); }; const update = (deltaTime: number) => { const dt = deltaTime / FRAME_TIME; for (let s = spawners.length - 1; s >= 0; s -= 1) { const spawner = spawners[s]!; if (spawner.elapsed < spawner.duration) { const progress = spawner.elapsed / spawner.duration; const inv = 1 - progress; const rate = inv * inv; spawner.accumulator += rate * (spawner.particleCount / spawner.duration) * 3 * deltaTime; const count = Math.floor(spawner.accumulator); spawner.accumulator -= count; for (let i = 0; i < count; i += 1) { spawnParticle(); } spawner.elapsed += deltaTime; } else { spawners.splice(s, 1); } } for (let i = particles.length - 1; i >= 0; i -= 1) { const p = particles[i]!; p.vy += GRAVITY * dt; p.vx += (Math.random() - 0.5) * DRIFT_STRENGTH * dt; p.vx *= Math.pow(VX_DAMPING, dt); p.swayPhase += p.swaySpeed * dt; p.x += (p.vx + Math.sin(p.swayPhase) * SWAY_AMPLITUDE) * dt; p.y += p.vy * dt; p.rotation += p.rotationSpeed * dt; if (p.y > TOTAL_HEIGHT + OFFSCREEN_MARGIN) { particles.splice(i, 1); } } }; export const useConfetti = () => ({ particles, spawn, update });