Canvas Fireworks Effect
An interactive particle effect that bursts into colorful fireworks when you click the night sky.
With JavaScript enabled, you can edit, run, and save this example. The fixed source is as follows:
<!doctype html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Canvas Fireworks Effect</title>
<style>
* { box-sizing: border-box; }
body { min-height: 100vh; margin: 0; overflow: hidden; color: #fff; font-family: "Avenir Next", "Microsoft YaHei", sans-serif; background: radial-gradient(ellipse at bottom, #17234b, #080b1b 60%, #03040a); }
body::after { content: ""; position: fixed; left: 0; right: 0; bottom: 0; height: 17vh; background: #03050b; clip-path: polygon(0 62%, 7% 62%, 8% 28%, 10% 28%, 11% 62%, 18% 62%, 20% 45%, 24% 45%, 25% 62%, 36% 62%, 38% 18%, 41% 18%, 42% 62%, 55% 62%, 57% 38%, 61% 38%, 62% 62%, 73% 62%, 75% 12%, 78% 12%, 79% 62%, 88% 62%, 90% 44%, 94% 44%, 95% 62%, 100% 62%, 100% 100%, 0 100%); pointer-events: none; }
canvas { position: fixed; inset: 0; width: 100%; height: 100%; cursor: crosshair; touch-action: none; }
canvas:focus-visible { outline: 3px solid #f4ce6b; outline-offset: -6px; }
header { position: fixed; z-index: 2; top: clamp(28px, 7vh, 70px); left: 50%; width: min(680px, calc(100% - 40px)); text-align: center; transform: translateX(-50%); pointer-events: none; }
.eyebrow { color: #f4ce6b; font-size: 11px; font-weight: 900; letter-spacing: .25em; }
h1 { margin: 10px 0 12px; font: 500 clamp(43px, 9vw, 84px)/1 Georgia, "STSong", serif; text-shadow: 0 8px 36px rgba(0,0,0,.6); }
p { margin: 0; color: #aeb8d2; font-size: 14px; letter-spacing: .05em; }
.hint { position: fixed; z-index: 3; left: 50%; bottom: 28px; padding: 9px 15px; border: 1px solid rgba(255,255,255,.18); border-radius: 999px; color: #c8d0e4; font-size: 11px; background: rgba(5,8,18,.55); transform: translateX(-50%); pointer-events: none; }
</style>
</head>
<body>
<canvas id="fireworks" role="button" tabindex="0" aria-label="Click the night sky or press Enter to launch fireworks"></canvas>
<header><span class="eyebrow">MAKE A LITTLE CELEBRATION</span><h1>Light Up Your Wish</h1><p>Every click generates a one-of-a-kind firework.</p></header>
<div class="hint">Click or touch the night sky</div>
<script>
const canvas = document.querySelector('#fireworks');
const context = canvas.getContext('2d');
const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
let width = 0, height = 0, rockets = [], particles = [], lastAuto = 0;
const colors = ['#ff5e7d', '#ffd166', '#63e6ff', '#9b8cff', '#73e2a7'];
function resize() {
const ratio = Math.min(devicePixelRatio || 1, 2);
width = innerWidth; height = innerHeight;
canvas.width = Math.round(width * ratio); canvas.height = Math.round(height * ratio);
context.setTransform(ratio, 0, 0, ratio, 0, 0);
}
function launch(targetX, targetY) {
rockets.push({ x: width / 2 + (Math.random() - .5) * 100, y: height, targetX, targetY, speed: 7 + Math.random() * 2, hue: colors[Math.floor(Math.random() * colors.length)] });
}
function explode(rocket) {
const count = reduceMotion ? 28 : 70;
for (let index = 0; index < count; index += 1) {
const angle = Math.PI * 2 * index / count + Math.random() * .08;
const speed = 1.5 + Math.random() * 4.8;
particles.push({ x: rocket.x, y: rocket.y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, life: 1, decay: .012 + Math.random() * .012, color: rocket.hue });
}
}
function frame(time) {
context.fillStyle = 'rgba(3, 4, 10, .2)'; context.fillRect(0, 0, width, height);
rockets = rockets.filter(rocket => {
const dx = rocket.targetX - rocket.x, dy = rocket.targetY - rocket.y, distance = Math.hypot(dx, dy);
if (distance < rocket.speed) { explode(rocket); return false; }
rocket.x += dx / distance * rocket.speed; rocket.y += dy / distance * rocket.speed;
context.fillStyle = rocket.hue; context.beginPath(); context.arc(rocket.x, rocket.y, 2.2, 0, Math.PI * 2); context.fill(); return true;
});
particles = particles.filter(particle => {
particle.vy += .035; particle.vx *= .99; particle.x += particle.vx; particle.y += particle.vy; particle.life -= particle.decay;
if (particle.life <= 0) return false;
context.globalAlpha = particle.life; context.fillStyle = particle.color; context.beginPath(); context.arc(particle.x, particle.y, 1.8, 0, Math.PI * 2); context.fill(); return true;
});
context.globalAlpha = 1;
if (!reduceMotion && time - lastAuto > 2200) { launch(width * (.2 + Math.random() * .6), height * (.25 + Math.random() * .35)); lastAuto = time; }
requestAnimationFrame(frame);
}
canvas.addEventListener('pointerdown', event => { const rect = canvas.getBoundingClientRect(); launch(event.clientX - rect.left, event.clientY - rect.top); });
canvas.addEventListener('keydown', event => {
if (event.key !== 'Enter' && event.key !== ' ') return;
event.preventDefault();
launch(width * (.3 + Math.random() * .4), height * (.3 + Math.random() * .25));
});
addEventListener('resize', resize); resize(); launch(width * .5, height * .4); requestAnimationFrame(frame);
</script>
</body>
</html>