Skip to content

Instantly share code, notes, and snippets.

@eznj
Created May 19, 2026 08:40
Show Gist options
  • Select an option

  • Save eznj/fe513f535eeb0cba7f12a165b0653518 to your computer and use it in GitHub Desktop.

Select an option

Save eznj/fe513f535eeb0cba7f12a165b0653518 to your computer and use it in GitHub Desktop.
"use client";
import { useEffect, useRef, useCallback } from "react";
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
alpha: number;
color: string;
decay: number;
size: number;
}
export default function Fireworks() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const particlesRef = useRef<Particle[]>([]);
const rafRef = useRef<number>(0);
const randomColor = useCallback(() => {
const colors = [
"#ff6b6b",
"#feca57",
"#48dbfb",
"#ff9ff3",
"#54a0ff",
"#5f27cd",
"#01a3a4",
"#f368e0",
"#ff9f43",
"#ee5a24",
"#00d2d3",
"#1dd1a1",
"#ffd32a",
"#ff6348",
];
return colors[Math.floor(Math.random() * colors.length)];
}, []);
const explode = useCallback(
(x: number, y: number) => {
const count = 60 + Math.floor(Math.random() * 40);
const color = randomColor();
for (let i = 0; i < count; i++) {
const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.5;
const speed = 2 + Math.random() * 4;
particlesRef.current.push({
x,
y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
alpha: 1,
color,
decay: 0.012 + Math.random() * 0.015,
size: 2 + Math.random() * 2,
});
}
},
[randomColor]
);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const resize = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
resize();
window.addEventListener("resize", resize);
// Launch initial burst of fireworks
const launchCount = 8;
let launched = 0;
const launchInterval = setInterval(() => {
explode(
window.innerWidth * 0.2 + Math.random() * window.innerWidth * 0.6,
window.innerHeight * 0.15 + Math.random() * window.innerHeight * 0.35
);
launched++;
if (launched >= launchCount) clearInterval(launchInterval);
}, 200);
// Animate
const animate = () => {
ctx.fillStyle = "rgba(0, 0, 0, 0.15)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
particlesRef.current = particlesRef.current.filter((p) => p.alpha > 0.01);
for (const p of particlesRef.current) {
p.x += p.vx;
p.y += p.vy;
p.vy += 0.04; // gravity
p.vx *= 0.99;
p.alpha -= p.decay;
ctx.globalAlpha = Math.max(0, p.alpha);
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
rafRef.current = requestAnimationFrame(animate);
};
rafRef.current = requestAnimationFrame(animate);
return () => {
window.removeEventListener("resize", resize);
cancelAnimationFrame(rafRef.current);
clearInterval(launchInterval);
};
}, [explode]);
return (
<canvas
ref={canvasRef}
className="fixed inset-0 pointer-events-none"
style={{ zIndex: 0 }}
/>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment