Fireworks
Rockets rise from the bottom of a section and burst into showers of sparks that droop and fade. Click anywhere inside to launch one at that spot.
Click anywhere
How to use it
- Paste the snippet just before
</head>. - Add
data-fireworksto a section with a dark background.
Options
Add any of these attributes to the same element.
data-rate | Time between launches. Default 900 ms. 0 launches only on click. |
data-colors | Comma-separated colours. |
data-sparks | Sparks per burst. Default 70. |
Drawn on a canvas behind your content and paused when off screen. From code, animacious.fireworks(section, x, y) bursts one at a point.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Fireworks: rockets rise from the bottom of a section and burst into
// showers of sparks that droop and fade. Click inside to launch one at that
// spot. Drawn on a canvas behind your content, and paused when off screen.
// Usage: <section data-fireworks style="background:#0b0b1a">…</section>
// Optional: data-rate (ms between launches, default 900; 0 = only on
// click), data-colors (comma-separated), data-sparks (per burst, default 70).
// Script use: animacious.fireworks(element, x, y) bursts one at a point
// (x and y in px from the element's top-left; centre if left out).
var DEFAULTS = { rate: 900, sparks: 70, colors: ['#a855f7', '#ec4899', '#facc15', '#38bdf8', '#4ade80', '#ffffff'] };
var GRAVITY = 0.0008; // px per ms² for a 1000px-tall section, scaled to its height
var css =
'[data-fireworks]{position:relative;isolation:isolate;overflow:hidden}' +
'.fwx-canvas{position:absolute;inset:0;width:100%;height:100%;z-index:-1;pointer-events:none}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function build(el) {
var d = el.dataset;
var canvas = document.createElement('canvas');
canvas.className = 'fwx-canvas';
canvas.setAttribute('aria-hidden', 'true');
el.insertBefore(canvas, el.firstChild);
var ctx = canvas.getContext('2d');
var colors = d.colors ? d.colors.split(',').map(function (c) { return c.trim(); }) : DEFAULTS.colors;
var sparksPer = +d.sparks || DEFAULTS.sparks;
var rate = d.rate !== undefined ? +d.rate : DEFAULTS.rate;
var w = 0, h = 0, dpr = 1, unit = 1;
var rockets = [], sparks = [], sinceLaunch = rate;
function resize() {
dpr = Math.min(2, window.devicePixelRatio || 1);
w = el.clientWidth;
h = el.clientHeight;
unit = Math.max(0.35, h / 1000);
canvas.width = w * dpr;
canvas.height = h * dpr;
}
resize();
if (window.ResizeObserver) new ResizeObserver(resize).observe(el);
function pick() { return colors[(Math.random() * colors.length) | 0]; }
function launch(tx, ty) {
tx = tx === undefined ? w * (0.2 + Math.random() * 0.6) : tx;
ty = ty === undefined ? h * (0.15 + Math.random() * 0.3) : ty;
var x = tx + (Math.random() - 0.5) * w * 0.1;
// Speed it needs to just reach the target height under gravity.
var g = GRAVITY * unit;
var vy = -Math.sqrt(2 * g * (h - ty));
var t = -vy / g;
rockets.push({ x: x, y: h, vx: (tx - x) / t, vy: vy, color: pick() });
}
function burst(x, y, color) {
var n = sparksPer;
for (var i = 0; i < n; i++) {
var a = (i / n) * Math.PI * 2 + Math.random() * 0.2;
var s = (0.05 + Math.random() * 0.12) * Math.max(0.6, unit * 1.6);
sparks.push({
x: x, y: y, px: x, py: y, vx: Math.cos(a) * s, vy: Math.sin(a) * s,
life: 0, max: 900 + Math.random() * 700,
color: Math.random() < 0.8 ? color : pick()
});
}
}
function step(dt) {
var g = GRAVITY * unit;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
ctx.lineCap = 'round';
if (rate > 0) {
sinceLaunch += dt;
if (sinceLaunch >= rate) { sinceLaunch = Math.random() * rate * 0.4; launch(); }
}
rockets = rockets.filter(function (r) {
var px = r.x, py = r.y;
r.vy += g * dt;
r.x += r.vx * dt;
r.y += r.vy * dt;
ctx.globalAlpha = 1;
ctx.strokeStyle = r.color;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(px, py + (py - r.y) * 3);
ctx.lineTo(r.x, r.y);
ctx.stroke();
if (r.vy >= 0) { burst(r.x, r.y, r.color); return false; }
return true;
});
sparks = sparks.filter(function (s) {
s.life += dt;
if (s.life >= s.max) return false;
s.px = s.x; s.py = s.y;
var drag = Math.pow(0.985, dt / 16);
s.vx *= drag; s.vy = s.vy * drag + g * dt * 0.6;
s.x += s.vx * dt;
s.y += s.vy * dt;
var k = 1 - s.life / s.max;
// Twinkle a little as they die out.
ctx.globalAlpha = k * (k < 0.4 ? 0.5 + Math.random() * 0.5 : 1);
ctx.strokeStyle = s.color;
ctx.lineWidth = 1 + 1.6 * k;
ctx.beginPath();
ctx.moveTo(s.px - s.vx * dt * 2, s.py - s.vy * dt * 2);
ctx.lineTo(s.x, s.y);
ctx.stroke();
return true;
});
}
var last = 0;
var timer = anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var now = performance.now();
var dt = last ? Math.min(50, now - last) : 16;
last = now;
step(dt);
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}).observe(el);
el.addEventListener('click', function (e) {
if (e.target.closest('a,button,input,select,textarea,label')) return;
var r = el.getBoundingClientRect();
launch(e.clientX - r.left, e.clientY - r.top);
});
el._fwx = {
burst: function (x, y) { burst(x === undefined ? w / 2 : x, y === undefined ? h / 2 : y, pick()); }
};
}
function init() {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
document.querySelectorAll('[data-fireworks]').forEach(function (el) {
if (el._fwx) return;
build(el);
});
}
window.animacious = window.animacious || {};
window.animacious.fireworks = function (el, x, y) { if (el && el._fwx) el._fwx.burst(x, y); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><section data-fireworks style="background: #0b0b1a; color: #fff">
<h1>Happy launch day</h1>
</section>