Skip to content

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>
typescript
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.

typescript
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.

typescript
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});