Skip to content

Commit 37b07db

Browse files
committed
merge: ConfettiAnimation — canvas overlay confetti con hook useConfetti(), auto-dismiss 3s
2 parents af9b070 + 25e2901 commit 37b07db

1 file changed

Lines changed: 133 additions & 0 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
'use client'
2+
3+
import { useCallback, useEffect, useRef, useState } from 'react'
4+
5+
/* ── Particella ── */
6+
interface Particle {
7+
x: number; y: number
8+
vx: number; vy: number
9+
color: string
10+
size: number
11+
rotation: number
12+
rotSpeed: number
13+
shape: 'rect' | 'circle' | 'ribbon'
14+
alpha: number
15+
}
16+
17+
const COLORS = ['#00e87a','#4ade80','#facc15','#fb923c','#f472b6','#60a5fa','#a78bfa','#fff']
18+
19+
function makeParticle(canvasW: number): Particle {
20+
return {
21+
x: Math.random() * canvasW,
22+
y: -10 - Math.random() * 40,
23+
vx: (Math.random() - 0.5) * 3,
24+
vy: 2.5 + Math.random() * 3.5,
25+
color: COLORS[Math.floor(Math.random() * COLORS.length)],
26+
size: 5 + Math.random() * 8,
27+
rotation: Math.random() * Math.PI * 2,
28+
rotSpeed: (Math.random() - 0.5) * 0.2,
29+
shape: (['rect','rect','circle','ribbon'] as const)[Math.floor(Math.random() * 4)],
30+
alpha: 1,
31+
}
32+
}
33+
34+
function drawParticle(ctx: CanvasRenderingContext2D, p: Particle) {
35+
ctx.save()
36+
ctx.globalAlpha = p.alpha
37+
ctx.fillStyle = p.color
38+
ctx.translate(p.x, p.y)
39+
ctx.rotate(p.rotation)
40+
if (p.shape === 'circle') {
41+
ctx.beginPath(); ctx.arc(0, 0, p.size / 2, 0, Math.PI * 2); ctx.fill()
42+
} else if (p.shape === 'ribbon') {
43+
ctx.fillRect(-p.size / 2, -p.size / 6, p.size, p.size / 3)
44+
} else {
45+
ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size)
46+
}
47+
ctx.restore()
48+
}
49+
50+
/* ── Canvas component ── */
51+
interface ConfettiCanvasProps { onDone: () => void; duration?: number }
52+
53+
function ConfettiCanvas({ onDone, duration = 3000 }: ConfettiCanvasProps) {
54+
const canvasRef = useRef<HTMLCanvasElement>(null)
55+
const rafRef = useRef<number>(0)
56+
const startRef = useRef<number>(0)
57+
const particles = useRef<Particle[]>([])
58+
59+
useEffect(() => {
60+
const canvas = canvasRef.current
61+
if (!canvas) return
62+
const ctx = canvas.getContext('2d')
63+
if (!ctx) return
64+
65+
const resize = () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight }
66+
resize()
67+
window.addEventListener('resize', resize)
68+
69+
// Burst iniziale: 120 particelle
70+
for (let i = 0; i < 120; i++) particles.current.push(makeParticle(canvas.width))
71+
72+
const tick = (now: number) => {
73+
if (!startRef.current) startRef.current = now
74+
const elapsed = now - startRef.current
75+
const fadeStart = duration * 0.55
76+
ctx.clearRect(0, 0, canvas.width, canvas.height)
77+
78+
particles.current.forEach(p => {
79+
p.x += p.vx; p.y += p.vy
80+
p.vy += 0.08 // gravità
81+
p.vx *= 0.995 // attrito aria
82+
p.rotation += p.rotSpeed
83+
if (elapsed > fadeStart) p.alpha = Math.max(0, 1 - (elapsed - fadeStart) / (duration - fadeStart))
84+
drawParticle(ctx, p)
85+
})
86+
87+
// Rimuovi particelle uscite dallo schermo
88+
particles.current = particles.current.filter(p => p.y < canvas.height + 20 && p.alpha > 0.01)
89+
90+
if (elapsed < duration) {
91+
// Aggiungi nuove particelle nei primi 600ms
92+
if (elapsed < 600 && Math.random() < 0.6) particles.current.push(makeParticle(canvas.width))
93+
rafRef.current = requestAnimationFrame(tick)
94+
} else {
95+
onDone()
96+
}
97+
}
98+
99+
rafRef.current = requestAnimationFrame(tick)
100+
return () => {
101+
cancelAnimationFrame(rafRef.current)
102+
window.removeEventListener('resize', resize)
103+
}
104+
}, [duration, onDone])
105+
106+
return (
107+
<canvas
108+
ref={canvasRef}
109+
style={{ position: 'fixed', inset: 0, zIndex: 9999, pointerEvents: 'none' }}
110+
/>
111+
)
112+
}
113+
114+
/* ── Hook ── */
115+
export function useConfetti() {
116+
const [active, setActive] = useState(false)
117+
const fire = useCallback(() => setActive(true), [])
118+
const dismiss = useCallback(() => setActive(false), [])
119+
return { fire, dismiss, active }
120+
}
121+
122+
/* ── Componente esportabile ── */
123+
export interface ConfettiAnimationProps {
124+
active: boolean
125+
onDone?: () => void
126+
duration?: number
127+
}
128+
129+
export default function ConfettiAnimation({ active, onDone, duration = 3000 }: ConfettiAnimationProps) {
130+
const handleDone = useCallback(() => { onDone?.() }, [onDone])
131+
if (!active) return null
132+
return <ConfettiCanvas onDone={handleDone} duration={duration} />
133+
}

0 commit comments

Comments
 (0)