'use client'; import * as React from 'react'; import { cn } from '@workspace/ui/lib/utils'; const rand = (min: number, max: number): number => Math.random() * (max - min) + min; const randInt = (min: number, max: number): number => Math.floor(Math.random() * (max - min) + min); const randColor = (): string => `hsl(${randInt(0, 360)}, 100%, 50%)`; type ParticleType = { x: number; y: number; color: string; speed: number; direction: number; vx: number; vy: number; gravity: number; friction: number; alpha: number; decay: number; size: number; update: () => void; draw: (ctx: CanvasRenderingContext2D) => void; isAlive: () => boolean; }; function createParticle( x: number, y: number, color: string, speed: number, direction: number, gravity: number, friction: number, size: number, ): ParticleType { const vx = Math.cos(direction) * speed; const vy = Math.sin(direction) * speed; const alpha = 1; const decay = rand(0.005, 0.02); return { x, y, color, speed, direction, vx, vy, gravity, friction, alpha, decay, size, update() { this.vx *= this.friction; this.vy *= this.friction; this.vy += this.gravity; this.x += this.vx; this.y += this.vy; this.alpha -= this.decay; }, draw(ctx: CanvasRenderingContext2D) { ctx.save(); ctx.globalAlpha = this.alpha; ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fillStyle = this.color; ctx.fill(); ctx.restore(); }, isAlive() { return this.alpha > 0; }, }; } type FireworkType = { x: number; y: number; targetY: number; color: string; speed: number; size: number; angle: number; vx: number; vy: number; trail: { x: number; y: number }[]; trailLength: number; exploded: boolean; update: () => boolean; explode: () => void; draw: (ctx: CanvasRenderingContext2D) => void; }; function createFirework( x: number, y: number, targetY: number, color: string, speed: number, size: number, particleSpeed: { min: number; max: number } | number, particleSize: { min: number; max: number } | number, onExplode: (particles: ParticleType[]) => void, ): FireworkType { const angle = -Math.PI / 2 + rand(-0.3, 0.3); const vx = Math.cos(angle) * speed; const vy = Math.sin(angle) * speed; const trail: { x: number; y: number }[] = []; const trailLength = randInt(10, 25); return { x, y, targetY, color, speed, size, angle, vx, vy, trail, trailLength, exploded: false, update() { this.trail.push({ x: this.x, y: this.y }); if (this.trail.length > this.trailLength) { this.trail.shift(); } this.x += this.vx; this.y += this.vy; this.vy += 0.02; if (this.vy >= 0 || this.y <= this.targetY) { this.explode(); return false; } return true; }, explode() { const numParticles = randInt(50, 150); const particles: ParticleType[] = []; for (let i = 0; i < numParticles; i++) { const particleAngle = rand(0, Math.PI * 2); const localParticleSpeed = getValueByRange(particleSpeed); const localParticleSize = getValueByRange(particleSize); particles.push( createParticle( this.x, this.y, this.color, localParticleSpeed, particleAngle, 0.05, 0.98, localParticleSize, ), ); } onExplode(particles); }, draw(ctx: CanvasRenderingContext2D) { ctx.save(); ctx.beginPath(); if (this.trail.length > 1) { ctx.moveTo(this.trail[0]?.x ?? this.x, this.trail[0]?.y ?? this.y); for (const point of this.trail) { ctx.lineTo(point.x, point.y); } } else { ctx.moveTo(this.x, this.y); ctx.lineTo(this.x, this.y); } ctx.strokeStyle = this.color; ctx.lineWidth = this.size; ctx.lineCap = 'round'; ctx.stroke(); ctx.restore(); }, }; } function getValueByRange(range: { min: number; max: number } | number): number { if (typeof range === 'number') { return range; } return rand(range.min, range.max); } function getColor(color: string | string[] | undefined): string { if (Array.isArray(color)) { return color[randInt(0, color.length)] ?? randColor(); } return color ?? randColor(); } type FireworksBackgroundProps = Omit, 'color'> & { canvasProps?: React.ComponentProps<'canvas'>; population?: number; color?: string | string[]; fireworkSpeed?: { min: number; max: number } | number; fireworkSize?: { min: number; max: number } | number; particleSpeed?: { min: number; max: number } | number; particleSize?: { min: number; max: number } | number; }; function FireworksBackground({ ref, className, canvasProps, population = 1, color, fireworkSpeed = { min: 4, max: 8 }, fireworkSize = { min: 2, max: 5 }, particleSpeed = { min: 2, max: 7 }, particleSize = { min: 1, max: 5 }, ...props }: FireworksBackgroundProps) { const canvasRef = React.useRef(null); const containerRef = React.useRef(null); React.useImperativeHandle(ref, () => containerRef.current as HTMLDivElement); React.useEffect(() => { const canvas = canvasRef.current; const container = containerRef.current; if (!canvas || !container) return; const ctx = canvas.getContext('2d'); if (!ctx) return; let maxX = window.innerWidth; let ratio = container.offsetHeight / container.offsetWidth; let maxY = maxX * ratio; canvas.width = maxX; canvas.height = maxY; const setCanvasSize = () => { maxX = window.innerWidth; ratio = container.offsetHeight / container.offsetWidth; maxY = maxX * ratio; canvas.width = maxX; canvas.height = maxY; }; window.addEventListener('resize', setCanvasSize); const explosions: ParticleType[] = []; const fireworks: FireworkType[] = []; const handleExplosion = (particles: ParticleType[]) => { explosions.push(...particles); }; const launchFirework = () => { const x = rand(maxX * 0.1, maxX * 0.9); const y = maxY; const targetY = rand(maxY * 0.1, maxY * 0.4); const fireworkColor = getColor(color); const speed = getValueByRange(fireworkSpeed); const size = getValueByRange(fireworkSize); fireworks.push( createFirework( x, y, targetY, fireworkColor, speed, size, particleSpeed, particleSize, handleExplosion, ), ); const timeout = rand(300, 800) / population; setTimeout(launchFirework, timeout); }; launchFirework(); let animationFrameId: number; const animate = () => { ctx.clearRect(0, 0, maxX, maxY); for (let i = fireworks.length - 1; i >= 0; i--) { const firework = fireworks[i]; if (!firework?.update()) { fireworks.splice(i, 1); } else { firework.draw(ctx); } } for (let i = explosions.length - 1; i >= 0; i--) { const particle = explosions[i]; particle?.update(); if (particle?.isAlive()) { particle.draw(ctx); } else { explosions.splice(i, 1); } } animationFrameId = requestAnimationFrame(animate); }; animate(); const handleClick = (event: MouseEvent) => { const x = event.clientX; const y = maxY; const targetY = event.clientY; const fireworkColor = getColor(color); const speed = getValueByRange(fireworkSpeed); const size = getValueByRange(fireworkSize); fireworks.push( createFirework( x, y, targetY, fireworkColor, speed, size, particleSpeed, particleSize, handleExplosion, ), ); }; container.addEventListener('click', handleClick); return () => { window.removeEventListener('resize', setCanvasSize); container.removeEventListener('click', handleClick); cancelAnimationFrame(animationFrameId); }; }, [ population, color, fireworkSpeed, fireworkSize, particleSpeed, particleSize, ]); return (
); } export { FireworksBackground, type FireworksBackgroundProps };