/**
 * Hypercube - Fez-style rotating tesseract wireframe.
 * @description A Fez-style rotating tesseract: watch a four-dimensional cube turn on a 256x256 PICO-8 sized canvas.
 *
 * A tesseract is a 4D cube: two 3D cubes linked along a fourth axis (W).
 * We rotate in 4D, then project down to 2D so you can see the links stretch
 * and the "inner" cube pass through the "outer" one - like the Fez logo.
 *
 * Drag (mouse or touch) spins it like a trackball: horizontal = yaw around Y,
 * vertical = pitch around X. On release the spin keeps the finger's 2D velocity
 * (inertia), then that spin vector slowly fades back to the automatic tumble.
 */

import { function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
,
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
, class Color32
Mutable 32-bit RGBA color value with 8-bit channels.
@since0.1.0
Color32
, class Palette
Mutable palette of indexed {@link Color32 } entries. The palette is the central color authority for all rendering: - **Index 0 is always transparent.** It is initialized with `Color32.transparent` and cannot be set to an opaque color. The primitive and sprite shaders discard any fragment whose palette index resolves to alpha 0. - **Variable sizes:** valid sizes are `2, 4, 16, 32, 64, 128, 256`. The active size determines the range for `set()` / `get()` and named-color lookups. - **Fixed GPU layout:** `toFloat32Array()` always outputs `256 * 4` floats so the renderer can upload a stable 4 KB uniform block regardless of palette size. Slots beyond the active size are padded with transparent black. - **Named aliases:** optional string tags map human-readable names to indices, e.g. `setNamed('player', 3)`. They carry no runtime cost when unused. - **Mutable by design:** palette-effect features modify entries in place. Use `clone()` when a snapshot is needed before modification.
@since1.0.3@changed1.7.0 Added the `fillBlock` method, writing a transformed block of colors into contiguous slots via `set()` and returning the next free slot, so consecutive blocks chain.
Palette
, class Vector2i
Integer 2D vector for pixel-perfect positioning. Used for points, sizes, directions, and camera offsets throughout the engine. The API includes both allocation-free `*To()` / `*InPlace()` variants and convenience methods that return new vectors.
@since0.1.0
Vector2i
} from 'blit386';
/** @typedef {import('blit386').IBTDemo} IBTDemo */ /** @typedef {import('blit386').HardwareSettings} HardwareSettings */ /** @typedef {import('blit386').Palette} PaletteType */ /** @typedef {import('blit386').Vector2i} Vector2iType */ /** * One wire of the tesseract. `color` is a fixed palette slot so painter's-order * sorting never swaps which cube an edge belongs to. * * @typedef {object} Edge * @property {number} i * @property {number} j * @property {number} depth * @property {number} color */ /** * Angular velocity in each rotation plane (radians per second). * * @typedef {object} Spin * @property {number} xw * @property {number} yz * @property {number} xy * @property {number} zw * @property {number} xz */ const const SIZE: 320SIZE = 320; const const SCALE: 36SCALE = 36; /** Perspective distance for the 4D → 3D step (larger = flatter). */ const const DIST_4: 2.6
Perspective distance for the 4D → 3D step (larger = flatter).
DIST_4
= 2.6;
/** Perspective distance for the 3D → 2D step. */ const const DIST_3: 3.2
Perspective distance for the 3D → 2D step.
DIST_3
= 3.2;
const const C_BG: 1C_BG = 1; const const C_NEAR: 7C_NEAR = 7; /** Fixed slots - stable colors that do not swap when edges are depth-sorted. */ const const C_CUBE_A: 8
Fixed slots - stable colors that do not swap when edges are depth-sorted.
C_CUBE_A
= 8;
const const C_CUBE_B: 9C_CUBE_B = 9; const const C_LINK: 10C_LINK = 10; const const C_DOT: 11C_DOT = 11; const const LINE_SLOTS: {}LINE_SLOTS = [const C_CUBE_A: 8
Fixed slots - stable colors that do not swap when edges are depth-sorted.
C_CUBE_A
, const C_CUBE_B: 9C_CUBE_B, const C_LINK: 10C_LINK, const C_DOT: 11C_DOT];
/** Hue drift in degrees per second. */ const const HUE_SPEED: 28
Hue drift in degrees per second.
HUE_SPEED
= 28;
const const LINE_SAT: 88LINE_SAT = 88; const const LINE_LIGHT: 58LINE_LIGHT = 58; const const LIGHT_PULSE: 10LIGHT_PULSE = 10; const const LIGHT_PULSE_RATE: 1.1LIGHT_PULSE_RATE = 1.1; /** * The automatic Fez tumble - the "home" spin vector we always fade back to. * `xz` stays 0 at rest; drag/flick uses it for screen-space yaw. * * @type {Readonly<Spin>} */ const const HOME_SPIN: Readonly<Spin>
The automatic Fez tumble - the "home" spin vector we always fade back to. `xz` stays 0 at rest; drag/flick uses it for screen-space yaw.
@type{Readonly<Spin>}
HOME_SPIN
= Object.freeze({
xw: numberxw: 0.55, yz: numberyz: 0.38, xy: numberxy: 0.12, zw: numberzw: 0.22, xz: numberxz: 0, }); /** * Radians of trackball turn per pixel of drag. * Horizontal pixels yaw (XZ); vertical pixels pitch (YZ). */ const const DRAG_SENSITIVITY: 0.01
Radians of trackball turn per pixel of drag. Horizontal pixels yaw (XZ); vertical pixels pitch (YZ).
DRAG_SENSITIVITY
= 0.01;
/** * How quickly we smooth the finger's instantaneous velocity while dragging * (higher = snappier, lower = softer). Used so a noisy last frame does not * become a wild flick. */ const const VELOCITY_SMOOTH: 14
How quickly we smooth the finger's instantaneous velocity while dragging (higher = snappier, lower = softer). Used so a noisy last frame does not become a wild flick.
VELOCITY_SMOOTH
= 14;
/** * How quickly free spin eases back toward HOME_SPIN after release (per second). * Lower = longer coast on the flick before the Fez tumble returns. */ const const SPIN_FADE: 1.15
How quickly free spin eases back toward HOME_SPIN after release (per second). Lower = longer coast on the flick before the Fez tumble returns.
SPIN_FADE
= 1.15;
/** Cap on flick spin so a frantic swipe cannot spin forever. */ const const MAX_FLICK_SPIN: 8
Cap on flick spin so a frantic swipe cannot spin forever.
MAX_FLICK_SPIN
= 8;
/** * The 16 corners of a unit tesseract (±1 on x, y, z, w). * Built once at load; never mutated - each frame copies into a scratch vector. * * @type {number[][]} */ const const VERTICES: {}
The 16 corners of a unit tesseract (±1 on x, y, z, w). Built once at load; never mutated - each frame copies into a scratch vector.
@type{number[][]}
VERTICES
= [];
for (let let i: numberi = 0; let i: numberi < 16; let i: numberi++) { // Each bit of i picks +1 or -1 on one axis (bit 0 = X, 1 = Y, 2 = Z, 3 = W). const VERTICES: {}
The 16 corners of a unit tesseract (±1 on x, y, z, w). Built once at load; never mutated - each frame copies into a scratch vector.
@type{number[][]}
VERTICES
.push([(let i: numberi & 1) !== 0 ? 1 : -1, (let i: numberi & 2) !== 0 ? 1 : -1, (let i: numberi & 4) !== 0 ? 1 : -1, (let i: numberi & 8) !== 0 ? 1 : -1]);
} /** * Farther edges first. Tie-break on endpoints so equal depths stay stable * (avoids color flicker when two edges share a depth). * * @param {Edge} a * @param {Edge} b * @returns {number} */ function function compareEdgeDepth(a: Edge, b: Edge): number
Farther edges first. Tie-break on endpoints so equal depths stay stable (avoids color flicker when two edges share a depth).
@parama@paramb@returns
compareEdgeDepth
(a: Edge
@parama
a
, b: Edge
@paramb
b
) {
return a: Edge
@parama
a
.depth: numberdepth - b: Edge
@paramb
b
.depth: numberdepth || a: Edge
@parama
a
.i: numberi - b: Edge
@paramb
b
.i: numberi || a: Edge
@parama
a
.j: numberj - b: Edge
@paramb
b
.j: numberj;
} /** * Clamp one spin component into ±MAX_FLICK_SPIN. * * @param {number} value * @returns {number} */ function function clampFlick(value: number): number
Clamp one spin component into ±MAX_FLICK_SPIN.
@paramvalue@returns
clampFlick
(value: number
@paramvalue
value
) {
return Math.max(-const MAX_FLICK_SPIN: 8
Cap on flick spin so a frantic swipe cannot spin forever.
MAX_FLICK_SPIN
, Math.min(const MAX_FLICK_SPIN: 8
Cap on flick spin so a frantic swipe cannot spin forever.
MAX_FLICK_SPIN
, value: number
@paramvalue
value
));
} /** @implements {IBTDemo} */ class class Demo
@implementsIBTDemo
Demo
{
/** @type {PaletteType | null} */ Demo.palette: Palette | null
@type{PaletteType | null}
palette
= null;
/** Screen positions after 4D → 2D projection (reused every frame). */ /** @type {Vector2iType[]} */ Demo.projected: {}
@type{Vector2iType[]}
projected
= [];
/** Per-vertex depth for painter's algorithm. */ /** @type {number[]} */ Demo.depths: {}
@type{number[]}
depths
= [];
/** @type {Edge[]} */ Demo.edgeOrder: {}
@type{Edge[]}
edgeOrder
= [];
/** Starting hues for cube A / cube B / link / dot (degrees). */ /** @type {number[]} */ Demo.baseHues: {}
@type{number[]}
baseHues
= [];
/** Scratch 4D point reused while rotating each corner (avoids per-frame arrays). */ /** @type {number[]} */ Demo.scratch: {}
@type{number[]}
scratch
= [0, 0, 0, 0];
/** @type {number} */ Demo.angleXW: number
@type{number}
angleXW
= 0.35;
/** @type {number} */ Demo.angleYZ: number
@type{number}
angleYZ
= 0.9;
/** @type {number} */ Demo.angleXY: number
@type{number}
angleXY
= 0.1;
/** @type {number} */ Demo.angleZW: number
@type{number}
angleZW
= 0.2;
/** Screen-space yaw (XZ plane - around Y). @type {number} */ Demo.angleXZ: number
Screen-space yaw (XZ plane - around Y).
@type{number}
angleXZ
= 0;
/** Free-motion angular velocity; eases toward HOME_SPIN. @type {Spin} */ Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
= { ...const HOME_SPIN: Readonly<Spin>
The automatic Fez tumble - the "home" spin vector we always fade back to. `xz` stays 0 at rest; drag/flick uses it for screen-space yaw.
@type{Readonly<Spin>}
HOME_SPIN
};
/** * Smoothed finger yaw/pitch (radians / sec) while dragging. * On release these become spin.xz / spin.yz so the model coasts. * * @type {{ xz: number, yz: number }} */
Demo.fingerSpin: {
    xz: number;
    yz: number;
}
Smoothed finger yaw/pitch (radians / sec) while dragging. On release these become spin.xz / spin.yz so the model coasts.
@type{{ xz: number, yz: number }}
fingerSpin
= { xz: numberxz: 0, yz: numberyz: 0 };
/** Pointer slot currently steering (−1 = none). @type {number} */ Demo.dragSlot: number
Pointer slot currently steering (−1 = none).
@type{number}
dragSlot
= -1;
/** * @returns {Partial<HardwareSettings>} */ Demo.configure(): Partial<HardwareSettings>
Optional hook to declare display size, optional output drawing-buffer size, upscale filter, target fixed-update rate, rendering backend, and overlay. When omitted, the engine uses {@link defaultConfig } (`320x240` logical, `640x480` drawing buffer, `60` FPS, overlay enabled). When present, you may return only the fields you want to change; the engine merges them with {@link defaultConfig } via {@link mergeHardwareSettings } . Omit `displaySize` to inherit the full default resolution and output buffer. Include `displaySize` when you want a custom logical size; optional fields you omit then stay unset (for example no `drawingBufferSize` means a 1:1 drawing buffer).
@returns
configure
() {
return { displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const SIZE: 320SIZE, const SIZE: 320SIZE),
maxCanvasSize: Vector2imaxCanvasSize: new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const SIZE: 320SIZE * 2, const SIZE: 320SIZE * 2),
targetFPS: numbertargetFPS: 60, isOverlayEnabled: booleanisOverlayEnabled: true, isOverlayVisibleAtStart: booleanisOverlayVisibleAtStart: true, isOverlayPaletteEnabled: booleanisOverlayPaletteEnabled: true,
overlayStyle: {
    barPaletteIndex: number;
    textPaletteIndex: number;
    gapPaletteIndex: number;
}
overlayStyle
: {
barPaletteIndex: numberbarPaletteIndex: const C_NEAR: 7C_NEAR, textPaletteIndex: numbertextPaletteIndex: const C_BG: 1C_BG, gapPaletteIndex: numbergapPaletteIndex: const C_BG: 1C_BG, }, }; } /** * @returns {Promise<boolean>} */ async Demo.init(): Promise<boolean>
Called once after the selected rendering backend has been initialized. Load assets and prepare a demo state here.
@returns
init
() {
this.Demo.palette: Palette | null
@type{PaletteType | null}
palette
= class Palette
Mutable palette of indexed {@link Color32 } entries. The palette is the central color authority for all rendering: - **Index 0 is always transparent.** It is initialized with `Color32.transparent` and cannot be set to an opaque color. The primitive and sprite shaders discard any fragment whose palette index resolves to alpha 0. - **Variable sizes:** valid sizes are `2, 4, 16, 32, 64, 128, 256`. The active size determines the range for `set()` / `get()` and named-color lookups. - **Fixed GPU layout:** `toFloat32Array()` always outputs `256 * 4` floats so the renderer can upload a stable 4 KB uniform block regardless of palette size. Slots beyond the active size are padded with transparent black. - **Named aliases:** optional string tags map human-readable names to indices, e.g. `setNamed('player', 3)`. They carry no runtime cost when unused. - **Mutable by design:** palette-effect features modify entries in place. Use `clone()` when a snapshot is needed before modification.
@since1.0.3@changed1.7.0 Added the `fillBlock` method, writing a transformed block of colors into contiguous slots via `set()` and returning the next free slot, so consecutive blocks chain.
Palette
.Palette.pico8(): Palette
Creates the PICO-8 16-color palette.
@returnsNew PICO-8 preset palette.
pico8
();
// Spread the four line colors around the wheel; randomize the starting angle. // BT.random is the engine's shared random number generator. // Its float() method returns a decimal from the first value up to (but not including) the second, // so this lands anywhere on the 360-degree color wheel. const const start: numberstart =
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.random: Random
Default engine PRNG (live reference - not a copy). Time-seeded when the engine singleton is created. Call {@link BT.randomSeed } for a reproducible run. Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.
@since1.5.0@returnsThe shared {@link Random} instance.@exampleBT.randomSeed(42); BT.random.int(150, 420); BT.random.pick(['a', 'b', 'c']);
random
.Random.float(min: number, max: number): number
Returns the next pseudo-random float in [min, max).
@parammin - Inclusive lower bound.@parammax - Exclusive upper bound.@returnsFloat in [min, max).@since1.5.0
float
(0, 360);
this.Demo.baseHues: {}
@type{number[]}
baseHues
= [const start: numberstart, const start: numberstart + 90, const start: numberstart + 180, const start: numberstart + 270];
this.Demo.applyLineColors(timeSeconds: number): void
@paramtimeSeconds@returns
applyLineColors
(0);
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.paletteSet: (palette: Palette) => void
Stores the active engine palette. Use this to swap the **entire palette** (e.g. switch between a day and night theme). After this call the renderer uploads the new palette uniform on the next frame. **Palette-value swap (change what a slot looks like):** mutate the live {@link BT.palette } in place with `palette.set(slot, newColor)`. The renderer uploads dirty slots on the next frame; no `paletteSet()` or {@link BT.spritesRefresh } needed. **Palette-layout swap (same colors, different slot positions):** build a new palette with the same colors at new indices, call `paletteSet()`, then call {@link BT.spritesRefresh } so every sprite sheet re-maps its original RGBA pixels against the new slot layout.
@since1.0.3@parampalette - Palette to make active.
paletteSet
(this.Demo.palette: Palette
@type{PaletteType | null}
palette
);
for (let let i: numberi = 0; let i: numberi < 16; let i: numberi++) { this.Demo.projected: {}
@type{Vector2iType[]}
projected
.push(new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(0, 0));
this.Demo.depths: {}
@type{number[]}
depths
.push(0);
} // Two corners form an edge when their indices differ in exactly one bit // (they are neighbors on the 4D hypercube). for (let let i: numberi = 0; let i: numberi < 16; let i: numberi++) { for (let let j: numberj = let i: numberi + 1; let j: numberj < 16; let j: numberj++) { const const axis: numberaxis = let i: numberi ^ let j: numberj; // Exactly one bit set: axis is a power of two. if (const axis: numberaxis !== 0 && (const axis: numberaxis & (const axis: numberaxis - 1)) === 0) { // Bit 3 (value 8) means the edge spans W - a strut between the two cubes. // Otherwise the edge lives on the cube whose W sign matches endpoint i. const const color: 8 | 9 | 10color = const axis: numberaxis === 8 ? const C_LINK: 10C_LINK : (let i: numberi & 8) !== 0 ? const C_CUBE_B: 9C_CUBE_B : const C_CUBE_A: 8
Fixed slots - stable colors that do not swap when edges are depth-sorted.
C_CUBE_A
;
this.Demo.edgeOrder: {}
@type{Edge[]}
edgeOrder
.push({ i: numberi, j: numberj, depth: numberdepth: 0, color: numbercolor });
} } } return true; } /** * @param {number} timeSeconds * @returns {void} */ Demo.applyLineColors(timeSeconds: number): void
@paramtimeSeconds@returns
applyLineColors
(timeSeconds: number
@paramtimeSeconds
timeSeconds
) {
for (let let i: numberi = 0; let i: numberi < const LINE_SLOTS: {}LINE_SLOTS.length; let i: numberi++) { const const hue: numberhue = (this.Demo.baseHues: {}
@type{number[]}
baseHues
[let i: numberi] + timeSeconds: number
@paramtimeSeconds
timeSeconds
* const HUE_SPEED: 28
Hue drift in degrees per second.
HUE_SPEED
) % 360;
const const light: numberlight = const LINE_LIGHT: 58LINE_LIGHT + Math.sin(timeSeconds: number
@paramtimeSeconds
timeSeconds
* const LIGHT_PULSE_RATE: 1.1LIGHT_PULSE_RATE + let i: numberi) * const LIGHT_PULSE: 10LIGHT_PULSE;
// init() always sets this.palette before update() runs. this.Demo.palette: Palette | null
@type{PaletteType | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const LINE_SLOTS: {}LINE_SLOTS[let i: numberi], class Color32
Mutable 32-bit RGBA color value with 8-bit channels.
@since0.1.0
Color32
.Color32.fromHSL(h: number, s: number, l: number, a?: number): Color32
Creates a color from HSL values.
@paramh - Hue in degrees (0-360).@params - Saturation as percentage (0-100).@paraml - Lightness as percentage (0-100).@parama - Alpha channel (0-255, defaults to 255).@returnsNew color converted from HSL values.
fromHSL
(const hue: numberhue, const LINE_SAT: 88LINE_SAT, const light: numberlight));
} } /** * Rotate a 4D point in the plane of axes `a` and `b` (0=X, 1=Y, 2=Z, 3=W). * `c` / `s` are cos/sin of the angle - precomputed once per frame. * * @param {number[]} v * @param {number} c * @param {number} s * @param {number} a * @param {number} b * @returns {void} */ Demo.rotatePlane(v: number[], c: number, s: number, a: number, b: number): void
Rotate a 4D point in the plane of axes `a` and `b` (0=X, 1=Y, 2=Z, 3=W). `c` / `s` are cos/sin of the angle - precomputed once per frame.
@paramv@paramc@params@parama@paramb@returns
rotatePlane
(v: {}
@paramv
v
, c: number
@paramc
c
, s: number
@params
s
, a: number
@parama
a
, b: number
@paramb
b
) {
const const x: anyx = v: {}
@paramv
v
[a: number
@parama
a
];
const const y: anyy = v: {}
@paramv
v
[b: number
@paramb
b
];
v: {}
@paramv
v
[a: number
@parama
a
] = const x: anyx * c: number
@paramc
c
- const y: anyy * s: number
@params
s
;
v: {}
@paramv
v
[b: number
@paramb
b
] = const x: anyx * s: number
@params
s
+ const y: anyy * c: number
@paramc
c
;
} /** * Mouse (slot 0): primary button held. Touch slots: contact active. * Same rule as pointer-paint. * * @param {number} slot * @returns {boolean} */ Demo.isDragHeld(slot: number): boolean
Mouse (slot 0): primary button held. Touch slots: contact active. Same rule as pointer-paint.
@paramslot@returns
isDragHeld
(slot: number
@paramslot
slot
) {
if (!
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.isPointerActive: (pointerIndex?: number) => boolean
Reports whether the given pointer slot has a live pointer. For slot 0 (mouse) this is true while the mouse is hovering inside the canvas; cleared on `pointerleave`. For slots 1-3 (touch / pen) this is true while the contact is down.
@since1.1.1@parampointerIndex - Pointer slot (defaults to 0 = mouse).@returns`true` while the slot has live position data.
isPointerActive
(slot: number
@paramslot
slot
)) {
return false; } return slot: number
@paramslot
slot
=== 0 ?
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.isDown: (button: number, player?: number) => boolean
Checks whether a button is currently held. For pointer buttons (`BTN_POINTER_A..D`), the second parameter is the pointer slot index (0 = mouse, 1-3 = touch / pen). For mouse slot 0: `A` is left, `B` is right, `C` is middle, `D` is back / forward (matches RetroBlit canonical, not DOM `PointerEvent.button` index). Touch / pen slots only support `A`; B/C/D return `false`. `button` accepts one or more bit flags from the `BTN_*` set (for example `BT.BTN_A | BT.BTN_B`). Matching uses ANY semantics: returns `true` when any selected button is held. For face buttons (`BTN_UP`…`BTN_SELECT`), players `0` and `1` merge keyboard and gamepad input (logical OR). Players `2` and `3` use gamepad only. Pointer flags (`BTN_POINTER_*`) use the `player` argument as pointer slot.
@since1.1.1@parambutton - Button constant from the `BTN_*` set.@paramplayer - Zero-based player index for gamepads / keyboard, or pointer slot (0-3) for `BTN_POINTER_*`.@returns`true` while the button remains pressed.
isDown
(
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.type BTN_POINTER_A: number
Primary pointer button code. Maps to mouse left for slot 0; touch contact for slots 1-3.
@since0.1.0
BTN_POINTER_A
, 0) : true;
} /** * @param {number} dt * @returns {void} */ Demo.integrateSpin(dt: number): void
@paramdt@returns
integrateSpin
(dt: number
@paramdt
dt
) {
this.Demo.angleXW: number
@type{number}
angleXW
+= this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.xw: numberxw * dt: number
@paramdt
dt
;
this.Demo.angleYZ: number
@type{number}
angleYZ
+= this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.yz: numberyz * dt: number
@paramdt
dt
;
this.Demo.angleXY: number
@type{number}
angleXY
+= this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.xy: numberxy * dt: number
@paramdt
dt
;
this.Demo.angleZW: number
@type{number}
angleZW
+= this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.zw: numberzw * dt: number
@paramdt
dt
;
this.Demo.angleXZ: number
Screen-space yaw (XZ plane - around Y).
@type{number}
angleXZ
+= this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.xz: numberxz * dt: number
@paramdt
dt
;
} /** * Exponential ease of `spin` toward HOME_SPIN (soft settle, no hard stop). * * @param {number} dt * @returns {void} */ Demo.fadeSpinToHome(dt: number): void
Exponential ease of `spin` toward HOME_SPIN (soft settle, no hard stop).
@paramdt@returns
fadeSpinToHome
(dt: number
@paramdt
dt
) {
const const t: numbert = 1 - Math.exp(-const SPIN_FADE: 1.15
How quickly free spin eases back toward HOME_SPIN after release (per second). Lower = longer coast on the flick before the Fez tumble returns.
SPIN_FADE
* dt: number
@paramdt
dt
);
this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.xw: numberxw += (const HOME_SPIN: Readonly<Spin>
The automatic Fez tumble - the "home" spin vector we always fade back to. `xz` stays 0 at rest; drag/flick uses it for screen-space yaw.
@type{Readonly<Spin>}
HOME_SPIN
.xw - this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.xw: numberxw) * const t: numbert;
this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.yz: numberyz += (const HOME_SPIN: Readonly<Spin>
The automatic Fez tumble - the "home" spin vector we always fade back to. `xz` stays 0 at rest; drag/flick uses it for screen-space yaw.
@type{Readonly<Spin>}
HOME_SPIN
.yz - this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.yz: numberyz) * const t: numbert;
this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.xy: numberxy += (const HOME_SPIN: Readonly<Spin>
The automatic Fez tumble - the "home" spin vector we always fade back to. `xz` stays 0 at rest; drag/flick uses it for screen-space yaw.
@type{Readonly<Spin>}
HOME_SPIN
.xy - this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.xy: numberxy) * const t: numbert;
this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.zw: numberzw += (const HOME_SPIN: Readonly<Spin>
The automatic Fez tumble - the "home" spin vector we always fade back to. `xz` stays 0 at rest; drag/flick uses it for screen-space yaw.
@type{Readonly<Spin>}
HOME_SPIN
.zw - this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.zw: numberzw) * const t: numbert;
this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.xz: numberxz += (const HOME_SPIN: Readonly<Spin>
The automatic Fez tumble - the "home" spin vector we always fade back to. `xz` stays 0 at rest; drag/flick uses it for screen-space yaw.
@type{Readonly<Spin>}
HOME_SPIN
.xz - this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.xz: numberxz) * const t: numbert;
} /** * Trackball drag + flick inertia. While held: yaw/pitch from pointer delta * and EMA the finger velocity. On release: that velocity becomes `spin`, * then fades back to HOME_SPIN. * * @param {number} dt * @returns {void} */ Demo.updateDrag(dt: number): void
Trackball drag + flick inertia. While held: yaw/pitch from pointer delta and EMA the finger velocity. On release: that velocity becomes `spin`, then fades back to HOME_SPIN.
@paramdt@returns
updateDrag
(dt: number
@paramdt
dt
) {
const const wasDragging: booleanwasDragging = this.Demo.dragSlot: number
Pointer slot currently steering (−1 = none).
@type{number}
dragSlot
>= 0;
// Stick with the current finger; otherwise claim the first held slot. if (this.Demo.dragSlot: number
Pointer slot currently steering (−1 = none).
@type{number}
dragSlot
>= 0 && !this.Demo.isDragHeld(slot: number): boolean
Mouse (slot 0): primary button held. Touch slots: contact active. Same rule as pointer-paint.
@paramslot@returns
isDragHeld
(this.Demo.dragSlot: number
Pointer slot currently steering (−1 = none).
@type{number}
dragSlot
)) {
this.Demo.dragSlot: number
Pointer slot currently steering (−1 = none).
@type{number}
dragSlot
= -1;
} if (this.Demo.dragSlot: number
Pointer slot currently steering (−1 = none).
@type{number}
dragSlot
< 0) {
for (let let slot: numberslot = 0; let slot: numberslot < 4; let slot: numberslot++) { if (this.Demo.isDragHeld(slot: number): boolean
Mouse (slot 0): primary button held. Touch slots: contact active. Same rule as pointer-paint.
@paramslot@returns
isDragHeld
(let slot: numberslot)) {
this.Demo.dragSlot: number
Pointer slot currently steering (−1 = none).
@type{number}
dragSlot
= let slot: numberslot;
// Drop press-frame jitter so it cannot become a throw. this.
Demo.fingerSpin: {
    xz: number;
    yz: number;
}
Smoothed finger yaw/pitch (radians / sec) while dragging. On release these become spin.xz / spin.yz so the model coasts.
@type{{ xz: number, yz: number }}
fingerSpin
.xz: numberxz = 0;
this.
Demo.fingerSpin: {
    xz: number;
    yz: number;
}
Smoothed finger yaw/pitch (radians / sec) while dragging. On release these become spin.xz / spin.yz so the model coasts.
@type{{ xz: number, yz: number }}
fingerSpin
.yz: numberyz = 0;
break; } } } if (this.Demo.dragSlot: number
Pointer slot currently steering (−1 = none).
@type{number}
dragSlot
>= 0) {
const const delta: Vector2idelta =
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.pointerDelta: (pointerIndex?: number) => Vector2i
Returns the position delta `(pos - prevPos)` for a pointer slot since the previous frame. Reflects movement accumulated between the previous and current frame. Snapshotted and reset by the engine at `endFrame()`, which runs after `update()` and `render()`. Returns `Vector2i.zero()` when the engine is not initialized or `pointerIndex` is out of range.
@since1.0.3@parampointerIndex - Pointer slot (defaults to 0 = mouse).@returnsPer-frame movement in display coordinates.
pointerDelta
(this.Demo.dragSlot: number
Pointer slot currently steering (−1 = none).
@type{number}
dragSlot
);
// Trackball: horizontal → yaw (XZ / around Y); vertical → pitch (YZ / around X). const const dYaw: numberdYaw = const delta: Vector2idelta.Vector2i.x: number
Horizontal component (defaults to 0).
x
* const DRAG_SENSITIVITY: 0.01
Radians of trackball turn per pixel of drag. Horizontal pixels yaw (XZ); vertical pixels pitch (YZ).
DRAG_SENSITIVITY
;
const const dPitch: numberdPitch = const delta: Vector2idelta.Vector2i.y: number
Vertical component (defaults to 0).
y
* const DRAG_SENSITIVITY: 0.01
Radians of trackball turn per pixel of drag. Horizontal pixels yaw (XZ); vertical pixels pitch (YZ).
DRAG_SENSITIVITY
;
this.Demo.angleXZ: number
Screen-space yaw (XZ plane - around Y).
@type{number}
angleXZ
+= const dYaw: numberdYaw;
this.Demo.angleYZ: number
@type{number}
angleYZ
+= const dPitch: numberdPitch;
// Guard dt so a hitch cannot explode the velocity sample. if (dt: number
@paramdt
dt
> 0.0001) {
const const alpha: numberalpha = 1 - Math.exp(-const VELOCITY_SMOOTH: 14
How quickly we smooth the finger's instantaneous velocity while dragging (higher = snappier, lower = softer). Used so a noisy last frame does not become a wild flick.
VELOCITY_SMOOTH
* dt: number
@paramdt
dt
);
this.
Demo.fingerSpin: {
    xz: number;
    yz: number;
}
Smoothed finger yaw/pitch (radians / sec) while dragging. On release these become spin.xz / spin.yz so the model coasts.
@type{{ xz: number, yz: number }}
fingerSpin
.xz: numberxz += (const dYaw: numberdYaw / dt: number
@paramdt
dt
- this.
Demo.fingerSpin: {
    xz: number;
    yz: number;
}
Smoothed finger yaw/pitch (radians / sec) while dragging. On release these become spin.xz / spin.yz so the model coasts.
@type{{ xz: number, yz: number }}
fingerSpin
.xz: numberxz) * const alpha: numberalpha;
this.
Demo.fingerSpin: {
    xz: number;
    yz: number;
}
Smoothed finger yaw/pitch (radians / sec) while dragging. On release these become spin.xz / spin.yz so the model coasts.
@type{{ xz: number, yz: number }}
fingerSpin
.yz: numberyz += (const dPitch: numberdPitch / dt: number
@paramdt
dt
- this.
Demo.fingerSpin: {
    xz: number;
    yz: number;
}
Smoothed finger yaw/pitch (radians / sec) while dragging. On release these become spin.xz / spin.yz so the model coasts.
@type{{ xz: number, yz: number }}
fingerSpin
.yz: numberyz) * const alpha: numberalpha;
} return; } // Coast on the flick (trackball axes only); fade restores the Fez planes. if (const wasDragging: booleanwasDragging) { this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.xz: numberxz = function clampFlick(value: number): number
Clamp one spin component into ±MAX_FLICK_SPIN.
@paramvalue@returns
clampFlick
(this.
Demo.fingerSpin: {
    xz: number;
    yz: number;
}
Smoothed finger yaw/pitch (radians / sec) while dragging. On release these become spin.xz / spin.yz so the model coasts.
@type{{ xz: number, yz: number }}
fingerSpin
.xz: numberxz);
this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.yz: numberyz = function clampFlick(value: number): number
Clamp one spin component into ±MAX_FLICK_SPIN.
@paramvalue@returns
clampFlick
(this.
Demo.fingerSpin: {
    xz: number;
    yz: number;
}
Smoothed finger yaw/pitch (radians / sec) while dragging. On release these become spin.xz / spin.yz so the model coasts.
@type{{ xz: number, yz: number }}
fingerSpin
.yz: numberyz);
this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.xw: numberxw = 0;
this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.xy: numberxy = 0;
this.Demo.spin: Spin
Free-motion angular velocity; eases toward HOME_SPIN.
@type{Spin}
spin
.zw: numberzw = 0;
} this.Demo.integrateSpin(dt: number): void
@paramdt@returns
integrateSpin
(dt: number
@paramdt
dt
);
this.Demo.fadeSpinToHome(dt: number): void
Exponential ease of `spin` toward HOME_SPIN (soft settle, no hard stop).
@paramdt@returns
fadeSpinToHome
(dt: number
@paramdt
dt
);
} /** * @returns {void} */ Demo.update(): void
Called zero or more times per frame at the fixed timestep declared by `targetFPS`. The accumulator pattern ensures the target rate is met on average, but a single frame may invoke this multiple times (catch-up) or not at all. Update simulation, timers, and input-driven state here. This is a hot path. Minimize allocations, reuse objects, and prefer in-place vector operations where possible. Avoid rendering work here; draw in `render()` instead.
@returns
update
() {
const const dt: numberdt =
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.deltaSeconds: number
Fixed-step seconds per update tick. Equivalent to `1 / BT.targetFPS` when `BT.targetFPS` is finite and positive. Falls back to `1 / 60` when target FPS is non-finite or non-positive.
@since1.0.4@returnsSeconds advanced by one fixed update tick.
deltaSeconds
;
this.Demo.updateDrag(dt: number): void
Trackball drag + flick inertia. While held: yaw/pitch from pointer delta and EMA the finger velocity. On release: that velocity becomes `spin`, then fades back to HOME_SPIN.
@paramdt@returns
updateDrag
(const dt: numberdt);
this.Demo.applyLineColors(timeSeconds: number): void
@paramtimeSeconds@returns
applyLineColors
(
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.timeSeconds: number
Fixed-step elapsed time in seconds (`BT.ticks * BT.deltaSeconds`).
@since1.0.4@returnsElapsed fixed-step time in seconds since initialization.
timeSeconds
);
const const cx: numbercx = const SIZE: 320SIZE / 2; const const cy: numbercy = const SIZE: 320SIZE / 2; const const cXW: anycXW = Math.cos(this.Demo.angleXW: number
@type{number}
angleXW
);
const const sXW: anysXW = Math.sin(this.Demo.angleXW: number
@type{number}
angleXW
);
const const cYZ: anycYZ = Math.cos(this.Demo.angleYZ: number
@type{number}
angleYZ
);
const const sYZ: anysYZ = Math.sin(this.Demo.angleYZ: number
@type{number}
angleYZ
);
const const cXY: anycXY = Math.cos(this.Demo.angleXY: number
@type{number}
angleXY
);
const const sXY: anysXY = Math.sin(this.Demo.angleXY: number
@type{number}
angleXY
);
const const cZW: anycZW = Math.cos(this.Demo.angleZW: number
@type{number}
angleZW
);
const const sZW: anysZW = Math.sin(this.Demo.angleZW: number
@type{number}
angleZW
);
const const cXZ: anycXZ = Math.cos(this.Demo.angleXZ: number
Screen-space yaw (XZ plane - around Y).
@type{number}
angleXZ
);
const const sXZ: anysXZ = Math.sin(this.Demo.angleXZ: number
Screen-space yaw (XZ plane - around Y).
@type{number}
angleXZ
);
const const v: {}v = this.Demo.scratch: {}
@type{number[]}
scratch
;
for (let let i: numberi = 0; let i: numberi < 16; let i: numberi++) { const const src: anysrc = const VERTICES: {}
The 16 corners of a unit tesseract (±1 on x, y, z, w). Built once at load; never mutated - each frame copies into a scratch vector.
@type{number[][]}
VERTICES
[let i: numberi];
const v: {}v[0] = const src: anysrc[0]; const v: {}v[1] = const src: anysrc[1]; const v: {}v[2] = const src: anysrc[2]; const v: {}v[3] = const src: anysrc[3]; // 4D tumble planes, then screen-space yaw (XZ) so a horizontal drag // turns the projected object like a solid in front of you. this.Demo.rotatePlane(v: number[], c: number, s: number, a: number, b: number): void
Rotate a 4D point in the plane of axes `a` and `b` (0=X, 1=Y, 2=Z, 3=W). `c` / `s` are cos/sin of the angle - precomputed once per frame.
@paramv@paramc@params@parama@paramb@returns
rotatePlane
(const v: {}v, const cXW: anycXW, const sXW: anysXW, 0, 3);
this.Demo.rotatePlane(v: number[], c: number, s: number, a: number, b: number): void
Rotate a 4D point in the plane of axes `a` and `b` (0=X, 1=Y, 2=Z, 3=W). `c` / `s` are cos/sin of the angle - precomputed once per frame.
@paramv@paramc@params@parama@paramb@returns
rotatePlane
(const v: {}v, const cYZ: anycYZ, const sYZ: anysYZ, 1, 2);
this.Demo.rotatePlane(v: number[], c: number, s: number, a: number, b: number): void
Rotate a 4D point in the plane of axes `a` and `b` (0=X, 1=Y, 2=Z, 3=W). `c` / `s` are cos/sin of the angle - precomputed once per frame.
@paramv@paramc@params@parama@paramb@returns
rotatePlane
(const v: {}v, const cXY: anycXY, const sXY: anysXY, 0, 1);
this.Demo.rotatePlane(v: number[], c: number, s: number, a: number, b: number): void
Rotate a 4D point in the plane of axes `a` and `b` (0=X, 1=Y, 2=Z, 3=W). `c` / `s` are cos/sin of the angle - precomputed once per frame.
@paramv@paramc@params@parama@paramb@returns
rotatePlane
(const v: {}v, const cZW: anycZW, const sZW: anysZW, 2, 3);
this.Demo.rotatePlane(v: number[], c: number, s: number, a: number, b: number): void
Rotate a 4D point in the plane of axes `a` and `b` (0=X, 1=Y, 2=Z, 3=W). `c` / `s` are cos/sin of the angle - precomputed once per frame.
@paramv@paramc@params@parama@paramb@returns
rotatePlane
(const v: {}v, const cXZ: anycXZ, const sXZ: anysXZ, 0, 2);
// 4D → 3D perspective: points farther in W shrink toward the origin. const const w: numberw = const DIST_4: 2.6
Perspective distance for the 4D → 3D step (larger = flatter).
DIST_4
/ (const DIST_4: 2.6
Perspective distance for the 4D → 3D step (larger = flatter).
DIST_4
- const v: {}v[3]);
const const x3: numberx3 = const v: {}v[0] * const w: numberw; const const y3: numbery3 = const v: {}v[1] * const w: numberw; const const z3: numberz3 = const v: {}v[2] * const w: numberw; // 3D → 2D perspective, then center on the canvas. const const p: numberp = const DIST_3: 3.2
Perspective distance for the 3D → 2D step.
DIST_3
/ (const DIST_3: 3.2
Perspective distance for the 3D → 2D step.
DIST_3
- const z3: numberz3);
const const x2: numberx2 = const x3: numberx3 * const p: numberp * const SCALE: 36SCALE + const cx: numbercx; const const y2: numbery2 = const y3: numbery3 * const p: numberp * const SCALE: 36SCALE + const cy: numbercy; this.Demo.projected: {}
@type{Vector2iType[]}
projected
[let i: numberi].set(const x2: numberx2, const y2: numbery2);
// Blend Z and W so links that dive "into" W sort behind nearer faces. this.Demo.depths: {}
@type{number[]}
depths
[let i: numberi] = const z3: numberz3 + const v: {}v[3] * 0.35;
} // Update depth on each edge in place. Do NOT rebuild edges from a sorted // index list - that pairs the wrong color with the wrong endpoints and flashes. for (let let e: numbere = 0; let e: numbere < this.Demo.edgeOrder: {}
@type{Edge[]}
edgeOrder
.length; let e: numbere++) {
const const edge: anyedge = this.Demo.edgeOrder: {}
@type{Edge[]}
edgeOrder
[let e: numbere];
const edge: anyedge.depth = (this.Demo.depths: {}
@type{number[]}
depths
[const edge: anyedge.i] + this.Demo.depths: {}
@type{number[]}
depths
[const edge: anyedge.j]) * 0.5;
} this.Demo.edgeOrder: {}
@type{Edge[]}
edgeOrder
.sort(function compareEdgeDepth(a: Edge, b: Edge): number
Farther edges first. Tie-break on endpoints so equal depths stay stable (avoids color flicker when two edges share a depth).
@parama@paramb@returns
compareEdgeDepth
);
} /** * @returns {void} */ Demo.render(): void
Called once per `requestAnimationFrame` tick (browser refresh rate). Issue all draw calls for the current frame here. When {@link HardwareSettings.isOverlayEnabled } is `true` (default), the engine draws a screen-space overlay HUD after this method returns (present FPS, target FPS, draw calls, frame/update()/render() timings, backend, demo title). Optional {@link overlayRows } adds stacked bars above the footer. Demos do not need to duplicate engine overlay text. Reserve about ~42 px at the top and space for the bottom palette grid (or ~13 px when {@link HardwareSettings.isOverlayPaletteEnabled } is `false`) at the bottom (plus ~14 px per custom overlay row) for overlay bars, or disable the overlay in `configure()` when using custom full-screen HUD layouts. This is a hot path. Batch draws by texture to reduce GPU state changes and reuse Color32/Vector2i instances instead of allocating per frame. Avoid mutating the simulation state here unless it is strictly visual.
@returns
render
() {
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.clear: (paletteIndex: number) => void
Sets the frame clear color using a palette index. The renderer uses this color when clearing the full display at the start of the next frame.
@since0.1.0@parampaletteIndex - Palette index for the full-screen clear pass.
clear
(const C_BG: 1C_BG);
for (let let e: numbere = 0; let e: numbere < this.Demo.edgeOrder: {}
@type{Edge[]}
edgeOrder
.length; let e: numbere++) {
const const edge: anyedge = this.Demo.edgeOrder: {}
@type{Edge[]}
edgeOrder
[let e: numbere];
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.drawLine: (p0: Vector2i, p1: Vector2i, paletteIndex: number) => void
Draws a pixel-perfect line between two points. Uses rasterized line drawing without antialiasing.
@since0.1.0@paramp0 - Start position in display coordinates.@paramp1 - End position in display coordinates.@parampaletteIndex - Palette color index.
drawLine
(this.Demo.projected: {}
@type{Vector2iType[]}
projected
[const edge: anyedge.i], this.Demo.projected: {}
@type{Vector2iType[]}
projected
[const edge: anyedge.j], const edge: anyedge.color);
} for (let let i: numberi = 0; let i: numberi < 16; let i: numberi++) {
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.drawPixel: (posOrX: Vector2i | number, yOrColor: number, maybeColor?: number) => void
Draws a single pixel. Accepts either: - `(posOrX: Vector2i, yOrColor: number)` where `yOrColor` is the palette index. - `(posOrX: number, yOrColor: number, maybeColor: number)` for `(x, y, paletteIndex)`.
@since0.1.0@paramposOrX - Pixel position as `Vector2i`, or x coordinate when using numeric overload.@paramyOrColor - Palette index for vector overload, or y coordinate for numeric overload.@parammaybeColor - Palette index when using numeric overload.
drawPixel
(this.Demo.projected: {}
@type{Vector2iType[]}
projected
[let i: numberi], const C_DOT: 11C_DOT);
} } } function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
(class Demo
@implementsIBTDemo
Demo
);