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