Confetti.tsx93 lines · main
1'use client'
2
3import { useEffect, useRef } from 'react'
4
5interface Particle {
6 x: number
7 y: number
8 vx: number
9 vy: number
10 size: number
11 color: string
12 rotation: number
13 rotationSpeed: number
14 opacity: number
15}
16
17const COLORS = ['#15803d', '#16a34a', '#22c55e', '#4ade80', '#86efac']
18
19export default function Confetti() {
20 const canvasRef = useRef<HTMLCanvasElement>(null)
21
22 useEffect(() => {
23 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
24
25 const canvas = canvasRef.current
26 if (!canvas) return
27
28 const ctx = canvas.getContext('2d')
29 if (!ctx) return
30
31 const rect = canvas.getBoundingClientRect()
32 canvas.width = rect.width
33 canvas.height = rect.height
34
35 const particles: Particle[] = []
36 const count = 80
37
38 for (let i = 0; i < count; i++) {
39 particles.push({
40 x: Math.random() * canvas.width,
41 y: -(Math.random() * canvas.height * 0.5),
42 vx: (Math.random() - 0.5) * 2,
43 vy: Math.random() * 1.5 + 0.5,
44 size: Math.random() * 6 + 3,
45 color: COLORS[Math.floor(Math.random() * COLORS.length)],
46 rotation: Math.random() * Math.PI * 2,
47 rotationSpeed: (Math.random() - 0.5) * 0.1,
48 opacity: Math.random() * 0.4 + 0.6,
49 })
50 }
51
52 let frame: number
53
54 function animate() {
55 ctx!.clearRect(0, 0, canvas!.width, canvas!.height)
56
57 let alive = false
58 for (const p of particles) {
59 if (p.opacity <= 0) continue
60 alive = true
61
62 p.x += p.vx
63 p.vy += 0.02
64 p.y += p.vy
65 p.rotation += p.rotationSpeed
66 p.opacity -= 0.003
67
68 ctx!.save()
69 ctx!.translate(p.x, p.y)
70 ctx!.rotate(p.rotation)
71 ctx!.globalAlpha = Math.max(0, p.opacity)
72 ctx!.fillStyle = p.color
73 ctx!.fillRect(-p.size * 0.15, -p.size / 2, p.size * 0.3, p.size)
74 ctx!.restore()
75 }
76
77 if (alive) {
78 frame = requestAnimationFrame(animate)
79 }
80 }
81
82 frame = requestAnimationFrame(animate)
83 return () => cancelAnimationFrame(frame)
84 }, [])
85
86 return (
87 <canvas
88 ref={canvasRef}
89 className="absolute inset-0 w-full h-full pointer-events-none z-10"
90 aria-hidden
91 />
92 )
93}