Fireworks
Sparkle exports the fireworks internals so you can build fully custom firework simulations without the Fireworks effect class.
createExplosion
Creates an array of Explosion particles for any of the 16 firework variants. This is the main entry point for using fireworks in a custom simulator — no Fireworks instance needed.
Demo
Select a variant and click anywhere to fire.
<template>
<div
class="effect-demo effect-demo--clickable"
ref="containerRef"
@click="onClick">
<canvas ref="canvasRef"></canvas>
<div class="effect-demo__controls">
<button
v-for="variant in FIREWORK_VARIANTS"
:key="variant"
:style="selectedVariant === variant ? 'background: rgba(255,255,255,.25); color: white;' : ''"
@click.stop="selectedVariant = variant">
{{ variant }}
</button>
</div>
</div>
</template>
<script
setup
lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import type { FireworkVariant } from '@basmilius/sparkle';
import { createExplosion, Explosion, FIREWORK_VARIANTS, Spark } from '@basmilius/sparkle';
const canvasRef = ref<HTMLCanvasElement>();
const containerRef = ref<HTMLDivElement>();
const selectedVariant = ref<FireworkVariant>('peony');
let ctx: CanvasRenderingContext2D | null = null;
let width = 0;
let height = 0;
let running = false;
let animFrame = 0;
let then = 0;
let explosions: Explosion[] = [];
let sparks: Spark[] = [];
function onClick(evt: MouseEvent): void {
if (!canvasRef.value) {
return;
}
const rect = canvasRef.value.getBoundingClientRect();
const position = {x: evt.clientX - rect.left, y: evt.clientY - rect.top};
const hue = Math.random() * 360;
explosions.push(...createExplosion(selectedVariant.value, position, hue));
}
function loop(now: number): void {
if (!running || !canvasRef.value || !ctx) {
return;
}
animFrame = requestAnimationFrame(loop);
const dt = then > 0 ? (now - then) / (1000 / 60) : 1;
then = now;
canvasRef.value.width = width;
canvasRef.value.height = height;
ctx.globalCompositeOperation = 'lighter';
for (let i = sparks.length - 1; i >= 0; i--) {
sparks[i].tick(dt);
if (sparks[i].isDead) {
sparks.splice(i, 1);
} else {
sparks[i].draw(ctx);
}
}
const newExplosions: Explosion[] = [];
const newSparks: Spark[] = [];
for (let i = explosions.length - 1; i >= 0; i--) {
const explosion = explosions[i];
explosion.tick(dt);
if (explosion.checkSplit()) {
for (let j = 0; j < 4; j++) {
const angle = explosion.angle + (Math.PI / 2) * j + Math.PI / 4;
newExplosions.push(new Explosion(explosion.position, explosion.hue, 3, 'peony', 1, angle, 3 + Math.random() * 3));
}
}
if (explosion.checkCrackle()) {
for (let j = 0; j < 8; j++) {
const angle = Math.random() * Math.PI * 2;
const spd = 3 + Math.random() * 5;
newSparks.push(new Spark(explosion.position, explosion.hue, Math.cos(angle) * spd, Math.sin(angle) * spd));
}
}
if (explosion.isDead) {
explosions.splice(i, 1);
} else {
explosion.draw(ctx);
}
}
explosions.push(...newExplosions);
sparks.push(...newSparks);
ctx.globalCompositeOperation = 'source-over';
}
onMounted(() => {
if (!canvasRef.value) {
return;
}
width = canvasRef.value.offsetWidth;
height = canvasRef.value.offsetHeight;
ctx = canvasRef.value.getContext('2d', {colorSpace: 'display-p3'});
running = true;
animFrame = requestAnimationFrame(loop);
});
onUnmounted(() => {
running = false;
cancelAnimationFrame(animFrame);
then = 0;
explosions = [];
sparks = [];
ctx = null;
});
</script>import { createExplosion, FIREWORK_VARIANTS } from '@basmilius/sparkle';
// Fire any variant at a position with a given hue
const hue = Math.random() * 360;
explosions.push(...createExplosion('heart', {x: 400, y: 300}, hue));
// Optional: custom line width, scale, and seeded RNG
explosions.push(...createExplosion('saturn', position, hue, {lineWidth: 5, scale: 1}, Math.random));All 16 variants work: peony, chrysanthemum, willow, ring, palm, crackle, crossette, saturn, dahlia, brocade, horsetail, strobe, heart, spiral, flower, concentric.
Split and crackle
crossette and crackle particles have secondary effects that trigger mid-flight. Handle them in your loop with checkSplit() and checkCrackle() — each fires only once per particle.
import { createExplosion, Explosion, Spark } from '@basmilius/sparkle';
// In your tick loop:
for (let i = explosions.length - 1; i >= 0; i--) {
const explosion = explosions[i];
explosion.tick(dt);
if (explosion.checkSplit()) {
for (let j = 0; j < 4; j++) {
const angle = explosion.angle + (Math.PI / 2) * j + Math.PI / 4;
explosions.push(new Explosion(explosion.position, explosion.hue, 3, 'peony', 1, angle, 3 + Math.random() * 3));
}
}
if (explosion.checkCrackle()) {
for (let j = 0; j < 8; j++) {
const angle = Math.random() * Math.PI * 2;
const speed = 3 + Math.random() * 5;
sparks.push(new Spark(explosion.position, explosion.hue, Math.cos(angle) * speed, Math.sin(angle) * speed));
}
}
if (explosion.isDead) {
explosions.splice(i, 1);
} else {
explosion.draw(ctx);
}
}Firework projectile
Trail moves from A to B with no event system. Use Firework when you want the projectile to emit sparks along the way and fire a 'remove' event on arrival.
import { Firework, createExplosion } from '@basmilius/sparkle';
const firework = new Firework(
{x: canvas.width / 2, y: canvas.height},
{x: canvas.width / 2, y: canvas.height * 0.2},
Math.random() * 360, 2, 5
);
firework.addEventListener('remove', () => {
const hue = Math.random() * 360;
explosions.push(...createExplosion('peony', firework.position, hue));
}, {once: true});