// Bunnymark - how many bouncing sprites the engine can move before a frame slips.
// @description Spawn bouncing bunnies and count the quads WebGPU skips past its sprite cap.
//
// What you will see:
//   - An original pixel bunny (not the Pixi.js rabbit) in several colors
//   - Click, hold, or a gamepad button adds them in batches
//   - A counter for how many were drawn and how many this demo did not submit
//   - An optional mode that alternates several copies of the picture so batches break
//
// Prerequisites:
//   Basics        https://demos.blit386.dev/basics
//   Sprites       https://demos.blit386.dev/sprites
//
// Guide: https://blit386.dev/docs/api/rendering
// Live version: https://demos.blit386.dev/bunnymark
//
// The engine overlay (press ` or the bottom-left corner) shows Present FPS, update(),
// render(), Draw Calls, and a timing chart between the title and the Present row.
// This demo turns that overlay and the chart on at startup. Draw Calls
// counts each BT.drawSprite, so it climbs with the bunny count in both sheet modes.
// The Batches row follows the sprite pipeline's rule (a new batch whenever the
// sheet changes), because the engine does not report that count yet (BT-114).
// One sheet stays one batch, and alternating sheets breaks that batch on every bunny.
// The overlay's bottom row (Prim / Spr) is the engine's own vertex count and
// overflow ("ov"). Dropped on this panel is bunnies this demo did not submit.
// ov stays 0 while the letter reserve holds; a rising ov means the panel used
// more quads than that reserve.
//
// Cap on the panel is 8333. The WebGPU sprite pipeline keeps 50,000 vertices for
// one frame. Each sprite is one quad, two triangles, so 6 vertices.
// floor(50000 / 6) is 8333. The next quad does not fit: the pipeline drops it and
// logs a warning. Bunnies stop 512 quads earlier, because the panel letters are
// sprites in that same buffer. That reserve keeps the readout from being the
// thing that gets dropped. The software renderer queues every sprite and has no
// vertex buffer of this size, so with ?backend=software this demo submits every
// bunny. Dropped stays 0 there, and the Cap row is hidden. The lists start
// 16384 long. WebGPU stops there. Software doubles them when a batch does not
// fit, up to 262144.
//
// For repeatable timing runs, two URL switches skip the clicking:
//   ?bunnies=5000   start with that many bunnies instead of one batch
//   ?split          start with Split sheets already on
// A missing ?bunnies starts one batch. A present value must be a whole number from 1
// to the field limit; anything else stops startup. Add &backend=software
// to time the software renderer.

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;
    ... 109 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 Rect2i
Integer rectangle for pixel-perfect bounds and regions. Used throughout the engine for sprite regions, display-space bounds, and zero-allocation geometry helpers. Both convenience getters and allocation-free `*To()` helpers are provided so callers can choose between readability and hot-path efficiency.
@since0.1.0
Rect2i
,
type SpriteSheet = SpriteSheet
class SpriteSheet
Sprite-sheet wrapper around a loaded image asset. The class keeps the original image available for CPU-side inspection while lazily creating and caching a GPU texture for rendering. When possible, `load()` also pre-decodes the source into an `ImageBitmap` so texture uploads preserve pixel-art alpha and color values more reliably. After calling `indexize()`, the sheet stores palette indices rather than RGBA data. The GPU texture becomes an `r8uint` format uploaded via `writeTexture`. The original RGBA bytes are retained so `reindexize()` can re-convert without reloading the image.
@since0.1.0
SpriteSheet
, 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';
import { import canvasToImagecanvasToImage } from './shared/canvas-sprites.js'; import { import applyThemeapplyTheme, import THEME_DEFAULT_START_SLOTTHEME_DEFAULT_START_SLOT, import THEME_PANEL_OFFSETTHEME_PANEL_OFFSET, import THEME_TEXT_OFFSETTHEME_TEXT_OFFSET, import uiui, import UI_ANCHORSUI_ANCHORS, } from './shared/ui.js'; /** @typedef {import('blit386').IBTDemo} IBTDemo */ /** @typedef {import('blit386').HardwareSettings} HardwareSettings */ /** @typedef {import('blit386').Palette} Palette */ /** @typedef {import('blit386').SpriteSheet} SpriteSheet */ // Paint-by-number for one front-facing bunny. Each character is a palette slot in the // first color set ('.' is empty air). A second picture is never stored: later colors are // the same numbers slid up the palette with drawSprite's paletteOffset. // // This is original art. It is not the side-view rabbit from the Pixi.js bunnymark. const const BUNNY_ROWS: {}BUNNY_ROWS = [ '....11....11....', '...1331..1331...', '...1331..1331...', '...1333111331...', '....12222221....', '...1222222221...', '..122244224221..', '..122222222221..', '...1222333221...', '...1222222221...', '....12222221....', '.....122221.....', '.....11..11.....', '....111..111....', '...11......11...', '..11........11..', ]; const const BUNNY_W: anyBUNNY_W = const BUNNY_ROWS: {}BUNNY_ROWS[0].length; const const BUNNY_H: anyBUNNY_H = const BUNNY_ROWS: {}BUNNY_ROWS.length; // How many bunnies one click or one held tick adds. A batch is a handful at once, // the way the classic stress test dumps sprites in instead of one at a time. const const SPAWN_BATCH: 100SPAWN_BATCH = 100; // Hard stop on WebGPU, and the length the lists start at. High enough to walk // past the sprite cap and show dropped quads. Software copies these lists into // a longer one when a batch does not fit, up to MAX_SOFTWARE_BUNNIES. const const MAX_WEBGPU_BUNNIES: 16384MAX_WEBGPU_BUNNIES = 16384; // Furthest the software field may grow. 16384 doubled four times. A held button // still has to stop somewhere. const const MAX_SOFTWARE_BUNNIES: 262144MAX_SOFTWARE_BUNNIES = 262144; // SpritePipeline keeps MAX_VERTICES vertices and spends 6 of them on each quad // (two triangles). 50000 / 6 = 8333 sprites, then further quads are dropped. // Manual-sync hazard: packages/blit386/src/render/SpritePipeline.ts (MAX_VERTICES // and the `6 * VALUES_PER_VERTEX` quad size). Change both together. const const SPRITE_VERTEX_CAP: 50000SPRITE_VERTEX_CAP = 50000; const const VERTICES_PER_QUAD: 6VERTICES_PER_QUAD = 6; const const SPRITE_QUAD_CAP: anySPRITE_QUAD_CAP = Math.floor(const SPRITE_VERTEX_CAP: 50000SPRITE_VERTEX_CAP / const VERTICES_PER_QUAD: 6VERTICES_PER_QUAD); // The on-screen panel is drawn with the system font, and those letters share the // same sprite buffer as the bunnies. Leave room so the readout is not the thing // that gets dropped. 512 quads is a generous budget for this panel's letters. const const HUD_QUAD_RESERVE: 512HUD_QUAD_RESERVE = 512; const const BUNNY_DRAW_CAP: numberBUNNY_DRAW_CAP = const SPRITE_QUAD_CAP: anySPRITE_QUAD_CAP - const HUD_QUAD_RESERVE: 512HUD_QUAD_RESERVE; // First palette slot of the bunny colors. Slot 0 stays transparent (empty pixels). // The shared UI theme occupies 240-251, far above these blocks. const const COLOR_BASE: 1COLOR_BASE = 1; // How many colors one bunny uses: outline, fur, inner ear / belly, eye. const const COLOR_COUNT: 4COLOR_COUNT = 4; // Separate copies of the same picture. Alternating them forces a new GPU batch // on every bunny, because the sprite pipeline flushes when the texture changes. const const SHEET_COUNT: 4SHEET_COUNT = 4; // The engine has four pointer slots: 0 is the mouse, 1-3 are touches. const const POINTER_SLOT_COUNT: 4POINTER_SLOT_COUNT = 4; // Face button A is also Space / KeyB for player 0, so a keyboard can spawn too. // KeyN is the on-screen Add button's own key, separate from that face button. const const KEY_ADD: "KeyN"KEY_ADD = 'KeyN'; // Same seed every load so the first hops repeat. BT.randomSeed in init() wins // over a ?seed= URL on purpose: this demo wants one known stream. const const RANDOM_SEED: 553RANDOM_SEED = 553; // Pixels added to downward speed each tick. Positive Y points down the screen, // so a positive gravity pulls bunnies toward the floor. const const GRAVITY: 0.4GRAVITY = 0.4; // How much upward speed survives a floor hit. 0.75 means each bounce is a bit // shorter than the last, like a ball that doesn't quite return to your hand. const const RESTITUTION: 0.75RESTITUTION = 0.75; // A landing softer than this (upward speed, so negative) gets a fresh hop, or // the pile would eventually sit still and stop looking like a stress test. const const HOP_LIMIT: -1.5HOP_LIMIT = -1.5; const const HOP_MIN: 2HOP_MIN = 2; const const HOP_MAX: 6HOP_MAX = 6; // Sideways speed and the upward kick (negative Y) given to a new bunny. const const SPAWN_VX_MIN: -4SPAWN_VX_MIN = -4; const const SPAWN_VX_MAX: 4SPAWN_VX_MAX = 4; const const SPAWN_VY_MIN: -8SPAWN_VY_MIN = -8; const const SPAWN_VY_MAX: -2SPAWN_VY_MAX = -2; // Overlay bands this demo turns on: title, timing chart, Present row, timing // text, and the sprite diagnostics row. Heights match the engine // (OVERLAY_BAR_HEIGHT, OVERLAY_ROW_GAP_PX, DEFAULT_TIMING_CHART_HEIGHT in // packages/blit386/src/overlay). Copy a change here or the panel slides // under the chart. const const OVERLAY_BAR_H: 13OVERLAY_BAR_H = 13; const const OVERLAY_ROW_GAP: 1OVERLAY_ROW_GAP = 1; const const TIMING_CHART_H: 22TIMING_CHART_H = 22; const const OVERLAY_TEXT_ROWS: 4OVERLAY_TEXT_ROWS = 4; // Each text row is a bar plus the gap after it. The chart band has no gap of its own // in that count: four gaps sit between the five bands, and the stack ends on the // diagnostics bar. const const OVERLAY_BOTTOM: numberOVERLAY_BOTTOM = const OVERLAY_TEXT_ROWS: 4OVERLAY_TEXT_ROWS * (const OVERLAY_BAR_H: 13OVERLAY_BAR_H + const OVERLAY_ROW_GAP: 1OVERLAY_ROW_GAP) + const TIMING_CHART_H: 22TIMING_CHART_H; // First pixel under those bars. Bunnies bounce here, and a fountain batch appears here. const const ARENA_TOP: numberARENA_TOP = const OVERLAY_BOTTOM: numberOVERLAY_BOTTOM + 1; // The panel has a fixed width. 118 pixels fits the widest row, the Split sheets // checkbox: a 10-pixel box, 16 letters of 6 pixels, and the padding around them. // Sprites draw on top of panels, so a bunny on the left can cover the numbers. const const PANEL_W: 118PANEL_W = 118; // Gap between the screen edge and the panel, and the panel's top edge. The top edge // sits below the engine overlay, including the timing chart, so the two never overlap. const const PANEL_MARGIN: 4PANEL_MARGIN = 4; const const PANEL_CLEARANCE: 3PANEL_CLEARANCE = 3; const const PANEL_Y: numberPANEL_Y = const OVERLAY_BOTTOM: numberOVERLAY_BOTTOM + const PANEL_CLEARANCE: 3PANEL_CLEARANCE; // URL switches for timing runs (see the header comment). A present ?bunnies must be a // whole number from 1 to the field limit: MAX_WEBGPU_BUNNIES on WebGPU, MAX_SOFTWARE_BUNNIES // on software. A missing one means "start with one batch". const const PARAM_BUNNIES: "bunnies"PARAM_BUNNIES = 'bunnies'; const const PARAM_SPLIT: "split"PARAM_SPLIT = 'split'; // Six recolors of the same four slots. Order inside each row is outline, fur, // inner ear / belly, eye. paletteOffset selects a row at draw time. const const VARIANT_TABLE: {}VARIANT_TABLE = [ [function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(42, 26, 18), function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(232, 192, 144), function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(240, 160, 176), function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(26, 18, 12)],
[function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(90, 90, 96), function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(244, 244, 248), function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(245, 190, 200), function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(40, 40, 48)],
[function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(40, 40, 44), function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(140, 140, 148), function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(196, 140, 156), function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(20, 20, 24)],
[function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(12, 12, 16), function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(48, 44, 52), function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(220, 120, 150), function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(230, 230, 236)],
[function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(90, 42, 16), function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(230, 140, 48), function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(245, 196, 170), function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(40, 20, 12)],
[function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(24, 40, 64), function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(150, 186, 214), function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(214, 230, 242), function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(12, 20, 36)],
]; const const VARIANT_COUNT: anyVARIANT_COUNT = const VARIANT_TABLE: {}VARIANT_TABLE.length; // Overlay colors. configure() runs before applyTheme(), so these are the slots // the theme will fill (start + offset), not this.theme which does not exist yet. const const UI_TEXT: anyUI_TEXT = import THEME_DEFAULT_START_SLOTTHEME_DEFAULT_START_SLOT + import THEME_TEXT_OFFSETTHEME_TEXT_OFFSET; const const UI_PANEL: anyUI_PANEL = import THEME_DEFAULT_START_SLOTTHEME_DEFAULT_START_SLOT + import THEME_PANEL_OFFSETTHEME_PANEL_OFFSET; /** * One opaque color. Alpha 255 so indexize() can match it exactly. * * @param {number} r * @param {number} g * @param {number} b * @returns {Color32} */ function function rgb(r: number, g: number, b: number): Color32
One opaque color. Alpha 255 so indexize() can match it exactly.
@paramr@paramg@paramb@returns
rgb
(r: number
@paramr
r
, g: number
@paramg
g
, b: number
@paramb
b
) {
return new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(r: number
@paramr
r
, g: number
@paramg
g
, b: number
@paramb
b
, 255);
} // Which bunny color a grid character stands for. -1 means "skip this pixel" (the '.' // empty cells). A character missing from this table is a typo in BUNNY_ROWS: indexize() // would still succeed and ship a shifted picture, so the painter throws instead. const
const PIXEL_COLOR_INDEX: {
    '.': number;
    1: number;
    2: number;
    3: number;
    4: number;
}
PIXEL_COLOR_INDEX
= {
'.': -1, 1: 0, 2: 1, 3: 2, 4: 3, }; /** * Checks that every row of the bunny grid is the same width. A short row would * shift the picture and indexize() would still succeed, so fail here instead. */ function function assertBunnyGrid(): void
Checks that every row of the bunny grid is the same width. A short row would shift the picture and indexize() would still succeed, so fail here instead.
assertBunnyGrid
() {
for (let let y: numbery = 1; let y: numbery < const BUNNY_ROWS: {}BUNNY_ROWS.length; let y: numbery++) { if (const BUNNY_ROWS: {}BUNNY_ROWS[let y: numbery].length !== const BUNNY_W: anyBUNNY_W) { throw new Error(`Bunny row ${let y: numbery} is ${const BUNNY_ROWS: {}BUNNY_ROWS[let y: numbery].length} pixels wide; expected ${const BUNNY_W: anyBUNNY_W}.`); } } } /** * Paints the bunny into an offscreen canvas using only the first color row. * Pixels are written one by one (no smooth edges), so each color matches a * palette slot exactly when the sheet is indexized. * * @param {Color32[]} colors - The four base colors, outline through eye. * @returns {OffscreenCanvas} */ function function buildBunnyCanvas(colors: Color32[]): OffscreenCanvas
Paints the bunny into an offscreen canvas using only the first color row. Pixels are written one by one (no smooth edges), so each color matches a palette slot exactly when the sheet is indexized.
@paramcolors - The four base colors, outline through eye.@returns
buildBunnyCanvas
(colors: {}
- The four base colors, outline through eye.
@paramcolors - The four base colors, outline through eye.
colors
) {
function assertBunnyGrid(): void
Checks that every row of the bunny grid is the same width. A short row would shift the picture and indexize() would still succeed, so fail here instead.
assertBunnyGrid
();
const const canvas: anycanvas = new OffscreenCanvas(const BUNNY_W: anyBUNNY_W, const BUNNY_H: anyBUNNY_H); const const ctx: anyctx = const canvas: anycanvas.getContext('2d'); const const image: anyimage = const ctx: anyctx.createImageData(const BUNNY_W: anyBUNNY_W, const BUNNY_H: anyBUNNY_H); const const data: anydata = const image: anyimage.data; for (let let y: numbery = 0; let y: numbery < const BUNNY_H: anyBUNNY_H; let y: numbery++) { const const row: anyrow = const BUNNY_ROWS: {}BUNNY_ROWS[let y: numbery]; for (let let x: numberx = 0; let x: numberx < const BUNNY_W: anyBUNNY_W; let x: numberx++) { const const colorIndex: anycolorIndex =
const PIXEL_COLOR_INDEX: {
    '.': number;
    1: number;
    2: number;
    3: number;
    4: number;
}
PIXEL_COLOR_INDEX
[const row: anyrow[let x: numberx]];
if (const colorIndex: anycolorIndex === var undefinedundefined) { throw new Error(`Bunny pixel "${const row: anyrow[let x: numberx]}" is not "." or "1"-"4".`); } if (const colorIndex: anycolorIndex < 0) { continue; } // Four numbers per pixel: red, green, blue, then opacity. const const i: numberi = (let y: numbery * const BUNNY_W: anyBUNNY_W + let x: numberx) * 4; const const color: anycolor = colors: {}
- The four base colors, outline through eye.
@paramcolors - The four base colors, outline through eye.
colors
[const colorIndex: anycolorIndex];
const data: anydata[const i: numberi] = const color: anycolor.r; const data: anydata[const i: numberi + 1] = const color: anycolor.g; const data: anydata[const i: numberi + 2] = const color: anycolor.b; const data: anydata[const i: numberi + 3] = 255; } } const ctx: anyctx.putImageData(const image: anyimage, 0, 0); return const canvas: anycanvas; } /** * Writes every recolor into the palette. Row 0 lands on COLOR_BASE. Each later * row starts COLOR_COUNT slots higher, which is exactly the paletteOffset the * draw loop adds for that variant. * * @param {Palette} palette */ function function installVariantColors(palette: Palette): void
Writes every recolor into the palette. Row 0 lands on COLOR_BASE. Each later row starts COLOR_COUNT slots higher, which is exactly the paletteOffset the draw loop adds for that variant.
@parampalette
installVariantColors
(palette: Palette
@parampalette
palette
) {
const const baseRow: anybaseRow = const VARIANT_TABLE: {}VARIANT_TABLE[0]; for (let let variant: numbervariant = 0; let variant: numbervariant < const VARIANT_COUNT: anyVARIANT_COUNT; let variant: numbervariant++) { const const row: anyrow = const VARIANT_TABLE: {}VARIANT_TABLE[let variant: numbervariant]; // fillBlock walks the base row and writes transform()'s color into the // next slot. The transform ignores the base color and picks this row, // so all six blocks are the same length. palette: Palette
@parampalette
palette
.Palette.fillBlock(start: number, source: readonly Color32[], transform: (color: Color32, index: number) => Color32): number
Writes a transformed block of colors into contiguous palette slots. Writes `transform(source[i], i)` into slot `start + i` for every `i` in `[0, source.length)`, delegating each write to {@link set } so it inherits {@link set } 's validation, including the rule that slot 0 must stay transparent. Collapses the common pattern of looping `palette.set(start + i, transform(baseColors[i]))` into one call.
@since1.7.0@paramstart - First palette index to write.@paramsource - Source colors to read from, in order. `source[i]` maps to slot `start + i`.@paramtransform - Called once per source color as `transform(color, i)`; its return value is written to slot `start + i`.@returnsThe next free slot after the written block (`start + source.length`), for chaining further writes.@throwsError if `start` is not a non-negative integer.@throwsError if the block would exceed the palette size.@throwsError if an individual slot write is invalid - see {@link set}.
fillBlock
(const COLOR_BASE: 1COLOR_BASE + let variant: numbervariant * const COLOR_COUNT: 4COLOR_COUNT, const baseRow: anybaseRow, (_base: Color32_base, index: numberindex) => const row: anyrow[index: numberindex]);
} } /** * One picture, uploaded SHEET_COUNT times. Each SpriteSheet owns its own GPU * texture, so switching sheets is a real batch break, not a shared upload. * * @param {Palette} palette * @returns {Promise<SpriteSheet[]>} */ async function function loadBunnySheets(palette: Palette): Promise<SpriteSheet[]>
One picture, uploaded SHEET_COUNT times. Each SpriteSheet owns its own GPU texture, so switching sheets is a real batch break, not a shared upload.
@parampalette@returns
loadBunnySheets
(palette: Palette
@parampalette
palette
) {
const const canvas: OffscreenCanvascanvas = function buildBunnyCanvas(colors: Color32[]): OffscreenCanvas
Paints the bunny into an offscreen canvas using only the first color row. Pixels are written one by one (no smooth edges), so each color matches a palette slot exactly when the sheet is indexized.
@paramcolors - The four base colors, outline through eye.@returns
buildBunnyCanvas
(const VARIANT_TABLE: {}VARIANT_TABLE[0]);
const const image: anyimage = await import canvasToImagecanvasToImage(const canvas: OffscreenCanvascanvas); const const sheets: {}sheets = []; for (let let i: numberi = 0; let i: numberi < const SHEET_COUNT: 4SHEET_COUNT; let i: numberi++) { const const sheet: SpriteSheetsheet = new new SpriteSheet(image: HTMLImageElement | null, size?: Vector2i): SpriteSheet
Creates a sprite sheet from a loaded image. Use the static load() method for easier loading from URL.
@paramimage - Pre-loaded HTMLImageElement, or null for raw indexed data sheets.@paramsize - Explicit dimensions (required when image is null).
SpriteSheet
(const image: anyimage);
const sheet: SpriteSheetsheet.SpriteSheet.indexize(palette: Palette): void
Converts the sprite sheet's RGBA pixels to palette indices. Each non-transparent pixel is looked up in the provided palette via exact color matching. Index 0 is always transparent. The resulting indices are stored internally; an `r8uint` GPU texture is created lazily on the next `getTexture()` call. The original RGBA data is retained so `reindexize()` can re-convert after a palette swap without reloading the image.
@parampalette - Active palette used for color-to-index mapping.@throwsIf any opaque pixel's color is not present in the palette.
indexize
(palette: Palette
@parampalette
palette
);
const sheets: {}sheets.push(const sheet: SpriteSheetsheet); } return const sheets: {}sheets; } function function bunnyCountFromParams(params: any, fieldMax: any): anybunnyCountFromParams(params: anyparams, fieldMax: anyfieldMax) { const const raw: anyraw = params: anyparams.get(const PARAM_BUNNIES: "bunnies"PARAM_BUNNIES); if (const raw: anyraw === null) { return const SPAWN_BATCH: 100SPAWN_BATCH; } if (!/^[1-9]\d*$/.test(const raw: anyraw)) { throw new Error(`?${const PARAM_BUNNIES: "bunnies"PARAM_BUNNIES} must be a whole number from 1 to ${fieldMax: anyfieldMax}, not "${const raw: anyraw}".`); } const const asked: anyasked = Number(const raw: anyraw); if (!Number.isSafeInteger(const asked: anyasked) || const asked: anyasked > fieldMax: anyfieldMax) { throw new Error(`?${const PARAM_BUNNIES: "bunnies"PARAM_BUNNIES}=${const raw: anyraw} is above the field limit of ${fieldMax: anyfieldMax}.`); } return const asked: anyasked; } /** * Copies one column into a longer typed array of the same kind. * A Float32Array cannot grow in place, so the old numbers are copied across. * * @param {Float32Array | Uint8Array} src * @param {number} next - New length. Must be greater than src.length. * @returns {Float32Array | Uint8Array} */ function function copyField(src: Float32Array | Uint8Array, next: number): Float32Array | Uint8Array
Copies one column into a longer typed array of the same kind. A Float32Array cannot grow in place, so the old numbers are copied across.
@paramsrc@paramnext - New length. Must be greater than src.length.@returns
copyField
(src: any
@paramsrc
src
, next: number
- New length. Must be greater than src.length.
@paramnext - New length. Must be greater than src.length.
next
) {
const const dest: anydest = new src: any
@paramsrc
src
.constructor(next: number
- New length. Must be greater than src.length.
@paramnext - New length. Must be greater than src.length.
next
);
const dest: anydest.set(src: any
@paramsrc
src
);
return const dest: anydest; } /** * Bouncing-sprite stress test. * * The lesson is the bunny loop: position and speed live in flat typed arrays * (one long row of numbers per property). The hop only writes into slots that * already exist. On software, a spawn that does not fit replaces those columns * with a longer copy first. * * @implements {IBTDemo} */ class class Demo
Bouncing-sprite stress test. The lesson is the bunny loop: position and speed live in flat typed arrays (one long row of numbers per property). The hop only writes into slots that already exist. On software, a spawn that does not fit replaces those columns with a longer copy first.
@implementsIBTDemo
Demo
{
/** @type {Palette | null} */ Demo.palette: Palette | null
@type{Palette | null}
palette
= null;
/** @type {ReturnType<typeof applyTheme> | null} */ Demo.theme: any
@type{ReturnType<typeof applyTheme> | null}
theme
= null;
/** @type {SpriteSheet[] | null} */ Demo.sheets: {} | null
@type{SpriteSheet[] | null}
sheets
= null;
// Source rectangle for the whole bunny picture. Created once, never replaced. Demo.srcRect: Rect2isrcRect = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(0, 0, const BUNNY_W: anyBUNNY_W, const BUNNY_H: anyBUNNY_H);
// Scratch points. drawSprite reads the numbers immediately, and pointerPosTo // writes into a vector we already own, so neither call needs a new one. Demo.drawPos: Vector2idrawPos = 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);
Demo.pointerScratch: Vector2ipointerScratch = 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);
// One column per property, MAX_WEBGPU_BUNNIES long, allocated here at construction. // Adding a bunny is "write the next free slot", not "make a new bunny". Demo.xs: anyxs = new Float32Array(const MAX_WEBGPU_BUNNIES: 16384MAX_WEBGPU_BUNNIES); Demo.ys: anyys = new Float32Array(const MAX_WEBGPU_BUNNIES: 16384MAX_WEBGPU_BUNNIES); Demo.vxs: anyvxs = new Float32Array(const MAX_WEBGPU_BUNNIES: 16384MAX_WEBGPU_BUNNIES); Demo.vys: anyvys = new Float32Array(const MAX_WEBGPU_BUNNIES: 16384MAX_WEBGPU_BUNNIES); Demo.variants: anyvariants = new Uint8Array(const MAX_WEBGPU_BUNNIES: 16384MAX_WEBGPU_BUNNIES); Demo.count: numbercount = 0; // Right and bottom walls of the bunnies' area. The screen size does not change, // so init() stores them once and the hop loop only reads them. Demo.arenaRight: numberarenaRight = 0; Demo.arenaFloor: numberarenaFloor = 0; Demo.splitSheets: booleansplitSheets = false; // True only on the WebGPU backend, which owns the 8333-quad buffer. Software // draws every submitted sprite, so the cap must not apply there. Demo.capsSprites: booleancapsSprites = false; // Set from the Add button during render(), consumed on the next update(). // Update runs before render, so a click cannot spawn in the same pass. Demo.addPressed: booleanaddPressed = false; Demo.spawnX: numberspawnX = 0; Demo.spawnY: numberspawnY = const ARENA_TOP: numberARENA_TOP; // Middle of the bunnies' area, worked out once in init() from the screen width. Demo.fountainX: numberfountainX = 0; Demo.drawn: numberdrawn = 0; Demo.dropped: numberdropped = 0; Demo.spriteBatches: numberspriteBatches = 0; /** * Show the engine overlay immediately so FPS and Draw Calls are on screen. * * @returns {Partial<HardwareSettings>} */ Demo.configure(): Partial<HardwareSettings>
Show the engine overlay immediately so FPS and Draw Calls are on screen.
@returns
configure
() {
return { // Logical pixels. Providing displaySize makes the drawing buffer match it, // so the playfield is 640 by 400 and the page scales that picture up. 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
(640, 400),
isOverlayVisibleAtStart: booleanisOverlayVisibleAtStart: true, // Space spawns bunnies, and the browser scrolls the host page on Space. // Opt in so that press stays in the demo. isCapturingKeyboardScroll: booleanisCapturingKeyboardScroll: true, // Adds the Prim / Spr row: the engine's own vertex use and overflow count. isOverlayRendererDiagnosticsBarEnabled: booleanisOverlayRendererDiagnosticsBarEnabled: true, // Scrolling update() and render() bars between the title and Present. // The default band is 22 px, which is what OVERLAY_BOTTOM counts on. isOverlayTimingChartEnabled: booleanisOverlayTimingChartEnabled: true,
overlayStyle: {
    textPaletteIndex: any;
    barPaletteIndex: any;
}
overlayStyle
: {
textPaletteIndex: anytextPaletteIndex: const UI_TEXT: anyUI_TEXT, barPaletteIndex: anybarPaletteIndex: const UI_PANEL: anyUI_PANEL, }, }; } /** * Builds the bunny picture, the recolor rows, and the sheet copies. * * @returns {Promise<boolean>} */ async Demo.init(): Promise<boolean>
Builds the bunny picture, the recolor rows, and the sheet copies.
@returns
init
() {
this.Demo.palette: Palette | null
@type{Palette | null}
palette
=
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;
    ... 109 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.paletteCreate: (size?: number) => Palette
Creates a standalone palette instance.
@since1.0.3@paramsize - Palette size. Defaults to 256 colors.@returnsNew mutable palette.
paletteCreate
(256);
function installVariantColors(palette: Palette): void
Writes every recolor into the palette. Row 0 lands on COLOR_BASE. Each later row starts COLOR_COUNT slots higher, which is exactly the paletteOffset the draw loop adds for that variant.
@parampalette
installVariantColors
(this.Demo.palette: Palette
@type{Palette | null}
palette
);
this.Demo.theme: any
@type{ReturnType<typeof applyTheme> | null}
theme
= import applyThemeapplyTheme(this.Demo.palette: Palette
@type{Palette | null}
palette
);
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;
    ... 109 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{Palette | null}
palette
);
// Read once. displaySize clones, and this demo does not resize the grid. const const display: Vector2idisplay =
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;
    ... 109 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.displaySize: Vector2i
Active logical render resolution in pixels. This is the game/simulation coordinate space configured by the demo, not the canvas element's CSS size. Each read returns a clone.
@since1.0.4@returnsConfigured logical size, or `Vector2i.zero()` before initialization.
displaySize
;
const const screenW: numberscreenW = const display: Vector2idisplay.Vector2i.x: number
Horizontal component (defaults to 0).
x
;
const const screenH: numberscreenH = const display: Vector2idisplay.Vector2i.y: number
Vertical component (defaults to 0).
y
;
// Sides and floor are the canvas. The ceiling is ARENA_TOP, under the chart. this.Demo.arenaRight: numberarenaRight = const screenW: numberscreenW - const BUNNY_W: anyBUNNY_W; this.Demo.arenaFloor: numberarenaFloor = const screenH: numberscreenH - const BUNNY_H: anyBUNNY_H; this.Demo.capsSprites: booleancapsSprites =
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;
    ... 109 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.activeBackend: Backend | null
Rendering backend that is currently active. `'webgpu'` or `'software'` after successful init; `null` before init or on failure. May differ from {@link BT.requestedBackend } when WebGPU was requested but unavailable (automatic software fallback). Use this getter for runtime behavior - for example, skipping post-process effects that only work under WebGPU: ```ts if (BT.activeBackend === 'webgpu') { for (const fx of BT.preset.crtPipBoy()) { BT.effectAdd(fx); } } ```
@since1.0.4@returns`'webgpu'` or `'software'` after successful init; `null` before init or on failure.
activeBackend
=== 'webgpu';
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;
    ... 109 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.randomSeed: (seed: number) => void
Reseeds the default engine PRNG so subsequent draws are reproducible. Calling this from the demo's `init()` overrides a `?seed=N` URL parameter, which `BT.init()` applies just before the demo's `init()`.
@since1.5.0@changed1.7.1 Documented precedence over the `?seed=N` URL parameter.@paramseed - Any finite number; only its lower 32 bits are used.@exampleBT.randomSeed(1234); const a = BT.random.next(); BT.randomSeed(1234); const b = BT.random.next(); // a === b
randomSeed
(const RANDOM_SEED: 553RANDOM_SEED);
// A failed upload throws out of init(). The engine reports that and does not // start the loop. this.Demo.sheets: {} | null
@type{SpriteSheet[] | null}
sheets
= await function loadBunnySheets(palette: Palette): Promise<SpriteSheet[]>
One picture, uploaded SHEET_COUNT times. Each SpriteSheet owns its own GPU texture, so switching sheets is a real batch break, not a shared upload.
@parampalette@returns
loadBunnySheets
(this.Demo.palette: Palette
@type{Palette | null}
palette
);
// The fountain sits in the middle of the canvas. `>> 1` halves a whole // number, like Math.floor(n / 2). Bunnies bounce off the canvas left edge. this.Demo.fountainX: numberfountainX = (const screenW: numberscreenW - const BUNNY_W: anyBUNNY_W) >> 1; this.Demo.applyUrlSwitches(): void
Reads the optional ?bunnies=N and ?split URL switches, so a timing run can start with a known crowd instead of a lot of clicking. Without ?bunnies the demo starts with one ordinary batch. A present value must be digits from 1 to the active field limit (5000, not 5.5 or 1e3), or init() stops.
applyUrlSwitches
();
return true; } 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.
update
() {
// Latches key presses and touches before we read them. import uiui.tick(); // The hop writes into slots that already exist. On software, a batch that // does not fit copies the columns into a longer list first. this.Demo.stepBunnies(): void
Moves every live bunny. Gravity, then bounce off the canvas sides, the overlay ceiling, and the floor. Nothing in this loop creates an object. That is the whole point.
stepBunnies
();
if (this.Demo.wantsSpawn(): boolean
True when the pointer, the Add button, gamepad A / Space, or KeyN wants another batch. Also chooses where that batch appears.
@returns
wantsSpawn
()) {
this.Demo.spawnBatch(size: number): void
Writes the next `size` bunnies into the free slots at the end of the arrays. Random velocities come from the seeded BT.random stream.
@paramsize - How many bunnies to add. Stops at the field limit.
spawnBatch
(const SPAWN_BATCH: 100SPAWN_BATCH);
} } 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.
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;
    ... 109 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
(this.Demo.theme: any
@type{ReturnType<typeof applyTheme> | null}
theme
.bg);
this.Demo.drawBunnies(): void
On WebGPU, submits at most BUNNY_DRAW_CAP bunnies and counts the rest as dropped, so the sprite buffer does not warn and the panel letters keep their reserve. Software has no vertex cap, so every live bunny is submitted and Dropped stays 0. render() runs only after init() stored the sheets.
drawBunnies
();
this.Demo.drawHud(): void
Panel: counts and the buttons. update() and render() stay on the engine overlay.
drawHud
();
} /** * Plain numbers for window.BT.testState() so a browser check can read the * counters without scraping the picture. * * @returns {object} Counts for the frame. WebGPU drops past the bunny draw cap. * Software draws every live bunny. */ Demo.testState(): object
Plain numbers for window.BT.testState() so a browser check can read the counters without scraping the picture.
@returnsCounts for the frame. WebGPU drops past the bunny draw cap. Software draws every live bunny.
testState
() {
return { count: numbercount: this.Demo.count: numbercount, drawn: numberdrawn: this.Demo.drawn: numberdrawn, dropped: numberdropped: this.Demo.dropped: numberdropped, batches: numberbatches: this.Demo.spriteBatches: numberspriteBatches, splitSheets: booleansplitSheets: this.Demo.splitSheets: booleansplitSheets, capsSprites: booleancapsSprites: this.Demo.capsSprites: booleancapsSprites, }; } /** * Reads the optional ?bunnies=N and ?split URL switches, so a timing run can * start with a known crowd instead of a lot of clicking. Without ?bunnies the * demo starts with one ordinary batch. A present value must be digits from 1 * to the active field limit (5000, not 5.5 or 1e3), or init() stops. */ Demo.applyUrlSwitches(): void
Reads the optional ?bunnies=N and ?split URL switches, so a timing run can start with a known crowd instead of a lot of clicking. Without ?bunnies the demo starts with one ordinary batch. A present value must be digits from 1 to the active field limit (5000, not 5.5 or 1e3), or init() stops.
applyUrlSwitches
() {
const const params: anyparams = new URLSearchParams(window.location.search); this.Demo.splitSheets: booleansplitSheets = const params: anyparams.has(const PARAM_SPLIT: "split"PARAM_SPLIT); this.Demo.spawnX: numberspawnX = this.Demo.fountainX: numberfountainX; this.Demo.spawnY: numberspawnY = const ARENA_TOP: numberARENA_TOP; this.Demo.spawnBatch(size: number): void
Writes the next `size` bunnies into the free slots at the end of the arrays. Random velocities come from the seeded BT.random stream.
@paramsize - How many bunnies to add. Stops at the field limit.
spawnBatch
(function bunnyCountFromParams(params: any, fieldMax: any): anybunnyCountFromParams(const params: anyparams, this.Demo.fieldLimit(): number
How many bunnies the lists may hold. WebGPU stays at the starting length. Software may grow up to MAX_SOFTWARE_BUNNIES.
@returns
fieldLimit
()));
} /** * Empties the field and restarts the random stream so the next batch matches * the one from a fresh load. */ Demo.reset(): void
Empties the field and restarts the random stream so the next batch matches the one from a fresh load.
reset
() {
this.Demo.count: numbercount = 0; this.Demo.drawn: numberdrawn = 0; this.Demo.dropped: numberdropped = 0; this.Demo.spriteBatches: numberspriteBatches = 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;
    ... 109 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.randomSeed: (seed: number) => void
Reseeds the default engine PRNG so subsequent draws are reproducible. Calling this from the demo's `init()` overrides a `?seed=N` URL parameter, which `BT.init()` applies just before the demo's `init()`.
@since1.5.0@changed1.7.1 Documented precedence over the `?seed=N` URL parameter.@paramseed - Any finite number; only its lower 32 bits are used.@exampleBT.randomSeed(1234); const a = BT.random.next(); BT.randomSeed(1234); const b = BT.random.next(); // a === b
randomSeed
(const RANDOM_SEED: 553RANDOM_SEED);
} /** * Moves every live bunny. Gravity, then bounce off the canvas sides, the * overlay ceiling, and the floor. Nothing in this loop creates an object. * That is the whole point. */ Demo.stepBunnies(): void
Moves every live bunny. Gravity, then bounce off the canvas sides, the overlay ceiling, and the floor. Nothing in this loop creates an object. That is the whole point.
stepBunnies
() {
const const xs: anyxs = this.Demo.xs: anyxs; const const ys: anyys = this.Demo.ys: anyys; const const vxs: anyvxs = this.Demo.vxs: anyvxs; const const vys: anyvys = this.Demo.vys: anyvys; const const right: numberright = this.Demo.arenaRight: numberarenaRight; const const floor: numberfloor = this.Demo.arenaFloor: numberarenaFloor; const const n: numbern = this.Demo.count: numbercount; const const random: Randomrandom =
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;
    ... 109 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, 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.
@since1.5.0@changed1.7.1 `?seed=N` URL parameter seeds the shared generator before the demo's `init()`.@returnsThe shared {@link Random} instance.@exampleBT.randomSeed(42); BT.random.int(150, 420); BT.random.pick(['a', 'b', 'c']);
random
;
for (let let i: numberi = 0; let i: numberi < const n: numbern; let i: numberi++) { let let vx: anyvx = const vxs: anyvxs[let i: numberi]; let let vy: anyvy = const vys: anyvys[let i: numberi] + const GRAVITY: 0.4GRAVITY; let let x: anyx = const xs: anyxs[let i: numberi] + let vx: anyvx; let let y: anyy = const ys: anyys[let i: numberi] + let vy: anyvy; // Past the side walls: pin to the wall and reverse sideways speed. // The left wall is the canvas edge. if (let x: anyx < 0) { let x: anyx = 0; let vx: anyvx = -let vx: anyvx; } else if (let x: anyx > const right: numberright) { let x: anyx = const right: numberright; let vx: anyvx = -let vx: anyvx; } // Past the ceiling: pin and head back down (positive Y). // The ceiling is the overlay, not the canvas top, so bunnies stay // under the timing chart. if (let y: anyy < const ARENA_TOP: numberARENA_TOP) { let y: anyy = const ARENA_TOP: numberARENA_TOP; let vy: anyvy = Math.abs(let vy: anyvy); } else if (let y: anyy > const floor: numberfloor) { let y: anyy = const floor: numberfloor; // Floor bounce flips a downward speed into a smaller upward one. let vy: anyvy = -Math.abs(let vy: anyvy) * const RESTITUTION: 0.75RESTITUTION; if (let vy: anyvy > const HOP_LIMIT: -1.5HOP_LIMIT && const random: Randomrandom.Random.next(): number
Returns the next pseudo-random float in [0, 1).
@returnsNext value in the deterministic sequence.@since1.5.0
next
() > 0.5) {
let vy: anyvy -= const random: Randomrandom.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
(const HOP_MIN: 2HOP_MIN, const HOP_MAX: 6HOP_MAX);
} } const xs: anyxs[let i: numberi] = let x: anyx; const ys: anyys[let i: numberi] = let y: anyy; const vxs: anyvxs[let i: numberi] = let vx: anyvx; const vys: anyvys[let i: numberi] = let vy: anyvy; } } /** * True when the pointer, the Add button, gamepad A / Space, or KeyN wants * another batch. Also chooses where that batch appears. * * @returns {boolean} */ Demo.wantsSpawn(): boolean
True when the pointer, the Add button, gamepad A / Space, or KeyN wants another batch. Also chooses where that batch appears.
@returns
wantsSpawn
() {
const const fromPointer: booleanfromPointer = this.Demo.pointerWantsSpawn(): boolean
Hold on empty screen (not on a button) with the mouse or a finger.
@returns
pointerWantsSpawn
();
const const fromButton: booleanfromButton = this.Demo.addPressed: booleanaddPressed; this.Demo.addPressed: booleanaddPressed = false; // Face button A is also Space. KeyN is the on-screen Add button's key. // Both are held state, so they keep spawning while held. const const fromHeld: booleanfromHeld =
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;
    ... 109 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;
    ... 109 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.type BTN_A: number
A button bit flag.
@since0.1.0
BTN_A
, 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;
    ... 109 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.isKeyDown: (key: string) => boolean
Checks whether a keyboard key is currently held. Uses `KeyboardEvent.code` (for example `"KeyW"`, `"Space"`, `"ArrowUp"`).
@since1.1.1@paramkey - DOM keyboard code string.@returns`true` while the key remains pressed.
isKeyDown
(const KEY_ADD: "KeyN"KEY_ADD);
if (!const fromPointer: booleanfromPointer && !const fromButton: booleanfromButton && !const fromHeld: booleanfromHeld) { return false; } if (this.Demo.count: numbercount >= this.Demo.fieldLimit(): number
How many bunnies the lists may hold. WebGPU stays at the starting length. Software may grow up to MAX_SOFTWARE_BUNNIES.
@returns
fieldLimit
()) {
return false; } // Pointer spawn already stored a point. Everything else uses the fountain. if (!const fromPointer: booleanfromPointer) { this.Demo.spawnX: numberspawnX = this.Demo.fountainX: numberfountainX; this.Demo.spawnY: numberspawnY = const ARENA_TOP: numberARENA_TOP; } return true; } /** * Hold on empty screen (not on a button) with the mouse or a finger. * * @returns {boolean} */ Demo.pointerWantsSpawn(): boolean
Hold on empty screen (not on a button) with the mouse or a finger.
@returns
pointerWantsSpawn
() {
for (let let slot: numberslot = 0; let slot: numberslot < const POINTER_SLOT_COUNT: 4POINTER_SLOT_COUNT; let slot: numberslot++) { 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;
    ... 109 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
(let slot: numberslot) || !
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;
    ... 109 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;
    ... 109 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
, let slot: numberslot)) {
continue; } // pointerPos() would clone a vector every call. pointerPosTo writes // into pointerScratch instead.
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;
    ... 109 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.pointerPosTo: (out: Vector2i, pointerIndex?: number) => Vector2i
Writes the position of the pointer in the given slot, in display coordinates, into `out`. Zero-allocation counterpart to {@link pointerPos } . Slot 0 is the mouse; slots 1 through 3 are touch / pen contacts assigned in arrival order. Writes `(0, 0)` into `out` when the engine has not been initialized, the slot index is out of `[0, 3]`, or the slot has no live pointer.
@since1.7.0@paramout - Vector2i to write the pointer position into.@parampointerIndex - Pointer slot (defaults to 0 = mouse).@returnsThe `out` vector, for chaining.
pointerPosTo
(this.Demo.pointerScratch: Vector2ipointerScratch, let slot: numberslot);
if (import uiui.overWidget(this.Demo.pointerScratch: Vector2ipointerScratch.Vector2i.x: number
Horizontal component (defaults to 0).
x
, this.Demo.pointerScratch: Vector2ipointerScratch.Vector2i.y: number
Vertical component (defaults to 0).
y
)) {
continue; } this.Demo.spawnX: numberspawnX = this.Demo.pointerScratch: Vector2ipointerScratch.Vector2i.x: number
Horizontal component (defaults to 0).
x
;
this.Demo.spawnY: numberspawnY = this.Demo.pointerScratch: Vector2ipointerScratch.Vector2i.y: number
Vertical component (defaults to 0).
y
;
return true; } return false; } /** * Keeps the spawn point on the canvas, so a click in a corner still shows a bunny. */ Demo.clampSpawn(): void
Keeps the spawn point on the canvas, so a click in a corner still shows a bunny.
clampSpawn
() {
// Math.max keeps the higher number, Math.min the lower, so a click past // the canvas edge is pushed back inside. The left edge is 0. The top // edge is the overlay, so a click on the chart spawns just under it. this.Demo.spawnX: numberspawnX = Math.min(this.Demo.arenaRight: numberarenaRight, Math.max(0, this.Demo.spawnX: numberspawnX)); this.Demo.spawnY: numberspawnY = Math.min(this.Demo.arenaFloor: numberarenaFloor, Math.max(const ARENA_TOP: numberARENA_TOP, this.Demo.spawnY: numberspawnY)); } /** * How many bunnies the lists may hold. WebGPU stays at the starting length. * Software may grow up to MAX_SOFTWARE_BUNNIES. * * @returns {number} */ Demo.fieldLimit(): number
How many bunnies the lists may hold. WebGPU stays at the starting length. Software may grow up to MAX_SOFTWARE_BUNNIES.
@returns
fieldLimit
() {
return this.Demo.capsSprites: booleancapsSprites ? const MAX_WEBGPU_BUNNIES: 16384MAX_WEBGPU_BUNNIES : const MAX_SOFTWARE_BUNNIES: 262144MAX_SOFTWARE_BUNNIES; } /** * Replaces each column with a longer copy. Doubles until `needed` fits, and * never passes MAX_SOFTWARE_BUNNIES. Called from spawn, not from the hop loop. * * @param {number} needed - Slots the next batch has to reach. */ Demo.growField(needed: number): void
Replaces each column with a longer copy. Doubles until `needed` fits, and never passes MAX_SOFTWARE_BUNNIES. Called from spawn, not from the hop loop.
@paramneeded - Slots the next batch has to reach.
growField
(needed: number
- Slots the next batch has to reach.
@paramneeded - Slots the next batch has to reach.
needed
) {
let let next: anynext = this.Demo.xs: anyxs.length; while (let next: anynext < needed: number
- Slots the next batch has to reach.
@paramneeded - Slots the next batch has to reach.
needed
) {
let next: anynext *= 2; } if (let next: anynext > const MAX_SOFTWARE_BUNNIES: 262144MAX_SOFTWARE_BUNNIES) { let next: anynext = const MAX_SOFTWARE_BUNNIES: 262144MAX_SOFTWARE_BUNNIES; } if (let next: anynext <= this.Demo.xs: anyxs.length) { return; } this.Demo.xs: anyxs = function copyField(src: Float32Array | Uint8Array, next: number): Float32Array | Uint8Array
Copies one column into a longer typed array of the same kind. A Float32Array cannot grow in place, so the old numbers are copied across.
@paramsrc@paramnext - New length. Must be greater than src.length.@returns
copyField
(this.Demo.xs: anyxs, let next: anynext);
this.Demo.ys: anyys = function copyField(src: Float32Array | Uint8Array, next: number): Float32Array | Uint8Array
Copies one column into a longer typed array of the same kind. A Float32Array cannot grow in place, so the old numbers are copied across.
@paramsrc@paramnext - New length. Must be greater than src.length.@returns
copyField
(this.Demo.ys: anyys, let next: anynext);
this.Demo.vxs: anyvxs = function copyField(src: Float32Array | Uint8Array, next: number): Float32Array | Uint8Array
Copies one column into a longer typed array of the same kind. A Float32Array cannot grow in place, so the old numbers are copied across.
@paramsrc@paramnext - New length. Must be greater than src.length.@returns
copyField
(this.Demo.vxs: anyvxs, let next: anynext);
this.Demo.vys: anyvys = function copyField(src: Float32Array | Uint8Array, next: number): Float32Array | Uint8Array
Copies one column into a longer typed array of the same kind. A Float32Array cannot grow in place, so the old numbers are copied across.
@paramsrc@paramnext - New length. Must be greater than src.length.@returns
copyField
(this.Demo.vys: anyvys, let next: anynext);
this.Demo.variants: anyvariants = function copyField(src: Float32Array | Uint8Array, next: number): Float32Array | Uint8Array
Copies one column into a longer typed array of the same kind. A Float32Array cannot grow in place, so the old numbers are copied across.
@paramsrc@paramnext - New length. Must be greater than src.length.@returns
copyField
(this.Demo.variants: anyvariants, let next: anynext);
} /** * Writes the next `size` bunnies into the free slots at the end of the arrays. * Random velocities come from the seeded BT.random stream. * * @param {number} size - How many bunnies to add. Stops at the field limit. */ Demo.spawnBatch(size: number): void
Writes the next `size` bunnies into the free slots at the end of the arrays. Random velocities come from the seeded BT.random stream.
@paramsize - How many bunnies to add. Stops at the field limit.
spawnBatch
(size: number
- How many bunnies to add. Stops at the field limit.
@paramsize - How many bunnies to add. Stops at the field limit.
size
) {
this.Demo.clampSpawn(): void
Keeps the spawn point on the canvas, so a click in a corner still shows a bunny.
clampSpawn
();
const const end: anyend = Math.min(this.Demo.fieldLimit(): number
How many bunnies the lists may hold. WebGPU stays at the starting length. Software may grow up to MAX_SOFTWARE_BUNNIES.
@returns
fieldLimit
(), this.Demo.count: numbercount + size: number
- How many bunnies to add. Stops at the field limit.
@paramsize - How many bunnies to add. Stops at the field limit.
size
);
if (const end: anyend > this.Demo.xs: anyxs.length) { this.Demo.growField(needed: number): void
Replaces each column with a longer copy. Doubles until `needed` fits, and never passes MAX_SOFTWARE_BUNNIES. Called from spawn, not from the hop loop.
@paramneeded - Slots the next batch has to reach.
growField
(const end: anyend);
} const const random: Randomrandom =
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;
    ... 109 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, 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.
@since1.5.0@changed1.7.1 `?seed=N` URL parameter seeds the shared generator before the demo's `init()`.@returnsThe shared {@link Random} instance.@exampleBT.randomSeed(42); BT.random.int(150, 420); BT.random.pick(['a', 'b', 'c']);
random
;
const const xs: anyxs = this.Demo.xs: anyxs; const const ys: anyys = this.Demo.ys: anyys; const const vxs: anyvxs = this.Demo.vxs: anyvxs; const const vys: anyvys = this.Demo.vys: anyvys; const const variants: anyvariants = this.Demo.variants: anyvariants; const const x: numberx = this.Demo.spawnX: numberspawnX; const const y: numbery = this.Demo.spawnY: numberspawnY; let let n: numbern = this.Demo.count: numbercount; while (let n: numbern < const end: anyend) { const xs: anyxs[let n: numbern] = const x: numberx; const ys: anyys[let n: numbern] = const y: numbery; const vxs: anyvxs[let n: numbern] = const random: Randomrandom.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
(const SPAWN_VX_MIN: -4SPAWN_VX_MIN, const SPAWN_VX_MAX: 4SPAWN_VX_MAX);
const vys: anyvys[let n: numbern] = const random: Randomrandom.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
(const SPAWN_VY_MIN: -8SPAWN_VY_MIN, const SPAWN_VY_MAX: -2SPAWN_VY_MAX);
const variants: anyvariants[let n: numbern] = const random: Randomrandom.Random.int(minOrMaxExclusive: number, maxExclusive?: number): number
Returns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).
@paramminOrMaxExclusive - When alone, exclusive upper bound from 0; otherwise inclusive min.@parammaxExclusive - Exclusive upper bound when two arguments are passed.@returnsWhole number in the half-open range.@since1.5.0
int
(const VARIANT_COUNT: anyVARIANT_COUNT);
let n: numbern += 1; } this.Demo.count: numbercount = let n: numbern; } /** * On WebGPU, submits at most BUNNY_DRAW_CAP bunnies and counts the rest as * dropped, so the sprite buffer does not warn and the panel letters keep * their reserve. Software has no vertex cap, so every live bunny is submitted * and Dropped stays 0. render() runs only after init() stored the sheets. */ Demo.drawBunnies(): void
On WebGPU, submits at most BUNNY_DRAW_CAP bunnies and counts the rest as dropped, so the sprite buffer does not warn and the panel letters keep their reserve. Software has no vertex cap, so every live bunny is submitted and Dropped stays 0. render() runs only after init() stored the sheets.
drawBunnies
() {
const const limit: anylimit = this.Demo.capsSprites: booleancapsSprites ? Math.min(this.Demo.count: numbercount, const BUNNY_DRAW_CAP: numberBUNNY_DRAW_CAP) : this.Demo.count: numbercount; this.Demo.drawn: numberdrawn = const limit: anylimit; this.Demo.dropped: numberdropped = this.Demo.count: numbercount - const limit: anylimit; if (const limit: anylimit === 0) { this.Demo.spriteBatches: numberspriteBatches = 0; return; } const const sheets: {} | nullsheets = this.Demo.sheets: {} | null
@type{SpriteSheet[] | null}
sheets
;
const const xs: anyxs = this.Demo.xs: anyxs; const const ys: anyys = this.Demo.ys: anyys; const const variants: anyvariants = this.Demo.variants: anyvariants; const const pos: Vector2ipos = this.Demo.drawPos: Vector2idrawPos; const const src: Rect2isrc = this.Demo.srcRect: Rect2isrcRect; // One sheet: span is 1, and the remainder of any count divided by 1 is 0, // so every bunny stays on texture 0 (one batch). Split mode uses all four // copies in turn (0, 1, 2, 3, 0, ...). Each change of sheet is another batch. const const sheetSpan: 1 | 4sheetSpan = this.Demo.splitSheets: booleansplitSheets ? const SHEET_COUNT: 4SHEET_COUNT : 1; let let batches: numberbatches = 0; let let lastSheet: numberlastSheet = -1; for (let let i: numberi = 0; let i: numberi < const limit: anylimit; let i: numberi++) { const const sheetIndex: numbersheetIndex = let i: numberi % const sheetSpan: 1 | 4sheetSpan; if (const sheetIndex: numbersheetIndex !== let lastSheet: numberlastSheet) { let batches: numberbatches += 1; let lastSheet: numberlastSheet = const sheetIndex: numbersheetIndex; } // `| 0` drops the fraction on a positive number, the way Math.floor // would, without a function call. Sprites want whole pixels. const pos: Vector2ipos.Vector2i.x: number
Horizontal component (defaults to 0).
x
= const xs: anyxs[let i: numberi] | 0;
const pos: Vector2ipos.Vector2i.y: number
Vertical component (defaults to 0).
y
= const ys: anyys[let i: numberi] | 0;
// paletteOffset slides the stored 1-4 colors onto this bunny's row.
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;
    ... 109 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => void
Draws a sprite region from an indexed sprite sheet. Sprite draws are batched internally. Grouping draws from the same {@link SpriteSheet } minimizes batch flushes and reduces GPU state changes. The sprite sheet must have been converted to palette indices via `spriteSheet.indexize(palette)` before the first draw call. Prefer `SpriteSheet.loadIndexed(...)` for one-call setup. **Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1. Index 0 is always transparent and is discarded by the fragment shader. The final palette lookup is `storedIndex + paletteOffset`, so: - `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`. `palette[0]` is never reachable because stored indices start at 1. - `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`, and so on. Use this for palette-swap effects such as team colors or damage flashes. **Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's robust buffer access returns 0 for every component; because the fragment shader forces alpha to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also produces out-of-bounds black pixels.
@since0.1.0@paramspriteSheet - Indexed sprite sheet.@paramsrcRect - Source rectangle within the sprite sheet, in pixels.@paramdestPos - Destination top-left position in display coordinates.@parampaletteOffset - Shift added to every stored pixel index before palette lookup (default 0).@exampleBT.drawSprite(sheet, new Rect2i(0, 0, 16, 16), new Vector2i(10, 10)); BT.drawSprite(sheet, new Rect2i(0, 0, 16, 16), new Vector2i(10, 10), 16); // blue team
drawSprite
(const sheets: {} | nullsheets[const sheetIndex: numbersheetIndex], const src: Rect2isrc, const pos: Vector2ipos, const variants: anyvariants[let i: numberi] * const COLOR_COUNT: 4COLOR_COUNT);
} this.Demo.spriteBatches: numberspriteBatches = let batches: numberbatches; } /** * Panel: counts and the buttons. update() and render() stay on the engine overlay. */ Demo.drawHud(): void
Panel: counts and the buttons. update() and render() stay on the engine overlay.
drawHud
() {
// Fixed width and position. Bunnies use the full canvas, including behind this panel. import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { y: numbery: const PANEL_Y: numberPANEL_Y, width: numberwidth: const PANEL_W: 118PANEL_W, margin: numbermargin: const PANEL_MARGIN: 4PANEL_MARGIN, kvCols: numberkvCols: 8 }); import uiui.panel('Bunnymark'); import uiui.kv('Bunnies', this.Demo.count: numbercount); import uiui.kv('Drawn', this.Demo.drawn: numberdrawn); import uiui.kv('Dropped', this.Demo.dropped: numberdropped); import uiui.kv('Batches', this.Demo.spriteBatches: numberspriteBatches); if (this.Demo.capsSprites: booleancapsSprites) { import uiui.kv('Cap', const SPRITE_QUAD_CAP: anySPRITE_QUAD_CAP); } import uiui.label(this.Demo.hudNote(): string
One short line under the numbers. Warm when bunnies are being skipped.
@returns
hudNote
(), { color: stringcolor: this.Demo.dropped: numberdropped > 0 ? 'warm' : 'dim' });
// The Batches row above already says which mode is on, so the checkbox needs no // extra label. A small gap keeps its box clear of the text and the button. import uiui.spacer(2); this.Demo.splitSheets: booleansplitSheets = import uiui.checkbox('Split sheets (S)', this.Demo.splitSheets: booleansplitSheets, { key: stringkey: 'KeyS' }); import uiui.spacer(4); if (import uiui.button('Add 100 (N)', { key: stringkey: const KEY_ADD: "KeyN"KEY_ADD })) { this.Demo.addPressed: booleanaddPressed = true; } if (import uiui.button('Reset (R)', { key: stringkey: 'KeyR' })) { this.Demo.reset(): void
Empties the field and restarts the random stream so the next batch matches the one from a fresh load.
reset
();
} import uiui.end(); } /** * One short line under the numbers. Warm when bunnies are being skipped. * * @returns {string} */ Demo.hudNote(): string
One short line under the numbers. Warm when bunnies are being skipped.
@returns
hudNote
() {
if (this.Demo.count: numbercount >= this.Demo.fieldLimit(): number
How many bunnies the lists may hold. WebGPU stays at the starting length. Software may grow up to MAX_SOFTWARE_BUNNIES.
@returns
fieldLimit
()) {
return 'Field is full'; } if (!this.Demo.capsSprites: booleancapsSprites) { return 'Software: no cap'; } if (this.Demo.dropped: numberdropped > 0) { return 'Over cap: skipped'; } return 'Hold, Space or A'; } } 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
Bouncing-sprite stress test. The lesson is the bunny loop: position and speed live in flat typed arrays (one long row of numbers per property). The hop only writes into slots that already exist. On software, a spawn that does not fit replaces those columns with a longer copy first.
@implementsIBTDemo
Demo
);