15 lines
9.5 KiB
JSON
15 lines
9.5 KiB
JSON
{
|
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
"name": "fireworks-background",
|
|
"type": "registry:ui",
|
|
"title": "Fireworks Background",
|
|
"description": "A background component that displays a fireworks animation.",
|
|
"files": [
|
|
{
|
|
"path": "registry/backgrounds/fireworks/index.tsx",
|
|
"content": "'use client';\n\nimport * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nconst rand = (min: number, max: number): number =>\n Math.random() * (max - min) + min;\n\nconst randInt = (min: number, max: number): number =>\n Math.floor(Math.random() * (max - min) + min);\n\nconst randColor = (): string => `hsl(${randInt(0, 360)}, 100%, 50%)`;\n\ntype ParticleType = {\n x: number;\n y: number;\n color: string;\n speed: number;\n direction: number;\n vx: number;\n vy: number;\n gravity: number;\n friction: number;\n alpha: number;\n decay: number;\n size: number;\n update: () => void;\n draw: (ctx: CanvasRenderingContext2D) => void;\n isAlive: () => boolean;\n};\n\nfunction createParticle(\n x: number,\n y: number,\n color: string,\n speed: number,\n direction: number,\n gravity: number,\n friction: number,\n size: number,\n): ParticleType {\n const vx = Math.cos(direction) * speed;\n const vy = Math.sin(direction) * speed;\n const alpha = 1;\n const decay = rand(0.005, 0.02);\n\n return {\n x,\n y,\n color,\n speed,\n direction,\n vx,\n vy,\n gravity,\n friction,\n alpha,\n decay,\n size,\n update() {\n this.vx *= this.friction;\n this.vy *= this.friction;\n this.vy += this.gravity;\n this.x += this.vx;\n this.y += this.vy;\n this.alpha -= this.decay;\n },\n draw(ctx: CanvasRenderingContext2D) {\n ctx.save();\n ctx.globalAlpha = this.alpha;\n ctx.beginPath();\n ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);\n ctx.fillStyle = this.color;\n ctx.fill();\n ctx.restore();\n },\n isAlive() {\n return this.alpha > 0;\n },\n };\n}\n\ntype FireworkType = {\n x: number;\n y: number;\n targetY: number;\n color: string;\n speed: number;\n size: number;\n angle: number;\n vx: number;\n vy: number;\n trail: { x: number; y: number }[];\n trailLength: number;\n exploded: boolean;\n update: () => boolean;\n explode: () => void;\n draw: (ctx: CanvasRenderingContext2D) => void;\n};\n\nfunction createFirework(\n x: number,\n y: number,\n targetY: number,\n color: string,\n speed: number,\n size: number,\n particleSpeed: { min: number; max: number } | number,\n particleSize: { min: number; max: number } | number,\n onExplode: (particles: ParticleType[]) => void,\n): FireworkType {\n const angle = -Math.PI / 2 + rand(-0.3, 0.3);\n const vx = Math.cos(angle) * speed;\n const vy = Math.sin(angle) * speed;\n const trail: { x: number; y: number }[] = [];\n const trailLength = randInt(10, 25);\n\n return {\n x,\n y,\n targetY,\n color,\n speed,\n size,\n angle,\n vx,\n vy,\n trail,\n trailLength,\n exploded: false,\n update() {\n this.trail.push({ x: this.x, y: this.y });\n if (this.trail.length > this.trailLength) {\n this.trail.shift();\n }\n this.x += this.vx;\n this.y += this.vy;\n this.vy += 0.02;\n if (this.vy >= 0 || this.y <= this.targetY) {\n this.explode();\n return false;\n }\n return true;\n },\n explode() {\n const numParticles = randInt(50, 150);\n const particles: ParticleType[] = [];\n for (let i = 0; i < numParticles; i++) {\n const particleAngle = rand(0, Math.PI * 2);\n const localParticleSpeed = getValueByRange(particleSpeed);\n const localParticleSize = getValueByRange(particleSize);\n particles.push(\n createParticle(\n this.x,\n this.y,\n this.color,\n localParticleSpeed,\n particleAngle,\n 0.05,\n 0.98,\n localParticleSize,\n ),\n );\n }\n onExplode(particles);\n },\n draw(ctx: CanvasRenderingContext2D) {\n ctx.save();\n ctx.beginPath();\n if (this.trail.length > 1) {\n ctx.moveTo(this.trail[0]?.x ?? this.x, this.trail[0]?.y ?? this.y);\n for (const point of this.trail) {\n ctx.lineTo(point.x, point.y);\n }\n } else {\n ctx.moveTo(this.x, this.y);\n ctx.lineTo(this.x, this.y);\n }\n ctx.strokeStyle = this.color;\n ctx.lineWidth = this.size;\n ctx.lineCap = 'round';\n ctx.stroke();\n ctx.restore();\n },\n };\n}\n\nfunction getValueByRange(range: { min: number; max: number } | number): number {\n if (typeof range === 'number') {\n return range;\n }\n return rand(range.min, range.max);\n}\n\nfunction getColor(color: string | string[] | undefined): string {\n if (Array.isArray(color)) {\n return color[randInt(0, color.length)] ?? randColor();\n }\n return color ?? randColor();\n}\n\ntype FireworksBackgroundProps = Omit<React.ComponentProps<'div'>, 'color'> & {\n canvasProps?: React.ComponentProps<'canvas'>;\n population?: number;\n color?: string | string[];\n fireworkSpeed?: { min: number; max: number } | number;\n fireworkSize?: { min: number; max: number } | number;\n particleSpeed?: { min: number; max: number } | number;\n particleSize?: { min: number; max: number } | number;\n};\n\nfunction FireworksBackground({\n ref,\n className,\n canvasProps,\n population = 1,\n color,\n fireworkSpeed = { min: 4, max: 8 },\n fireworkSize = { min: 2, max: 5 },\n particleSpeed = { min: 2, max: 7 },\n particleSize = { min: 1, max: 5 },\n ...props\n}: FireworksBackgroundProps) {\n const canvasRef = React.useRef<HTMLCanvasElement>(null);\n const containerRef = React.useRef<HTMLDivElement>(null);\n React.useImperativeHandle(ref, () => containerRef.current as HTMLDivElement);\n\n React.useEffect(() => {\n const canvas = canvasRef.current;\n const container = containerRef.current;\n if (!canvas || !container) return;\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n let maxX = window.innerWidth;\n let ratio = container.offsetHeight / container.offsetWidth;\n let maxY = maxX * ratio;\n canvas.width = maxX;\n canvas.height = maxY;\n\n const setCanvasSize = () => {\n maxX = window.innerWidth;\n ratio = container.offsetHeight / container.offsetWidth;\n maxY = maxX * ratio;\n canvas.width = maxX;\n canvas.height = maxY;\n };\n window.addEventListener('resize', setCanvasSize);\n\n const explosions: ParticleType[] = [];\n const fireworks: FireworkType[] = [];\n\n const handleExplosion = (particles: ParticleType[]) => {\n explosions.push(...particles);\n };\n\n const launchFirework = () => {\n const x = rand(maxX * 0.1, maxX * 0.9);\n const y = maxY;\n const targetY = rand(maxY * 0.1, maxY * 0.4);\n const fireworkColor = getColor(color);\n const speed = getValueByRange(fireworkSpeed);\n const size = getValueByRange(fireworkSize);\n fireworks.push(\n createFirework(\n x,\n y,\n targetY,\n fireworkColor,\n speed,\n size,\n particleSpeed,\n particleSize,\n handleExplosion,\n ),\n );\n const timeout = rand(300, 800) / population;\n setTimeout(launchFirework, timeout);\n };\n\n launchFirework();\n\n let animationFrameId: number;\n const animate = () => {\n ctx.clearRect(0, 0, maxX, maxY);\n\n for (let i = fireworks.length - 1; i >= 0; i--) {\n const firework = fireworks[i];\n if (!firework?.update()) {\n fireworks.splice(i, 1);\n } else {\n firework.draw(ctx);\n }\n }\n\n for (let i = explosions.length - 1; i >= 0; i--) {\n const particle = explosions[i];\n particle?.update();\n if (particle?.isAlive()) {\n particle.draw(ctx);\n } else {\n explosions.splice(i, 1);\n }\n }\n\n animationFrameId = requestAnimationFrame(animate);\n };\n\n animate();\n\n const handleClick = (event: MouseEvent) => {\n const x = event.clientX;\n const y = maxY;\n const targetY = event.clientY;\n const fireworkColor = getColor(color);\n const speed = getValueByRange(fireworkSpeed);\n const size = getValueByRange(fireworkSize);\n fireworks.push(\n createFirework(\n x,\n y,\n targetY,\n fireworkColor,\n speed,\n size,\n particleSpeed,\n particleSize,\n handleExplosion,\n ),\n );\n };\n\n container.addEventListener('click', handleClick);\n\n return () => {\n window.removeEventListener('resize', setCanvasSize);\n container.removeEventListener('click', handleClick);\n cancelAnimationFrame(animationFrameId);\n };\n }, [\n population,\n color,\n fireworkSpeed,\n fireworkSize,\n particleSpeed,\n particleSize,\n ]);\n\n return (\n <div\n ref={containerRef}\n data-slot=\"fireworks-background\"\n className={cn('relative size-full overflow-hidden', className)}\n {...props}\n >\n <canvas\n {...canvasProps}\n ref={canvasRef}\n className={cn('absolute inset-0 size-full', canvasProps?.className)}\n />\n </div>\n );\n}\n\nexport { FireworksBackground, type FireworksBackgroundProps };\n",
|
|
"type": "registry:ui",
|
|
"target": "components/animate-ui/backgrounds/fireworks.tsx"
|
|
}
|
|
]
|
|
} |