// 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.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 Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32, class Rect2iInteger 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.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.SpriteSheet, 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';
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): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(42, 26, 18), function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(232, 192, 144), function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(240, 160, 176), function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(26, 18, 12)],
[function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(90, 90, 96), function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(244, 244, 248), function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(245, 190, 200), function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(40, 40, 48)],
[function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(40, 40, 44), function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(140, 140, 148), function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(196, 140, 156), function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(20, 20, 24)],
[function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(12, 12, 16), function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(48, 44, 52), function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(220, 120, 150), function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(230, 230, 236)],
[function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(90, 42, 16), function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(230, 140, 48), function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(245, 196, 170), function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(40, 20, 12)],
[function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(24, 40, 64), function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(150, 186, 214), function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(214, 230, 242), function rgb(r: number, g: number, b: number): Color32One opaque color. Alpha 255 so indexize() can match it exactly.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): Color32One opaque color. Alpha 255 so indexize() can match it exactly.rgb(r: numberr, g: numberg, b: numberb) {
return new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(r: numberr, g: numberg, b: numberb, 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(): voidChecks 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[]): OffscreenCanvasPaints 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.buildBunnyCanvas(colors: {}- The four base colors, outline through eye.colors) {
function assertBunnyGrid(): voidChecks 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.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): voidWrites 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.installVariantColors(palette: Palettepalette) {
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: Palettepalette.Palette.fillBlock(start: number, source: readonly Color32[], transform: (color: Color32, index: number) => Color32): numberWrites 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.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.loadBunnySheets(palette: Palettepalette) {
const const canvas: OffscreenCanvascanvas = function buildBunnyCanvas(colors: Color32[]): OffscreenCanvasPaints 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.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): SpriteSheetCreates a sprite sheet from a loaded image.
Use the static load() method for easier loading from URL.SpriteSheet(const image: anyimage);
const sheet: SpriteSheetsheet.SpriteSheet.indexize(palette: Palette): voidConverts 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.indexize(palette: Palettepalette);
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 | Uint8ArrayCopies one column into a longer typed array of the same kind.
A Float32Array cannot grow in place, so the old numbers are copied across.copyField(src: anysrc, next: number- New length. Must be greater than src.length.next) {
const const dest: anydest = new src: anysrc.constructor(next: number- New length. Must be greater than src.length.next);
const dest: anydest.set(src: anysrc);
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 DemoBouncing-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.Demo {
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = null;
/** @type {ReturnType<typeof applyTheme> | null} */
Demo.theme: anytheme = null;
/** @type {SpriteSheet[] | null} */
Demo.sheets: {} | nullsheets = 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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(0, 0);
Demo.pointerScratch: Vector2ipointerScratch = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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.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): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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.init() {
this.Demo.palette: Palette | nullpalette = 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) => PaletteCreates a standalone palette instance.paletteCreate(256);
function installVariantColors(palette: Palette): voidWrites 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.installVariantColors(this.Demo.palette: Palettepalette);
this.Demo.theme: anytheme = import applyThemeapplyTheme(this.Demo.palette: Palettepalette);
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) => 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);
// 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: Vector2iActive 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.displaySize;
const const screenW: numberscreenW = const display: Vector2idisplay.Vector2i.x: numberHorizontal component (defaults to 0).x;
const const screenH: numberscreenH = const display: Vector2idisplay.Vector2i.y: numberVertical 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 | nullRendering 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);
}
}
```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) => voidReseeds 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()`.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: {} | nullsheets = 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.loadBunnySheets(this.Demo.palette: Palettepalette);
// 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(): voidReads 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(): voidCalled 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(): voidMoves 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(): booleanTrue when the pointer, the Add button, gamepad A / Space, or KeyN wants
another batch. Also chooses where that batch appears.wantsSpawn()) {
this.Demo.spawnBatch(size: number): voidWrites the next `size` bunnies into the free slots at the end of the arrays.
Random velocities come from the seeded BT.random stream.spawnBatch(const SPAWN_BATCH: 100SPAWN_BATCH);
}
}
Demo.render(): voidCalled 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) => 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(this.Demo.theme: anytheme.bg);
this.Demo.drawBunnies(): voidOn 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(): voidPanel: 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(): objectPlain numbers for window.BT.testState() so a browser check can read the
counters without scraping the picture.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(): voidReads 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): voidWrites the next `size` bunnies into the free slots at the end of the arrays.
Random velocities come from the seeded BT.random stream.spawnBatch(function bunnyCountFromParams(params: any, fieldMax: any): anybunnyCountFromParams(const params: anyparams, this.Demo.fieldLimit(): numberHow many bunnies the lists may hold. WebGPU stays at the starting length.
Software may grow up to MAX_SOFTWARE_BUNNIES.fieldLimit()));
}
/**
* Empties the field and restarts the random stream so the next batch matches
* the one from a fresh load.
*/
Demo.reset(): voidEmpties 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) => voidReseeds 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()`.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(): voidMoves 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: 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;
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(): numberReturns the next pseudo-random float in [0, 1).next() > 0.5) {
let vy: anyvy -= const random: Randomrandom.Random.float(min: number, max: number): numberReturns the next pseudo-random float in [min, max).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(): booleanTrue when the pointer, the Add button, gamepad A / Space, or KeyN wants
another batch. Also chooses where that batch appears.wantsSpawn() {
const const fromPointer: booleanfromPointer = this.Demo.pointerWantsSpawn(): booleanHold on empty screen (not on a button) with the mouse or a finger.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) => 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.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: numberA button bit flag.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) => booleanChecks whether a keyboard key is currently held.
Uses `KeyboardEvent.code` (for example `"KeyW"`, `"Space"`, `"ArrowUp"`).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(): numberHow many bunnies the lists may hold. WebGPU stays at the starting length.
Software may grow up to MAX_SOFTWARE_BUNNIES.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(): booleanHold on empty screen (not on a button) with the mouse or a finger.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) => 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(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) => 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.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: numberPrimary pointer button code.
Maps to mouse left for slot 0; touch contact for slots 1-3.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) => Vector2iWrites 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.pointerPosTo(this.Demo.pointerScratch: Vector2ipointerScratch, let slot: numberslot);
if (import uiui.overWidget(this.Demo.pointerScratch: Vector2ipointerScratch.Vector2i.x: numberHorizontal component (defaults to 0).x, this.Demo.pointerScratch: Vector2ipointerScratch.Vector2i.y: numberVertical component (defaults to 0).y)) {
continue;
}
this.Demo.spawnX: numberspawnX = this.Demo.pointerScratch: Vector2ipointerScratch.Vector2i.x: numberHorizontal component (defaults to 0).x;
this.Demo.spawnY: numberspawnY = this.Demo.pointerScratch: Vector2ipointerScratch.Vector2i.y: numberVertical 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(): voidKeeps 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(): numberHow many bunnies the lists may hold. WebGPU stays at the starting length.
Software may grow up to MAX_SOFTWARE_BUNNIES.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): voidReplaces each column with a longer copy. Doubles until `needed` fits, and
never passes MAX_SOFTWARE_BUNNIES. Called from spawn, not from the hop loop.growField(needed: number- 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.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 | Uint8ArrayCopies one column into a longer typed array of the same kind.
A Float32Array cannot grow in place, so the old numbers are copied across.copyField(this.Demo.xs: anyxs, let next: anynext);
this.Demo.ys: anyys = function copyField(src: Float32Array | Uint8Array, next: number): Float32Array | Uint8ArrayCopies one column into a longer typed array of the same kind.
A Float32Array cannot grow in place, so the old numbers are copied across.copyField(this.Demo.ys: anyys, let next: anynext);
this.Demo.vxs: anyvxs = function copyField(src: Float32Array | Uint8Array, next: number): Float32Array | Uint8ArrayCopies one column into a longer typed array of the same kind.
A Float32Array cannot grow in place, so the old numbers are copied across.copyField(this.Demo.vxs: anyvxs, let next: anynext);
this.Demo.vys: anyvys = function copyField(src: Float32Array | Uint8Array, next: number): Float32Array | Uint8ArrayCopies one column into a longer typed array of the same kind.
A Float32Array cannot grow in place, so the old numbers are copied across.copyField(this.Demo.vys: anyvys, let next: anynext);
this.Demo.variants: anyvariants = function copyField(src: Float32Array | Uint8Array, next: number): Float32Array | Uint8ArrayCopies one column into a longer typed array of the same kind.
A Float32Array cannot grow in place, so the old numbers are copied across.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): voidWrites the next `size` bunnies into the free slots at the end of the arrays.
Random velocities come from the seeded BT.random stream.spawnBatch(size: number- How many bunnies to add. Stops at the field limit.size) {
this.Demo.clampSpawn(): voidKeeps 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(): numberHow many bunnies the lists may hold. WebGPU stays at the starting length.
Software may grow up to MAX_SOFTWARE_BUNNIES.fieldLimit(), this.Demo.count: numbercount + size: number- 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): voidReplaces each column with a longer copy. Doubles until `needed` fits, and
never passes MAX_SOFTWARE_BUNNIES. Called from spawn, not from the hop loop.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: 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;
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): numberReturns the next pseudo-random float in [min, max).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): numberReturns the next pseudo-random float in [min, max).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): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).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(): voidOn 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: {} | nullsheets;
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: numberHorizontal component (defaults to 0).x = const xs: anyxs[let i: numberi] | 0;
const pos: Vector2ipos.Vector2i.y: numberVertical 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) => voidDraws 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.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(): voidPanel: 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(): stringOne short line under the numbers. Warm when bunnies are being skipped.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(): voidEmpties 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(): stringOne short line under the numbers. Warm when bunnies are being skipped.hudNote() {
if (this.Demo.count: numbercount >= this.Demo.fieldLimit(): numberHow many bunnies the lists may hold. WebGPU stays at the starting length.
Software may grow up to MAX_SOFTWARE_BUNNIES.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.bootstrap(class DemoBouncing-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.Demo);