DOM ENGINEV0.10.0FIZLET

FX V0.10.0

fizlet/fx

Explosions that land. One fixed pool of particles (struct-of-arrays, no allocation), drawn on a canvas: square debris, flashes, shock rings, spinning square rings, smoke, scorch marks and pop words, plus screen shake. blast() is the DOM VIRUS / Battlefield hero recipe. Gravity (gravity, px/s²) pulls each kind by its own share. Particle kinds are data (shape, drag, gravity, how they leave an emitter): emitter(fx, KINDS) registers the built-in bubble, spark, drip and debris (or your own) and sprays them; a page that never calls it never ships it. Visual only: its own random (seeded from Math.random), never the simulation's seeded random.

fizlet/fx · GZIP2,797 B2.7 KB of a 3.0 KB budget

Measured on this build: minified and gzipped with everything it imports, the way npm run size checks the budget in CI.

LIVE · LOADINGClick the board for the hero blast. The particles are pooled; once the last one is gone the engine settles.
MODULES · hosted on this site
import { SHAPES, BUBBLE, SPARK, DRIP, … } from "https://coded-dot-design.github.io/fizlet/dist/fx.js";
NPM · COMING SOON
npm install fizlet   # not published yet
import { SHAPES, BUBBLE, SPARK, DRIP, … } from "fizlet/fx";
IN SHORT
const fx = effects({ cap: 420 });
fx.blast(x, y, 1.2);       // flash, rings, debris, scorch
fx.step(dt); fx.draw(ctx); // pooled: never allocates

API

Every export, with its doc comment, read from the published type declarations of v0.10.0.

SHAPES const

Shapes a kind can draw as (a streak trails its velocity: sparks, drips).

export declare const SHAPES: readonly ["square", "disc", "ring", "frame", "word", "smoke", "streak"];

Kind interface

A particle kind, as data. Ranges are [min, max]; speed and size scale with emit's scale.

export interface Kind {
    shape: (typeof SHAPES)[number];
    /** Velocity lost per second, as a fraction (1.4 is the blast's smoke and flashes). */
    drag: number;
    /** Share of the pool's gravity (0: none; negative floats up, like a bubble). */
    gravity: number;
    /** Emit direction in radians (-π/2 is up) and the spread around it (2π: all round). */
    angle: number;
    spread: number;
    speed: readonly [number, number];
    size: readonly [number, number];
    life: readonly [number, number];
}

BUBBLE const

export declare const BUBBLE: Kind;

SPARK const

export declare const SPARK: Kind;

DRIP const

export declare const DRIP: Kind;

DEBRIS_KIND const

export declare const DEBRIS_KIND: Kind;

KINDS const

All four by name: emitter(fx, KINDS), or pick some (emitter(fx, { bubble: BUBBLE })).

export declare const KINDS: {
    bubble: Kind;
    spark: Kind;
    drip: Kind;
    debris: Kind;
};

FxOptions interface

export interface FxOptions {
    /** Max live particles (default 420). New ones are refused (and counted in `dropped`) when full. */
    cap?: number;
    /** Colour table, at most 256; particles store an index (default: the Battlefield blast palette). */
    colors?: string[];
    /** Word table for pop words (index into it), at most 256. */
    words?: string[];
    /** Max scorch marks kept on the ground (default 36; 0 turns them off). */
    scorch?: number;
    /** Gravity in px/s² down (default 0); each kind takes its own share of it. */
    gravity?: number;
}

effects function

Overload policy: particles are decorative, so a full pool refuses new ones and counts them in dropped; a blast stops its per-particle work as soon as the pool is full. Invalid input (non-finite numbers, a life that would never end) is refused and counted the same way.

export declare function effects(o?: FxOptions): {
    colors: string[];
    words: string[];
    spawn: (k: number, x: number, y: number, vx: number, vy: number, size: number, life: number, c: number, rot?: number, spin?: number, w?: number) => boolean;
    readonly live: number;
    readonly shake: number;
    /** Particles refused so far: pool full, or invalid input. Decorative, so they may drop; counted. */
    readonly dropped: number;
    /** Set gravity in px/s² down (each kind takes its own share; read it back from your own state). */
    gravity: number;
    /** The pool itself, for emitter() and other helpers in this file. Not public API. */
    raw: {
        f: Float32Array<ArrayBuffer>[];
        kind: Uint8Array<ArrayBuffer>;
        col: Uint8Array<ArrayBuffer>;
        word: Uint8Array<ArrayBuffer>;
        st: Float64Array<ArrayBuffer>;
        K: Float32Array<ArrayBuffer>;
        cap: number;
        rnd: (a: number, b: number) => number;
    };
    /** Add screen shake (decays on its own; read `shake` when drawing). */
    kick(amount: number): void;
    /** Pop word (index into `words`), drifting up. */
    word(x: number, y: number, w: number, c: number, size?: number): void;
    /** The hero explosion: flash, fireball, shock ring, spinning square ring, debris, smoke, scorch. */
    blast(x: number, y: number, power?: number, tint?: number): undefined;
    /** Advance every particle (call once per frame or per step). */
    step(dt: number): void;
    /** Scorch marks under everything; call before drawing the actors. */
    drawGround(ctx: CanvasRenderingContext2D, light?: boolean): void;
    /** Every live particle, on top of the actors. */
    draw(ctx: CanvasRenderingContext2D, light?: boolean): void;
    clear(): void;
};

Effects type

export type Effects = ReturnType<typeof effects>;

emitter function

Named particle kinds on a pool: const { ids, emit } = emitter(fx, KINDS), then emit(ids.bubble, x, y, 12). Its own export, so a page that never sprays kinds never ships it. At most 26 kinds per pool (ids 6-31); a second emitter on the same pool adds after the first.

export declare function emitter<N extends string>(fx: Effects, kinds: Record<N, Kind>): {
    ids: Record<N, number>;
    /**
     * `n` particles of kind `k` from (x, y), as that kind's data says: direction, spread, speed, size
     * and life (speed and size times `scale`). Colours are picked from `c`...`c + cn - 1`. Bounded: it
     * stops at capacity and counts the rest in `dropped`. Writes the arrays directly: nothing boxes.
     */
    emit(k: number, x: number, y: number, n: number, c?: number, cn?: number, scale?: number): undefined;
};