/*
Hypercube: a Fez-style rotating tesseract wireframe you can spin by dragging.
@description A Fez-style rotating tesseract: a four-dimensional cube turning in PICO-8 colors on a 320x320 canvas.
Part of the BLIT386 demo series.
Prerequisites:
- Basics: https://demos.blit386.dev/basics
- Primitives: https://demos.blit386.dev/primitives
- Palette Presets: https://demos.blit386.dev/palette-presets
- Pointer Basics: https://demos.blit386.dev/pointer-basics
- Pointer Drag Flick: https://demos.blit386.dev/pointer-drag-flick
Live version: https://demos.blit386.dev/hypercube
Guides:
- Input: https://blit386.dev/docs/guides/input#pointer-slot-model
- Input: https://blit386.dev/docs/guides/input#position-and-delta
- Palette Presets: https://blit386.dev/docs/guides/palette-presets#pico8-16-slots
- Random: https://blit386.dev/docs/guides/random#deterministic-by-default
- Overlay: https://blit386.dev/docs/guides/overlay#optional-bands
Reference:
- Rendering: https://blit386.dev/docs/api/rendering#primitives
- Palette: https://blit386.dev/docs/api/palette#built-in-presets
- Core: https://blit386.dev/docs/api/core#hardware-settings
---
WHAT YOU WILL SEE
-----------------
A 320x320 canvas, cleared to PICO-8 slot 1 (dark blue), with a wireframe tesseract turning
in the middle: two cubes drawn in two colors, eight struts linking matching corners, and a
dot on each of the 16 corners. The four line colors drift around the hue wheel at 28
degrees per second with a slow lightness pulse. Edges are drawn farthest first, so the
"inner" cube visibly passes through the "outer" one as the 4D rotation carries it around.
The engine overlay is open from the first frame with its palette grid switched on, drawn
in white bars (slot 7) with dark text (slot 1).
WHAT YOU WILL LEARN
-------------------
- A 4D rotation as a sequence of plane rotations (XW, YZ, XY, ZW, then a screen-space XZ
yaw), each one an ordinary 2D rotation of two coordinates.
- Two perspective divides: 4D to 3D with DIST_4, then 3D to 2D with DIST_3, followed by
scaling and centering on the canvas.
- The painter's algorithm: every tick the 32 edges are sorted by averaged depth, with a
stable tie-break so two edges at equal depth never swap colors.
- Palette.pico8() as the palette, and Color32.fromHSL() rewriting four slots per tick.
- Pointer slots: BT.isPointerActive(slot), BT.pointerDelta(slot), and
BT.isDown(BT.BTN_POINTER_A, 0) for the mouse button.
- Frame-rate-independent integration with BT.deltaSeconds, and BT.timeSeconds as the clock
for the color drift.
- Exponential smoothing, 1 - Math.exp(-k * dt), both for filtering the drag velocity and
for easing the spin back to its home value.
- BT.random.float() for the starting hue, and configure() for displaySize, maxCanvasSize,
and the overlay palette grid.
HOW THE DRAG WORKS
------------------
Any of the four pointer slots can steer. The mouse (slot 0) counts as held while
BT.BTN_POINTER_A is down; a touch slot counts as held while its contact is active. The
first held slot is claimed and kept until it releases. While a slot is held, each tick
reads BT.pointerDelta(slot): horizontal pixels turn the XZ plane (yaw around Y) and
vertical pixels turn the YZ plane (pitch around X), DRAG_SENSITIVITY radians per pixel.
The automatic tumble pauses for the duration of the drag.
The per-tick angular velocity is smoothed with an exponential moving average
(VELOCITY_SMOOTH) so a single noisy frame does not decide the throw. On release the
smoothed velocity becomes spin.xz and spin.yz, clamped to MAX_FLICK_SPIN, and the three
4D tumble planes are zeroed. From then on the spin eases toward HOME_SPIN at rate
SPIN_FADE, so the flick coasts and the Fez tumble gradually returns.
TRY THIS
--------
- Drag with the mouse or a finger to spin the tesseract; flick and let go to make it
coast.
- Watch the palette grid in the engine overlay: slots 8-11 change color every tick while
the rest of the PICO-8 palette holds still. Press the backquote key (`) to hide the
overlay.
*/
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.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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT, class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32, class PaletteMutable 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.Palette, class Vector2iInteger 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.Vector2i } from 'blit386';
/** @typedef {import('blit386').Palette} PaletteType */
/** @typedef {import('blit386').Vector2i} Vector2iType */
/** @typedef {{ i: number, j: number, depth: number, color: number }} Edge */
/** @typedef {{ xw: number, yz: number, xy: number, zw: number, xz: number }} Spin */
// Canvas size in pixels and the projection scale: half the tesseract's projected extent,
// in pixels, before perspective.
const const SIZE: 320SIZE = 320;
const const SCALE: 36SCALE = 36;
// Perspective distance for the 4D to 3D step (larger = flatter).
const const DIST_4: 2.6DIST_4 = 2.6;
// Perspective distance for the 3D to 2D step.
const const DIST_3: 3.2DIST_3 = 3.2;
// PICO-8 slots: dark blue background, white overlay bars.
const const C_BG: 1C_BG = 1;
const const C_NEAR: 7C_NEAR = 7;
// One fixed slot per wire kind. An Edge carries its slot, so depth-sorting the edges
// never swaps which cube a wire belongs to.
const const C_CUBE_A: 8C_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: 8C_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, the fixed saturation and base lightness of the four
// line colors, and the amplitude and rate of the lightness pulse.
const const HUE_SPEED: 28HUE_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: angular velocity in each rotation plane, in radians per
// second. This is the "home" spin the demo always eases back to. `xz` stays 0 at rest;
// drag and flick use it for screen-space yaw.
/** @type {Readonly<Spin>} */
const const HOME_SPIN: 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.01DRAG_SENSITIVITY = 0.01;
// How quickly the finger's instantaneous velocity is smoothed while dragging (higher =
// snappier, lower = softer), so a noisy last frame does not become a wild flick.
const const VELOCITY_SMOOTH: 14VELOCITY_SMOOTH = 14;
// How quickly free spin eases back toward HOME_SPIN after release, per second. Lower =
// a longer coast on the flick before the Fez tumble returns.
const const SPIN_FADE: 1.15SPIN_FADE = 1.15;
// Cap on flick spin so a frantic swipe cannot spin forever.
const const MAX_FLICK_SPIN: 8MAX_FLICK_SPIN = 8;
// The 16 corners of a unit tesseract (+1 or -1 on x, y, z, w). Built once at load and
// never mutated; each tick copies a corner into a scratch vector before rotating it.
/** @type {number[][]} */
const const VERTICES: {}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: {}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]);
}
// Sort comparator: farther edges first. Ties break on the endpoint indices so equal
// depths keep a stable order, which avoids color flicker when two edges share a depth.
function function compareEdgeDepth(a: any, b: any): numbercompareEdgeDepth(a: anya, b: anyb) {
return a: anya.depth - b: anyb.depth || a: anya.i - b: anyb.i || a: anya.j - b: anyb.j;
}
// Clamps one spin component into the range -MAX_FLICK_SPIN..MAX_FLICK_SPIN.
function function clampFlick(value: any): anyclampFlick(value: anyvalue) {
return Math.max(-const MAX_FLICK_SPIN: 8MAX_FLICK_SPIN, Math.min(const MAX_FLICK_SPIN: 8MAX_FLICK_SPIN, value: anyvalue));
}
// A tesseract wireframe rotated in 4D, projected to 2D, and drawn with the painter's
// algorithm. A pointer drag spins it like a trackball; a flick coasts and eases back to
// the automatic tumble.
class class DemoDemo {
/** @type {PaletteType | null} */
Demo.palette: Palette | nullpalette = null;
// Screen positions after the 4D to 2D projection, reused every tick.
/** @type {Vector2iType[]} */
Demo.projected: {}projected = [];
// Per-vertex depth for the painter's algorithm.
/** @type {number[]} */
Demo.depths: {}depths = [];
// The 32 edges, kept sorted farthest first.
/** @type {Edge[]} */
Demo.edgeOrder: {}edgeOrder = [];
// Starting hues for cube A, cube B, the links, and the dots, in degrees.
/** @type {number[]} */
Demo.baseHues: {}baseHues = [];
// Scratch 4D point reused while rotating each corner, so no array is allocated per
// tick.
/** @type {number[]} */
Demo.scratch: {}scratch = [0, 0, 0, 0];
// Current rotation angle in each 4D plane, in radians.
/** @type {number} */
Demo.angleXW: numberangleXW = 0.35;
/** @type {number} */
Demo.angleYZ: numberangleYZ = 0.9;
/** @type {number} */
Demo.angleXY: numberangleXY = 0.1;
/** @type {number} */
Demo.angleZW: numberangleZW = 0.2;
// Screen-space yaw (XZ plane, around Y), driven by drag and flick.
/** @type {number} */
Demo.angleXZ: numberangleXZ = 0;
// Free-motion angular velocity; eases toward HOME_SPIN.
/** @type {Spin} */
Demo.spin: Spinspin = { ...const HOME_SPIN: Readonly<Spin>HOME_SPIN };
// Smoothed finger yaw and pitch in radians per second while dragging. On release
// these become spin.xz and spin.yz so the model coasts.
/** @type {{ xz: number, yz: number }} */
Demo.fingerSpin: {
xz: number;
yz: number;
}
fingerSpin = { xz: numberxz: 0, yz: numberyz: 0 };
// Pointer slot currently steering (-1 = none).
/** @type {number} */
Demo.dragSlot: numberdragSlot = -1;
// A square canvas with the engine overlay open from the start, palette grid included,
// so the four animated slots can be watched alongside the picture.
Demo.configure(): {
displaySize: Vector2i;
maxCanvasSize: Vector2i;
targetFPS: number;
isOverlayEnabled: boolean;
isOverlayVisibleAtStart: boolean;
isOverlayPaletteEnabled: boolean;
overlayStyle: {
barPaletteIndex: number;
textPaletteIndex: number;
gapPaletteIndex: number;
};
}
configure() {
return {
displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const SIZE: 320SIZE, const SIZE: 320SIZE),
maxCanvasSize: Vector2imaxCanvasSize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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,
},
};
}
// Loads the PICO-8 palette, picks the starting hues, and builds the vertex and edge
// tables.
async Demo.init(): unknowninit() {
this.Demo.palette: Palette | nullpalette = class PaletteMutable 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.Palette.Palette.pico8(): PaletteCreates the PICO-8 16-color palette.pico8();
// Spread the four line colors evenly around the hue wheel from a random starting
// angle. BT.random.float(min, max) returns a value in [min, max).
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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.float(min: number, max: number): numberReturns the next pseudo-random float in [min, max).float(0, 360);
this.Demo.baseHues: {}baseHues = [const start: numberstart, const start: numberstart + 90, const start: numberstart + 180, const start: numberstart + 270];
this.Demo.applyLineColors(timeSeconds: any): voidapplyLineColors(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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteSet: (palette: Palette) => voidStores 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.paletteSet(this.Demo.palette: Palettepalette);
for (let let i: numberi = 0; let i: numberi < 16; let i: numberi++) {
this.Demo.projected: {}projected.push(new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(0, 0));
this.Demo.depths: {}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: 8C_CUBE_A;
this.Demo.edgeOrder: {}edgeOrder.push({ i: numberi, j: numberj, depth: numberdepth: 0, color: numbercolor });
}
}
}
return true;
}
// Rewrites the four line slots for the given time: each hue drifts at HUE_SPEED and
// the lightness pulses on a sine, phase-shifted per slot.
Demo.applyLineColors(timeSeconds: any): voidapplyLineColors(timeSeconds: anytimeSeconds) {
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: {}baseHues[let i: numberi] + timeSeconds: anytimeSeconds * const HUE_SPEED: 28HUE_SPEED) % 360;
const const light: numberlight = const LINE_LIGHT: 58LINE_LIGHT + Math.sin(timeSeconds: anytimeSeconds * 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 | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const LINE_SLOTS: {}LINE_SLOTS[let i: numberi], class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32.Color32.fromHSL(h: number, s: number, l: number, a?: number): Color32Creates a color from HSL values.fromHSL(const hue: numberhue, const LINE_SAT: 88LINE_SAT, const light: numberlight));
}
}
// Rotates the 4D point `v` in the plane of axes `a` and `b` (0 = X, 1 = Y, 2 = Z,
// 3 = W). `c` and `s` are the cosine and sine of the angle, precomputed once per
// tick.
Demo.rotatePlane(v: any, c: any, s: any, a: any, b: any): voidrotatePlane(v: anyv, c: anyc, s: anys, a: anya, b: anyb) {
const const x: anyx = v: anyv[a: anya];
const const y: anyy = v: anyv[b: anyb];
v: anyv[a: anya] = const x: anyx * c: anyc - const y: anyy * s: anys;
v: anyv[b: anyb] = const x: anyx * s: anys + const y: anyy * c: anyc;
}
// True while a pointer slot should steer. The mouse (slot 0) counts while its primary
// button is held; a touch slot counts while its contact is active. Same rule as the
// Pointer Paint demo.
Demo.isDragHeld(slot: any): booleanisDragHeld(slot: anyslot) {
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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.isPointerActive: (pointerIndex?: number) => booleanReports 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.isPointerActive(slot: anyslot)) {
return false;
}
return slot: anyslot === 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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.isDown: (button: number, player?: number) => booleanChecks 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.
`BTN_L2`, `BTN_R2` and `BTN_TRIGGER` are gamepad only (no keyboard mapping).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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.type BTN_POINTER_A: numberPrimary pointer button code.
Maps to mouse left for slot 0; touch contact for slots 1-3.BTN_POINTER_A, 0) : true;
}
// Advances every plane angle by its angular velocity over `dt` seconds.
Demo.integrateSpin(dt: any): voidintegrateSpin(dt: anydt) {
this.Demo.angleXW: numberangleXW += this.Demo.spin: Spinspin.xw: numberxw * dt: anydt;
this.Demo.angleYZ: numberangleYZ += this.Demo.spin: Spinspin.yz: numberyz * dt: anydt;
this.Demo.angleXY: numberangleXY += this.Demo.spin: Spinspin.xy: numberxy * dt: anydt;
this.Demo.angleZW: numberangleZW += this.Demo.spin: Spinspin.zw: numberzw * dt: anydt;
this.Demo.angleXZ: numberangleXZ += this.Demo.spin: Spinspin.xz: numberxz * dt: anydt;
}
// Exponential ease of `spin` toward HOME_SPIN: a soft settle with no hard stop.
Demo.fadeSpinToHome(dt: any): voidfadeSpinToHome(dt: anydt) {
const const t: numbert = 1 - Math.exp(-const SPIN_FADE: 1.15SPIN_FADE * dt: anydt);
this.Demo.spin: Spinspin.xw: numberxw += (const HOME_SPIN: Readonly<Spin>HOME_SPIN.xw - this.Demo.spin: Spinspin.xw: numberxw) * const t: numbert;
this.Demo.spin: Spinspin.yz: numberyz += (const HOME_SPIN: Readonly<Spin>HOME_SPIN.yz - this.Demo.spin: Spinspin.yz: numberyz) * const t: numbert;
this.Demo.spin: Spinspin.xy: numberxy += (const HOME_SPIN: Readonly<Spin>HOME_SPIN.xy - this.Demo.spin: Spinspin.xy: numberxy) * const t: numbert;
this.Demo.spin: Spinspin.zw: numberzw += (const HOME_SPIN: Readonly<Spin>HOME_SPIN.zw - this.Demo.spin: Spinspin.zw: numberzw) * const t: numbert;
this.Demo.spin: Spinspin.xz: numberxz += (const HOME_SPIN: Readonly<Spin>HOME_SPIN.xz - this.Demo.spin: Spinspin.xz: numberxz) * const t: numbert;
}
// Trackball drag plus flick inertia. While held: yaw and pitch from the pointer
// delta, and an exponential moving average of the finger velocity. On release: that
// velocity becomes `spin`, which then fades back to HOME_SPIN.
Demo.updateDrag(dt: any): voidupdateDrag(dt: anydt) {
const const wasDragging: booleanwasDragging = this.Demo.dragSlot: numberdragSlot >= 0;
// Stick with the current finger; otherwise claim the first held slot.
if (this.Demo.dragSlot: numberdragSlot >= 0 && !this.Demo.isDragHeld(slot: any): booleanisDragHeld(this.Demo.dragSlot: numberdragSlot)) {
this.Demo.dragSlot: numberdragSlot = -1;
}
if (this.Demo.dragSlot: numberdragSlot < 0) {
for (let let slot: numberslot = 0; let slot: numberslot < 4; let slot: numberslot++) {
if (this.Demo.isDragHeld(slot: any): booleanisDragHeld(let slot: numberslot)) {
this.Demo.dragSlot: numberdragSlot = let slot: numberslot;
// Drop press-frame jitter so it cannot become a throw.
this.Demo.fingerSpin: {
xz: number;
yz: number;
}
fingerSpin.xz: numberxz = 0;
this.Demo.fingerSpin: {
xz: number;
yz: number;
}
fingerSpin.yz: numberyz = 0;
break;
}
}
}
if (this.Demo.dragSlot: numberdragSlot >= 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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.pointerDelta: (pointerIndex?: number) => Vector2iReturns 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.pointerDelta(this.Demo.dragSlot: numberdragSlot);
// Trackball: horizontal pixels yaw (XZ, around Y); vertical pixels pitch (YZ,
// around X).
const const dYaw: numberdYaw = const delta: Vector2idelta.Vector2i.x: numberHorizontal component (defaults to 0).x * const DRAG_SENSITIVITY: 0.01DRAG_SENSITIVITY;
const const dPitch: numberdPitch = const delta: Vector2idelta.Vector2i.y: numberVertical component (defaults to 0).y * const DRAG_SENSITIVITY: 0.01DRAG_SENSITIVITY;
this.Demo.angleXZ: numberangleXZ += const dYaw: numberdYaw;
this.Demo.angleYZ: numberangleYZ += const dPitch: numberdPitch;
// Guard dt so a hitch cannot explode the velocity sample.
if (dt: anydt > 0.0001) {
const const alpha: numberalpha = 1 - Math.exp(-const VELOCITY_SMOOTH: 14VELOCITY_SMOOTH * dt: anydt);
this.Demo.fingerSpin: {
xz: number;
yz: number;
}
fingerSpin.xz: numberxz += (const dYaw: numberdYaw / dt: anydt - this.Demo.fingerSpin: {
xz: number;
yz: number;
}
fingerSpin.xz: numberxz) * const alpha: numberalpha;
this.Demo.fingerSpin: {
xz: number;
yz: number;
}
fingerSpin.yz: numberyz += (const dPitch: numberdPitch / dt: anydt - this.Demo.fingerSpin: {
xz: number;
yz: number;
}
fingerSpin.yz: numberyz) * const alpha: numberalpha;
}
// The automatic tumble pauses while a drag is in progress.
return;
}
// Coast on the flick (trackball axes only); the fade restores the Fez planes.
if (const wasDragging: booleanwasDragging) {
this.Demo.spin: Spinspin.xz: numberxz = function clampFlick(value: any): anyclampFlick(this.Demo.fingerSpin: {
xz: number;
yz: number;
}
fingerSpin.xz: numberxz);
this.Demo.spin: Spinspin.yz: numberyz = function clampFlick(value: any): anyclampFlick(this.Demo.fingerSpin: {
xz: number;
yz: number;
}
fingerSpin.yz: numberyz);
this.Demo.spin: Spinspin.xw: numberxw = 0;
this.Demo.spin: Spinspin.xy: numberxy = 0;
this.Demo.spin: Spinspin.zw: numberzw = 0;
}
this.Demo.integrateSpin(dt: any): voidintegrateSpin(dt: anydt);
this.Demo.fadeSpinToHome(dt: any): voidfadeSpinToHome(dt: anydt);
}
// Advances the drag/spin state, rotates and projects all 16 corners, and re-sorts the
// edges by depth.
Demo.update(): voidupdate() {
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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.deltaSeconds: numberFixed-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.deltaSeconds;
this.Demo.updateDrag(dt: any): voidupdateDrag(const dt: numberdt);
this.Demo.applyLineColors(timeSeconds: any): voidapplyLineColors(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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.timeSeconds: numberFixed-step elapsed time in seconds (`BT.ticks * BT.deltaSeconds`).timeSeconds);
const const cx: numbercx = const SIZE: 320SIZE / 2;
const const cy: numbercy = const SIZE: 320SIZE / 2;
// Sine and cosine of each plane angle, computed once per tick rather than once
// per corner.
const const cXW: anycXW = Math.cos(this.Demo.angleXW: numberangleXW);
const const sXW: anysXW = Math.sin(this.Demo.angleXW: numberangleXW);
const const cYZ: anycYZ = Math.cos(this.Demo.angleYZ: numberangleYZ);
const const sYZ: anysYZ = Math.sin(this.Demo.angleYZ: numberangleYZ);
const const cXY: anycXY = Math.cos(this.Demo.angleXY: numberangleXY);
const const sXY: anysXY = Math.sin(this.Demo.angleXY: numberangleXY);
const const cZW: anycZW = Math.cos(this.Demo.angleZW: numberangleZW);
const const sZW: anysZW = Math.sin(this.Demo.angleZW: numberangleZW);
const const cXZ: anycXZ = Math.cos(this.Demo.angleXZ: numberangleXZ);
const const sXZ: anysXZ = Math.sin(this.Demo.angleXZ: numberangleXZ);
const const v: {}v = this.Demo.scratch: {}scratch;
for (let let i: numberi = 0; let i: numberi < 16; let i: numberi++) {
const const src: anysrc = const VERTICES: {}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];
// The 4D tumble planes first, then the screen-space yaw (XZ), so a horizontal
// drag turns the projected object like a solid in front of the viewer.
this.Demo.rotatePlane(v: any, c: any, s: any, a: any, b: any): voidrotatePlane(const v: {}v, const cXW: anycXW, const sXW: anysXW, 0, 3);
this.Demo.rotatePlane(v: any, c: any, s: any, a: any, b: any): voidrotatePlane(const v: {}v, const cYZ: anycYZ, const sYZ: anysYZ, 1, 2);
this.Demo.rotatePlane(v: any, c: any, s: any, a: any, b: any): voidrotatePlane(const v: {}v, const cXY: anycXY, const sXY: anysXY, 0, 1);
this.Demo.rotatePlane(v: any, c: any, s: any, a: any, b: any): voidrotatePlane(const v: {}v, const cZW: anycZW, const sZW: anysZW, 2, 3);
this.Demo.rotatePlane(v: any, c: any, s: any, a: any, b: any): voidrotatePlane(const v: {}v, const cXZ: anycXZ, const sXZ: anysXZ, 0, 2);
// 4D to 3D perspective: points farther along W shrink toward the origin.
const const w: numberw = const DIST_4: 2.6DIST_4 / (const DIST_4: 2.6DIST_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 to 2D perspective, then scale and center on the canvas.
const const p: numberp = const DIST_3: 3.2DIST_3 / (const DIST_3: 3.2DIST_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: {}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: {}depths[let i: numberi] = const z3: numberz3 + const v: {}v[3] * 0.35;
}
// Update the depth on each edge in place. Rebuilding the edges from a sorted
// index list would pair the wrong color with the wrong endpoints and flash.
for (let let e: numbere = 0; let e: numbere < this.Demo.edgeOrder: {}edgeOrder.length; let e: numbere++) {
const const edge: anyedge = this.Demo.edgeOrder: {}edgeOrder[let e: numbere];
const edge: anyedge.depth = (this.Demo.depths: {}depths[const edge: anyedge.i] + this.Demo.depths: {}depths[const edge: anyedge.j]) * 0.5;
}
this.Demo.edgeOrder: {}edgeOrder.sort(function compareEdgeDepth(a: any, b: any): numbercompareEdgeDepth);
}
// Draws the edges farthest first, then a dot on every corner.
Demo.render(): voidrender() {
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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.clear: (paletteIndex: number) => voidSets 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.clear(const C_BG: 1C_BG);
for (let let e: numbere = 0; let e: numbere < this.Demo.edgeOrder: {}edgeOrder.length; let e: numbere++) {
const const edge: anyedge = this.Demo.edgeOrder: {}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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawLine: (p0: Vector2i, p1: Vector2i, paletteIndex: number) => voidDraws a pixel-perfect line between two points.
Uses rasterized line drawing without antialiasing.drawLine(this.Demo.projected: {}projected[const edge: anyedge.i], this.Demo.projected: {}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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawPixel: (posOrX: Vector2i | number, yOrColor: number, maybeColor?: number) => voidDraws 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)`.drawPixel(this.Demo.projected: {}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.bootstrap(class DemoDemo);