@zakkster/lite-fireworks
v1.0.0
Published
Zero-GC, SoA fireworks engine supporting OKLCH colors, cinematic bloom trails, vector-comet mode, and multi-phase shell explosions. Designed for high-performance displays with no allocations in the hot path.
Maintainers
Readme
@zakkster/lite-fireworks
Zero-GC SoA fireworks engine with OKLCH colors. Bloom trails on dark backgrounds, vector-comets on light. One dependency. 165 lines.
Why lite-fireworks?
| Feature | lite-fireworks | fireworks-js | tsparticles | p5.js | |---|---|---|---|---| | Zero-GC hot path | Yes | No | No | No | | SoA flat arrays | Yes | No | No | No | | OKLCH colors | Yes | No | No | No | | Physics-correct arcs | Yes | Approximate | No | Manual | | Two render modes | Bloom + Vector | One | One | Manual | | Live config | Yes | Partial | Yes | No | | Seeded RNG (optional) | Yes | No | No | No | | Dependencies | 1 | 0 | 10+ | 0 | | Bundle size | < 2KB | ~4KB | ~40KB | ~800KB |
Installation
npm install @zakkster/lite-fireworksQuick Start
import { FireworksEngine } from '@zakkster/lite-fireworks';
const canvas = document.getElementById('stage');
const ctx = canvas.getContext('2d');
const sky = new FireworksEngine(5000);
let w = canvas.width, h = canvas.height;
let last = performance.now();
function loop(time) {
const dt = Math.min((time - last) / 1000, 0.1);
last = time;
sky.updateAndDraw(ctx, dt, w, h);
requestAnimationFrame(loop);
}
// Launch a shell from the bottom, explode at 30% height
sky.launch(w / 2, h, h * 0.3);
requestAnimationFrame(loop);Two Render Modes
lite-fireworks has two distinct visual modes, switched via a single boolean:
Bloom Mode (default — dark backgrounds)
Particles render as glowing dots with additive blending (globalCompositeOperation: 'lighter'). A semi-transparent overlay (fadeColor) creates cinematic bloom trails. This is the classic fireworks look.
const sky = new FireworksEngine(5000, {
transparentBackground: false, // bloom mode
fadeColor: 'rgba(10, 10, 10, 0.25)', // trail persistence
});Vector-Comet Mode (light/transparent backgrounds)
Particles render as velocity-direction strokes with lineCap: 'round'. The canvas is fully cleared each frame. Perfect for overlaying on light-colored pages or photos.
const sky = new FireworksEngine(5000, {
transparentBackground: true, // vector-comet mode
});Full Config Reference
All config values are live-mutable — change them between frames for real-time tuning.
| Option | Type | Default | Description |
|---|---|---|---|
| gravity | number | 250 | Downward acceleration in px/s². Controls shell arc height and star fall speed. |
| friction | number | 0.96 | Per-frame velocity retention (0–1). Lower = stars slow faster. 0.98 = willow effect. |
| starCount | number | 60 | Stars spawned per shell explosion. |
| transparentBackground | boolean | false | false = bloom mode, true = vector-comet mode. |
| fadeColor | string | 'rgba(10,10,10,0.25)' | Fade overlay color for bloom trails. Lower alpha = longer trails. |
| colors | Array | 7 OKLCH defaults | Array of OKLCH objects { l, c, h } or CSS strings. Pre-parsed at construction. |
| rng | Function | Math.random | RNG function () => number in [0, 1). Inject seeded RNG for determinism. |
Particle Physics
Shell Phase (state = 1)
A shell launches from (startX, startY) with velocity computed from real ballistic physics:
vy = -√(2 × gravity × |startY - targetY|)This guarantees the shell reaches exactly targetY before deceleration triggers the explosion. No magic tuning needed — change gravity and the arcs stay physically correct.
Shells receive a slight horizontal wobble (±25 px/s) for natural variation. Shells do not experience drag — they are powered projectiles.
Star Phase (state = 2)
When a shell's vertical velocity crosses -20 px/s (near apex), it dies and spawns starCount stars in a radial burst. Each star gets a random angle, speed (50–350 px/s), and lifetime (1.0–1.2s).
Stars experience full physics: gravity pulls them down, friction decelerates them. The friction coefficient controls the "feel" — 0.92 is sharp and snappy, 0.98 is long and willow-like.
Stars are culled when: life ≤ 0, or they exit the viewport bounds.
Canvas Setup (No Built-in Resize — By Design)
lite-fireworks is a pure engine. It takes (ctx, dt, w, h) and renders — it doesn't own the canvas, doesn't create DOM elements, doesn't listen for resize events. This is intentional:
- The engine stays < 2KB with zero DOM coupling
- You control DPR scaling, ResizeObserver strategy, and canvas lifecycle
- It composes cleanly with lite-viewport or any custom setup
Here's the recommended canvas setup:
import { FireworksEngine } from '@zakkster/lite-fireworks';
const canvas = document.getElementById('stage');
const ctx = canvas.getContext('2d');
const sky = new FireworksEngine();
let w = 0, h = 0;
const dpr = window.devicePixelRatio || 1;
function updateSize() {
w = canvas.clientWidth || window.innerWidth;
h = canvas.clientHeight || window.innerHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr);
}
// RAF-debounced ResizeObserver (prevents layout thrashing)
let scheduled = false;
new ResizeObserver(() => {
if (!scheduled) {
scheduled = true;
requestAnimationFrame(() => { scheduled = false; updateSize(); });
}
}).observe(canvas.parentElement || document.body);
updateSize();
// Game loop
let last = performance.now();
function loop(time) {
const dt = Math.min((time - last) / 1000, 0.1);
last = time;
sky.updateAndDraw(ctx, dt, w, h);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);Or with lite-viewport (even simpler):
import { FireworksEngine } from '@zakkster/lite-fireworks';
import { Viewport } from 'lite-viewport';
const canvas = document.getElementById('stage');
const sky = new FireworksEngine();
let W, H;
const vp = new Viewport({
canvas,
onResize(w, h, dpr) { W = w; H = h; }
});
function loop(time) {
sky.updateAndDraw(vp.ctx, 0.016, W, H);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);Seeded Random (Deterministic Replays)
lite-fireworks uses Math.random by default for zero extra dependencies. For deterministic output, inject @zakkster/lite-random:
import { FireworksEngine } from '@zakkster/lite-fireworks';
import { Random } from '@zakkster/lite-random';
const rng = new Random(42);
const sky = new FireworksEngine(5000, {
rng: () => rng.next(), // inject seeded RNG
});
// Same seed + same launch sequence = identical visual output
sky.launch(400, 600, 200);
sky.launch(200, 600, 150);
// Reset for replay
rng.reset(42);
sky.clear();
sky.launch(400, 600, 200); // exact same firework
sky.launch(200, 600, 150); // exact same fireworkRecipes
const sky = new FireworksEngine(8000);
setInterval(() => {
if (document.hidden) return;
const startX = w * 0.2 + Math.random() * (w * 0.6);
const targetY = h * 0.1 + Math.random() * (h * 0.4);
sky.launch(startX, h, targetY);
}, 800);canvas.addEventListener('click', (e) => {
sky.launch(e.clientX, h, e.clientY);
});const sky = new FireworksEngine(8000, {
gravity: 80,
friction: 0.98,
starCount: 120,
fadeColor: 'rgba(10, 10, 10, 0.12)', // very persistent trails
});const sky = new FireworksEngine(3000, {
gravity: 450,
friction: 0.92,
starCount: 40,
transparentBackground: true,
});import { FireworksEngine } from '@zakkster/lite-fireworks';
const sky = new FireworksEngine(5000, {
colors: [
{ l: 0.6, c: 0.25, h: 280 }, // brand purple
{ l: 0.8, c: 0.15, h: 60 }, // warm gold
{ l: 0.9, c: 0.08, h: 0 }, // soft white
],
});// Skip the launch phase — explode directly at a position
canvas.addEventListener('click', (e) => {
const colorId = Math.floor(Math.random() * sky.colors.length);
sky.explode(e.clientX, e.clientY, colorId);
});// All config values are live-mutable
gravitySlider.oninput = () => sky.config.gravity = +gravitySlider.value;
frictionSlider.oninput = () => sky.config.friction = +frictionSlider.value;
starSlider.oninput = () => sky.config.starCount = +starSlider.value;
modeToggle.onchange = () => sky.config.transparentBackground = modeToggle.checked;API
new FireworksEngine(maxParticles?, config?)
Creates a fireworks engine with a pre-allocated SoA particle pool.
| Parameter | Type | Default | Description |
|---|---|---|---|
| maxParticles | number | 5000 | Pool capacity. Shared between shells and stars. |
| config | FireworksConfig | see above | All options. Live-mutable after construction. |
Methods
| Method | Description |
|---|---|
| .launch(startX, startY, targetY, colorId?) | Fire a shell. Velocity from physics. Color random if omitted. |
| .explode(originX, originY, colorId) | Manual radial burst. Called automatically at shell apex. |
| .updateAndDraw(ctx, dt, w, h) | Physics + render. Call once per frame. dt in seconds. |
| .clear() | Kill all particles immediately. |
| .destroy() | Null all typed arrays. Idempotent. |
License
MIT
Part of the @zakkster ecosystem
Zero-GC, deterministic, tree-shakeable micro-libraries for high-performance web presentation.
