103 lines
2.5 KiB
TypeScript
103 lines
2.5 KiB
TypeScript
import { useEffect, useMemo, useState, useId } from "react";
|
|
|
|
type ConfettiBurstProps = {
|
|
className?: string;
|
|
colors?: string[];
|
|
count?: number;
|
|
};
|
|
|
|
type Piece = {
|
|
id: number;
|
|
style: React.CSSProperties & ConfettiVars;
|
|
};
|
|
|
|
type ConfettiVars = {
|
|
["--x0"]: string;
|
|
["--y0"]: string;
|
|
["--x1"]: string;
|
|
["--y1"]: string;
|
|
};
|
|
|
|
const hashStringToUint32 = (value: string) => {
|
|
// FNV-1a 32-bit
|
|
let hash = 2166136261;
|
|
for (let i = 0; i < value.length; i++) {
|
|
hash ^= value.charCodeAt(i);
|
|
hash = Math.imul(hash, 16777619);
|
|
}
|
|
return hash >>> 0;
|
|
};
|
|
|
|
const mulberry32 = (seed: number) => {
|
|
return () => {
|
|
let t = (seed += 0x6d2b79f5);
|
|
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
};
|
|
};
|
|
|
|
export const ConfettiBurst = ({
|
|
className,
|
|
colors = [
|
|
"var(--color-primary)",
|
|
"var(--color-accent)",
|
|
"var(--color-primary-light)",
|
|
"var(--color-accent-light)",
|
|
],
|
|
count = 18,
|
|
}: ConfettiBurstProps) => {
|
|
const [visible, setVisible] = useState(true);
|
|
const instanceId = useId();
|
|
|
|
useEffect(() => {
|
|
const t = window.setTimeout(() => setVisible(false), 1100);
|
|
return () => window.clearTimeout(t);
|
|
}, []);
|
|
|
|
const pieces = useMemo<Piece[]>(() => {
|
|
const seed = hashStringToUint32(`${instanceId}:${count}:${colors.join(",")}`);
|
|
const rand = mulberry32(seed);
|
|
const rnd = (min: number, max: number) => min + rand() * (max - min);
|
|
|
|
return Array.from({ length: count }).map((_, i) => {
|
|
const x0 = rnd(-6, 6);
|
|
const y0 = rnd(-6, 6);
|
|
const x1 = rnd(-160, 160);
|
|
const y1 = rnd(60, 220);
|
|
const delay = rnd(0, 120);
|
|
const rotate = rnd(0, 360);
|
|
const color = colors[i % colors.length];
|
|
|
|
return {
|
|
id: i,
|
|
style: {
|
|
left: "50%",
|
|
top: "0%",
|
|
backgroundColor: color,
|
|
transform: `translate(${x0}px, ${y0}px) rotate(${rotate}deg)`,
|
|
animationDelay: `${delay}ms`,
|
|
// consumed by keyframes
|
|
["--x0"]: `${x0}px`,
|
|
["--y0"]: `${y0}px`,
|
|
["--x1"]: `${x1}px`,
|
|
["--y1"]: `${y1}px`,
|
|
},
|
|
};
|
|
});
|
|
}, [colors, count, instanceId]);
|
|
|
|
if (!visible) return null;
|
|
|
|
return (
|
|
<div
|
|
className={`pointer-events-none absolute inset-0 overflow-visible ${className ?? ""}`}
|
|
aria-hidden
|
|
>
|
|
{pieces.map((p) => (
|
|
<span key={p.id} className="lila-confetti-piece" style={p.style} />
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|