// Sprites: how to draw images (sprites) on screen using BLIT386.
// @description Draw images from a programmatic sprite sheet, using source rectangles and palette offsets to vary them.
//
// Prerequisites: Basics (https://demos.blit386.dev/basics),
// Primitives (https://demos.blit386.dev/primitives),
// Colors (https://demos.blit386.dev/colors).
// Guide: https://blit386.dev/docs/api/rendering#sprites
//
// A "sprite" is a 2D image used in a game - like a character, a coin, or an enemy.
// In BLIT386, sprites are stored in a "sprite sheet": one big image that
// contains many small sprites arranged in a grid. You draw individual sprites by
// telling the engine which rectangular region (a Rect2i "source rect") to copy.
//
// This demo builds a six-shape sheet on an offscreen canvas, then shows:
//   1. BT.drawSprite() with different source regions (one shape per cell).
//   2. Palette offsets - shifting every pixel index to a different color block.
//   3. Opacity pulsing - rewriting palette alpha slots in update().
//
// Captions and the code panel are drawn with the shared UI kit (src/shared/ui.js), which
// installs its own twelve UI colors high in the palette (slots 240-251) via applyTheme().
//
// In a real project you would load PNGs from disk instead:
//   await SpriteSheet.load('/sprites/hero.png')
//   await SpriteSheet.loadIndexed('/sprites/hero.png', palette, startSlot)
//
// HOW PALETTE OFFSETS WORK FOR SPRITES:
//
// After calling sheet.indexize(palette), each pixel in the sprite is stored
// as a palette index number. When you draw the sprite:
//
//   BT.drawSprite(sheet, src, pos, 0)           - uses original colors
//   BT.drawSprite(sheet, src, pos, colorCount)  - shifts ALL pixel indices up by colorCount
//
// If the original colors are at palette[10..14], offset=5 shifts every pixel
// to use palette[15..19] - a completely different color theme!
// This is how retro games did "team colors" and environmental lighting.
//
// Palette Presets demo explores the palette system in depth:
// https://demos.blit386.dev/palette-presets

import { function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
,
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
, class Color32
Mutable 32-bit RGBA color value with 8-bit channels.
@since0.1.0
Color32
,
type Rect2i = Rect2i
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, import registerCanvasColorsregisterCanvasColors } from './shared/canvas-sprites.js'; import { import applyThemeapplyTheme, 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 */ /** @typedef {import('blit386').Rect2i} Rect2i */ // Where in the palette the sprite's original colors start. The sprite uses two colors // (fill + stroke), and the recolored theme blocks below stack up to about slot 19, so // everything above that stays free for the shared UI theme (slots 240-251). const const COLOR_BASE: 10COLOR_BASE = 10; // Each shape cell in the programmatic sheet is 20x20 pixels. const const SHAPE_CELL: 20SHAPE_CELL = 20; const const SHAPE_COLS: 3SHAPE_COLS = 3; const const SHAPE_ROWS: 2SHAPE_ROWS = 2; // One name per shape cell, in the same order drawShapeInCell() paints them. // This single list drives both the sheet builder (how many cells to draw) and the // captions under each shape in render(). The captions sit 50 pixels apart, so the // longer names are shortened ('Tri', 'Gem') to keep each label inside its column. const const SHAPE_NAMES: {}SHAPE_NAMES = ['Square', 'Circle', 'Tri', 'Star', 'Heart', 'Gem']; // Palette slots of the shared UI theme. applyTheme() in init() writes the twelve UI kit // colors into slots 240-251 (its default start slot). configure() runs BEFORE init(), so // the overlay styles below cannot read this.theme yet - these constants spell out where // each theme color will land once init() runs. const const UI_BG: 240UI_BG = 240; // 'ui_bg' - deep navy screen background const const UI_TEXT: 244UI_TEXT = 244; // 'ui_text' - off-white primary text const const UI_DIM: 245UI_DIM = 245; // 'ui_text_dim' - secondary gray text const const UI_BAR: 246UI_BAR = 246; // 'ui_bar' - bar chart color const const UI_INFO: 248UI_INFO = 248; // 'ui_info' - code blue // The exact two colors drawShapeInCell() paints with (see fill/stroke below). // The canvas smooths shape edges automatically (anti-aliasing), which blends these two // colors - and the transparent background - together one pixel at a time. Read back from // the canvas, that blending produces dozens of barely-different colors along every curve, // which would each want their own palette slot. Since our palette can only hold 256 colors // total, and this demo needs room for several recolored copies of the same shape, we snap // every blended edge pixel back to whichever of these two colors it is closer to. This // keeps the palette usage small and predictable no matter how smooth the edges look. // drawShapeInCell() reads these same two Color32 values (via toHex()) for its canvas // fillStyle/strokeStyle, so the paint colors and the quantization targets can never drift apart. const const FILL_COLOR: Color32FILL_COLOR = 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
(0x55, 0x99, 0xee);
const const STROKE_COLOR: Color32STROKE_COLOR = 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
(0xff, 0xff, 0xff);
/** * Finds whichever of FILL_COLOR/STROKE_COLOR is closer to a given pixel color. * "Closer" here means smaller distance in RGB space - treating red, green, and blue like * three coordinates and measuring the straight-line distance between two colors, the same * way you would measure distance between two points on a map. * * @param {number} r * @param {number} g * @param {number} b * @returns {Color32} */ function function nearestShapeColor(r: number, g: number, b: number): Color32
Finds whichever of FILL_COLOR/STROKE_COLOR is closer to a given pixel color. "Closer" here means smaller distance in RGB space - treating red, green, and blue like three coordinates and measuring the straight-line distance between two colors, the same way you would measure distance between two points on a map.
@paramr@paramg@paramb@returns
nearestShapeColor
(r: number
@paramr
r
, g: number
@paramg
g
, b: number
@paramb
b
) {
const const distanceToFill: numberdistanceToFill = (r: number
@paramr
r
- const FILL_COLOR: Color32FILL_COLOR.Color32.r: number
Red channel (0-255).
r
) ** 2 + (g: number
@paramg
g
- const FILL_COLOR: Color32FILL_COLOR.Color32.g: number
Green channel (0-255).
g
) ** 2 + (b: number
@paramb
b
- const FILL_COLOR: Color32FILL_COLOR.Color32.b: number
Blue channel (0-255).
b
) ** 2;
const const distanceToStroke: numberdistanceToStroke = (r: number
@paramr
r
- const STROKE_COLOR: Color32STROKE_COLOR.Color32.r: number
Red channel (0-255).
r
) ** 2 + (g: number
@paramg
g
- const STROKE_COLOR: Color32STROKE_COLOR.Color32.g: number
Green channel (0-255).
g
) ** 2 + (b: number
@paramb
b
- const STROKE_COLOR: Color32STROKE_COLOR.Color32.b: number
Blue channel (0-255).
b
) ** 2;
return const distanceToFill: numberdistanceToFill <= const distanceToStroke: numberdistanceToStroke ? const FILL_COLOR: Color32FILL_COLOR : const STROKE_COLOR: Color32STROKE_COLOR; } // Below this alpha, an anti-aliased edge pixel is mostly background - treat it as fully // transparent instead of a faint smudge of shape color. const const ALPHA_OPAQUE_THRESHOLD: 128ALPHA_OPAQUE_THRESHOLD = 128; /** * Rewrites every pixel of the canvas so it is either fully transparent or one of the exact * design colors (see FILL_COLOR/STROKE_COLOR above). sheet.indexize() later requires each * pixel to match a palette entry exactly, so the smooth, blended edges anti-aliasing draws * must be snapped to flat colors here - otherwise indexize() would reject every blended * edge pixel as "not in the palette". * * @param {OffscreenCanvasRenderingContext2D} ctx * @param {number} w * @param {number} h */ function function quantizeCanvasToShapeColors(ctx: OffscreenCanvasRenderingContext2D, w: number, h: number): void
Rewrites every pixel of the canvas so it is either fully transparent or one of the exact design colors (see FILL_COLOR/STROKE_COLOR above). sheet.indexize() later requires each pixel to match a palette entry exactly, so the smooth, blended edges anti-aliasing draws must be snapped to flat colors here - otherwise indexize() would reject every blended edge pixel as "not in the palette".
@paramctx@paramw@paramh
quantizeCanvasToShapeColors
(ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
, w: number
@paramw
w
, h: number
@paramh
h
) {
const const imageData: anyimageData = ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.getImageData(0, 0, w: number
@paramw
w
, h: number
@paramh
h
);
const const data: anydata = const imageData: anyimageData.data; for (let let i: numberi = 0; let i: numberi < const data: anydata.length; let i: numberi += 4) { if (const data: anydata[let i: numberi + 3] < const ALPHA_OPAQUE_THRESHOLD: 128ALPHA_OPAQUE_THRESHOLD) { const data: anydata[let i: numberi] = 0; const data: anydata[let i: numberi + 1] = 0; const data: anydata[let i: numberi + 2] = 0; const data: anydata[let i: numberi + 3] = 0; continue; } const { const r: number
Red channel (0-255).
r
, const g: number
Green channel (0-255).
g
, const b: number
Blue channel (0-255).
b
} = function nearestShapeColor(r: number, g: number, b: number): Color32
Finds whichever of FILL_COLOR/STROKE_COLOR is closer to a given pixel color. "Closer" here means smaller distance in RGB space - treating red, green, and blue like three coordinates and measuring the straight-line distance between two colors, the same way you would measure distance between two points on a map.
@paramr@paramg@paramb@returns
nearestShapeColor
(const data: anydata[let i: numberi], const data: anydata[let i: numberi + 1], const data: anydata[let i: numberi + 2]);
const data: anydata[let i: numberi] = const r: number
Red channel (0-255).
r
;
const data: anydata[let i: numberi + 1] = const g: number
Green channel (0-255).
g
;
const data: anydata[let i: numberi + 2] = const b: number
Blue channel (0-255).
b
;
const data: anydata[let i: numberi + 3] = 255; } ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.putImageData(const imageData: anyimageData, 0, 0);
} /** * Draws one filled shape centered inside a square cell. * * @param {OffscreenCanvasRenderingContext2D} ctx * @param {number} cellX - Left edge of the cell in sheet pixels. * @param {number} cellY - Top edge of the cell in sheet pixels. * @param {number} kind - 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond. */ function function drawShapeInCell(ctx: OffscreenCanvasRenderingContext2D, cellX: number, cellY: number, kind: number): void
Draws one filled shape centered inside a square cell.
@paramctx@paramcellX - Left edge of the cell in sheet pixels.@paramcellY - Top edge of the cell in sheet pixels.@paramkind - 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.
drawShapeInCell
(ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
, cellX: number
- Left edge of the cell in sheet pixels.
@paramcellX - Left edge of the cell in sheet pixels.
cellX
, cellY: number
- Top edge of the cell in sheet pixels.
@paramcellY - Top edge of the cell in sheet pixels.
cellY
, kind: number
- 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.
@paramkind - 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.
kind
) {
const const cx: numbercx = cellX: number
- Left edge of the cell in sheet pixels.
@paramcellX - Left edge of the cell in sheet pixels.
cellX
+ const SHAPE_CELL: 20SHAPE_CELL / 2;
const const cy: numbercy = cellY: number
- Top edge of the cell in sheet pixels.
@paramcellY - Top edge of the cell in sheet pixels.
cellY
+ const SHAPE_CELL: 20SHAPE_CELL / 2;
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.fillStyle = const FILL_COLOR: Color32FILL_COLOR.Color32.toHex(): string
Converts color to CSS hex string format.
@returnsHex string in the format "#RRGGBBAA".
toHex
();
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.strokeStyle = const STROKE_COLOR: Color32STROKE_COLOR.Color32.toHex(): string
Converts color to CSS hex string format.
@returnsHex string in the format "#RRGGBBAA".
toHex
();
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.lineWidth = 1;
if (kind: number
- 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.
@paramkind - 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.
kind
=== 0) {
// Square ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.fillRect(cellX: number
- Left edge of the cell in sheet pixels.
@paramcellX - Left edge of the cell in sheet pixels.
cellX
+ 4, cellY: number
- Top edge of the cell in sheet pixels.
@paramcellY - Top edge of the cell in sheet pixels.
cellY
+ 4, const SHAPE_CELL: 20SHAPE_CELL - 8, const SHAPE_CELL: 20SHAPE_CELL - 8);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.strokeRect(cellX: number
- Left edge of the cell in sheet pixels.
@paramcellX - Left edge of the cell in sheet pixels.
cellX
+ 4, cellY: number
- Top edge of the cell in sheet pixels.
@paramcellY - Top edge of the cell in sheet pixels.
cellY
+ 4, const SHAPE_CELL: 20SHAPE_CELL - 8, const SHAPE_CELL: 20SHAPE_CELL - 8);
} else if (kind: number
- 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.
@paramkind - 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.
kind
=== 1) {
// Circle ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.beginPath();
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.arc(const cx: numbercx, const cy: numbercy, const SHAPE_CELL: 20SHAPE_CELL / 2 - 4, 0, Math.PI * 2);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.fill();
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.stroke();
} else if (kind: number
- 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.
@paramkind - 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.
kind
=== 2) {
// Triangle ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.beginPath();
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.moveTo(const cx: numbercx, cellY: number
- Top edge of the cell in sheet pixels.
@paramcellY - Top edge of the cell in sheet pixels.
cellY
+ 3);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.lineTo(cellX: number
- Left edge of the cell in sheet pixels.
@paramcellX - Left edge of the cell in sheet pixels.
cellX
+ const SHAPE_CELL: 20SHAPE_CELL - 3, cellY: number
- Top edge of the cell in sheet pixels.
@paramcellY - Top edge of the cell in sheet pixels.
cellY
+ const SHAPE_CELL: 20SHAPE_CELL - 3);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.lineTo(cellX: number
- Left edge of the cell in sheet pixels.
@paramcellX - Left edge of the cell in sheet pixels.
cellX
+ 3, cellY: number
- Top edge of the cell in sheet pixels.
@paramcellY - Top edge of the cell in sheet pixels.
cellY
+ const SHAPE_CELL: 20SHAPE_CELL - 3);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.closePath();
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.fill();
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.stroke();
} else if (kind: number
- 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.
@paramkind - 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.
kind
=== 3) {
// Five-point star ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.beginPath();
for (let let i: numberi = 0; let i: numberi < 5; let i: numberi++) { const const outerAngle: numberouterAngle = (let i: numberi / 5) * Math.PI * 2 - Math.PI / 2; const const innerAngle: numberinnerAngle = const outerAngle: numberouterAngle + Math.PI / 5; const const outerR: numberouterR = const SHAPE_CELL: 20SHAPE_CELL / 2 - 2; const const innerR: numberinnerR = const outerR: numberouterR * 0.45; ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.lineTo(const cx: numbercx + Math.cos(const outerAngle: numberouterAngle) * const outerR: numberouterR, const cy: numbercy + Math.sin(const outerAngle: numberouterAngle) * const outerR: numberouterR);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.lineTo(const cx: numbercx + Math.cos(const innerAngle: numberinnerAngle) * const innerR: numberinnerR, const cy: numbercy + Math.sin(const innerAngle: numberinnerAngle) * const innerR: numberinnerR);
} ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.closePath();
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.fill();
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.stroke();
} else if (kind: number
- 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.
@paramkind - 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.
kind
=== 4) {
// Heart (two circles plus a triangle wedge) ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.beginPath();
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.arc(const cx: numbercx - 4, const cy: numbercy - 2, 5, 0, Math.PI * 2);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.arc(const cx: numbercx + 4, const cy: numbercy - 2, 5, 0, Math.PI * 2);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.fill();
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.beginPath();
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.moveTo(cellX: number
- Left edge of the cell in sheet pixels.
@paramcellX - Left edge of the cell in sheet pixels.
cellX
+ 3, const cy: numbercy);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.lineTo(const cx: numbercx, cellY: number
- Top edge of the cell in sheet pixels.
@paramcellY - Top edge of the cell in sheet pixels.
cellY
+ const SHAPE_CELL: 20SHAPE_CELL - 3);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.lineTo(cellX: number
- Left edge of the cell in sheet pixels.
@paramcellX - Left edge of the cell in sheet pixels.
cellX
+ const SHAPE_CELL: 20SHAPE_CELL - 3, const cy: numbercy);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.closePath();
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.fill();
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.stroke();
} else { // Diamond ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.beginPath();
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.moveTo(const cx: numbercx, cellY: number
- Top edge of the cell in sheet pixels.
@paramcellY - Top edge of the cell in sheet pixels.
cellY
+ 3);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.lineTo(cellX: number
- Left edge of the cell in sheet pixels.
@paramcellX - Left edge of the cell in sheet pixels.
cellX
+ const SHAPE_CELL: 20SHAPE_CELL - 3, const cy: numbercy);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.lineTo(const cx: numbercx, cellY: number
- Top edge of the cell in sheet pixels.
@paramcellY - Top edge of the cell in sheet pixels.
cellY
+ const SHAPE_CELL: 20SHAPE_CELL - 3);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.lineTo(cellX: number
- Left edge of the cell in sheet pixels.
@paramcellX - Left edge of the cell in sheet pixels.
cellX
+ 3, const cy: numbercy);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.closePath();
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.fill();
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.stroke();
} } /** * Builds a 3x2 sprite sheet with six shapes on an offscreen canvas. * * @returns {{ canvas: OffscreenCanvas, ctx: OffscreenCanvasRenderingContext2D, rects: Rect2i[] }} */ function
function buildShapeSheet(): {
    canvas: OffscreenCanvas;
    ctx: OffscreenCanvasRenderingContext2D;
    rects: Rect2i[];
}
Builds a 3x2 sprite sheet with six shapes on an offscreen canvas.
@returns
buildShapeSheet
() {
const const sheetW: numbersheetW = const SHAPE_COLS: 3SHAPE_COLS * const SHAPE_CELL: 20SHAPE_CELL; const const sheetH: numbersheetH = const SHAPE_ROWS: 2SHAPE_ROWS * const SHAPE_CELL: 20SHAPE_CELL; const const canvas: anycanvas = new OffscreenCanvas(const sheetW: numbersheetW, const sheetH: numbersheetH); const const ctx: anyctx = const canvas: anycanvas.getContext('2d'); if (!const ctx: anyctx) { throw new Error('Could not create 2D context for shape sheet'); } // Clear to transparent so unused pixels stay invisible. const ctx: anyctx.clearRect(0, 0, const sheetW: numbersheetW, const sheetH: numbersheetH); const const rects: {}rects = []; // One cell per entry in the shared SHAPE_NAMES list (the same list captions use). for (let let i: numberi = 0; let i: numberi < const SHAPE_NAMES: {}SHAPE_NAMES.length; let i: numberi++) { const const col: numbercol = let i: numberi % const SHAPE_COLS: 3SHAPE_COLS; const const row: anyrow = Math.floor(let i: numberi / const SHAPE_COLS: 3SHAPE_COLS); const const cellX: numbercellX = const col: numbercol * const SHAPE_CELL: 20SHAPE_CELL; const const cellY: numbercellY = const row: anyrow * const SHAPE_CELL: 20SHAPE_CELL; function drawShapeInCell(ctx: OffscreenCanvasRenderingContext2D, cellX: number, cellY: number, kind: number): void
Draws one filled shape centered inside a square cell.
@paramctx@paramcellX - Left edge of the cell in sheet pixels.@paramcellY - Top edge of the cell in sheet pixels.@paramkind - 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.
drawShapeInCell
(const ctx: anyctx, const cellX: numbercellX, const cellY: numbercellY, let i: numberi);
const rects: {}rects.push(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
(const cellX: numbercellX, const cellY: numbercellY, const SHAPE_CELL: 20SHAPE_CELL, const SHAPE_CELL: 20SHAPE_CELL));
} // Flatten the smooth, anti-aliased edges into flat colors so every pixel matches a // palette entry exactly (see quantizeCanvasToShapeColors() for why this is required). function quantizeCanvasToShapeColors(ctx: OffscreenCanvasRenderingContext2D, w: number, h: number): void
Rewrites every pixel of the canvas so it is either fully transparent or one of the exact design colors (see FILL_COLOR/STROKE_COLOR above). sheet.indexize() later requires each pixel to match a palette entry exactly, so the smooth, blended edges anti-aliasing draws must be snapped to flat colors here - otherwise indexize() would reject every blended edge pixel as "not in the palette".
@paramctx@paramw@paramh
quantizeCanvasToShapeColors
(const ctx: anyctx, const sheetW: numbersheetW, const sheetH: numbersheetH);
return { canvas: OffscreenCanvascanvas, ctx: OffscreenCanvasRenderingContext2Dctx, rects: {}rects }; } /** * Demonstrates sprite sheets, source rectangles, palette offsets, and opacity pulsing. * * @implements {IBTDemo} */ class class Demo
Demonstrates sprite sheets, source rectangles, palette offsets, and opacity pulsing.
@implementsIBTDemo
Demo
{
/** @type {Palette | null} */ Demo.palette: Palette | null
@type{Palette | null}
palette
= null;
/** @type {SpriteSheet | null} */ Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
= null;
// Slot map for the shared UI kit theme, filled in init() by applyTheme(). // theme.bg, theme.text, and friends are palette indices for our own drawing. Demo.theme: nulltheme = null; // One Rect2i per shape cell in the programmatic sheet. Demo.shapeRects: {}shapeRects = []; // Star cell - reused for the palette-offset row below the shape grid. /** @type {Rect2i | null} */ Demo.themeRect: Rect2i | null
@type{Rect2i | null}
themeRect
= null;
Demo.colorCount: numbercolorCount = 0; Demo.baseColors: {}baseColors = []; Demo.animTime: numberanimTime = 0; /** * @returns {Partial<HardwareSettings>} */ Demo.configure(): Partial<HardwareSettings>
Optional hook to declare display size, optional output drawing-buffer size, upscale filter, target fixed-update rate, rendering backend, and overlay. When omitted, the engine uses {@link defaultConfig } (`320x240` logical, `640x480` drawing buffer, `60` FPS, overlay enabled). When present, you may return only the fields you want to change; the engine merges them with {@link defaultConfig } via {@link mergeHardwareSettings } . Omit `displaySize` to inherit the full default resolution and output buffer. Include `displaySize` when you want a custom logical size; optional fields you omit then stay unset (for example no `drawingBufferSize` means a 1:1 drawing buffer).
@returns
configure
() {
return { isOverlayTimingChartEnabled: booleanisOverlayTimingChartEnabled: true,
overlayStyle: {
    barPaletteIndex: number;
    textPaletteIndex: number;
    gapPaletteIndex: number;
}
overlayStyle
: {
barPaletteIndex: numberbarPaletteIndex: const UI_BG: 240UI_BG, textPaletteIndex: numbertextPaletteIndex: const UI_DIM: 245UI_DIM, gapPaletteIndex: numbergapPaletteIndex: const UI_BAR: 246UI_BAR, },
overlayTimingChartStyle: {
    updateBarPaletteIndex: number;
    renderBarPaletteIndex: number;
    warningPaletteIndex: number;
    errorPaletteIndex: number;
    tagPaletteIndex: number;
}
overlayTimingChartStyle
: {
updateBarPaletteIndex: numberupdateBarPaletteIndex: const UI_DIM: 245UI_DIM, renderBarPaletteIndex: numberrenderBarPaletteIndex: const UI_INFO: 248UI_INFO, warningPaletteIndex: numberwarningPaletteIndex: const UI_INFO: 248UI_INFO, errorPaletteIndex: numbererrorPaletteIndex: const UI_TEXT: 244UI_TEXT, tagPaletteIndex: numbertagPaletteIndex: const UI_DIM: 245UI_DIM, }, }; } /** * Builds the shape sheet on a canvas, registers colors, and calls sheet.indexize(). * * @returns {Promise<boolean>} */ async Demo.init(): Promise<boolean>
Builds the shape sheet on a canvas, registers colors, and calls sheet.indexize().
@returns
init
() {
console.log('[SpriteDemo] Initializing...'); 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;
    ... 106 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);
// Install the shared UI theme: applyTheme() writes the twelve UI kit colors into // high palette slots (240-251), far above this demo's sprite colors (slots 10-19), // and returns a map of friendly names to those slots (this.theme.bg, .text, ...). // Every caption and the code panel below draw with these shared colors. this.Demo.theme: nulltheme = import applyThemeapplyTheme(this.Demo.palette: Palette
@type{Palette | null}
palette
);
try { const { const canvas: OffscreenCanvascanvas, const ctx: OffscreenCanvasRenderingContext2Dctx, const rects: {}rects } =
function buildShapeSheet(): {
    canvas: OffscreenCanvas;
    ctx: OffscreenCanvasRenderingContext2D;
    rects: Rect2i[];
}
Builds a 3x2 sprite sheet with six shapes on an offscreen canvas.
@returns
buildShapeSheet
();
this.Demo.shapeRects: {}shapeRects = const rects: {}rects; this.Demo.themeRect: Rect2i | null
@type{Rect2i | null}
themeRect
= const rects: {}rects[3]; // Star - used for palette-offset demos.
this.Demo.baseColors: {}baseColors = import registerCanvasColorsregisterCanvasColors(this.Demo.palette: Palette
@type{Palette | null}
palette
, const ctx: OffscreenCanvasRenderingContext2Dctx, const canvas: OffscreenCanvascanvas.width, const canvas: OffscreenCanvascanvas.height, const COLOR_BASE: 10COLOR_BASE);
this.Demo.colorCount: numbercolorCount = this.Demo.baseColors: {}baseColors.length; const const colorCount: numbercolorCount = this.Demo.colorCount: numbercolorCount; // Build theme blocks: Fire, Ice, Void, and Pulse are static once written here. // palette.fillBlock(start, source, transform) writes transform(baseColor) into one // slot per base color, starting at `start` - it replaces a hand-written for loop. // Fire: push red up and pull blue down. this.Demo.palette: Palette
@type{Palette | null}
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: 10COLOR_BASE + const colorCount: numbercolorCount, this.Demo.baseColors: {}baseColors, (base: Color32base) => 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
(Math.min(255, base: Color32base.Color32.r: number
Red channel (0-255).
r
+ 80), base: Color32base.Color32.g: number
Green channel (0-255).
g
, Math.max(0, base: Color32base.Color32.b: number
Blue channel (0-255).
b
- 80)),
); // Ice: pull red down and push blue up. this.Demo.palette: Palette
@type{Palette | null}
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: 10COLOR_BASE + const colorCount: numbercolorCount * 2, this.Demo.baseColors: {}baseColors, (base: Color32base) => 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
(Math.max(0, base: Color32base.Color32.r: number
Red channel (0-255).
r
- 60), base: Color32base.Color32.g: number
Green channel (0-255).
g
, Math.min(255, base: Color32base.Color32.b: number
Blue channel (0-255).
b
+ 80)),
); // Void: darken every channel toward black. this.Demo.palette: Palette
@type{Palette | null}
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: 10COLOR_BASE + const colorCount: numbercolorCount * 3, this.Demo.baseColors: {}baseColors, (base: Color32base) => 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
(Math.floor(base: Color32base.Color32.r: number
Red channel (0-255).
r
* 0.25), Math.floor(base: Color32base.Color32.g: number
Green channel (0-255).
g
* 0.25), Math.floor(base: Color32base.Color32.b: number
Blue channel (0-255).
b
* 0.25)),
); // Pulse: same colors as the original, at full opacity. this.Demo.palette: Palette
@type{Palette | null}
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: 10COLOR_BASE + const colorCount: numbercolorCount * 4, this.Demo.baseColors: {}baseColors, (base: Color32base) => 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
(base: Color32base.Color32.r: number
Red channel (0-255).
r
, base: Color32base.Color32.g: number
Green channel (0-255).
g
, base: Color32base.Color32.b: number
Blue channel (0-255).
b
, 255),
); const const image: anyimage = await import canvasToImagecanvasToImage(const canvas: OffscreenCanvascanvas); this.Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
= 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);
this.Demo.sheet: SpriteSheet
@type{SpriteSheet | null}
sheet
.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
(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;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.paletteSet: (palette: Palette) => void
Stores the active engine palette. Use this to swap the **entire palette** (e.g. switch between a day and night theme). After this call the renderer uploads the new palette uniform on the next frame. **Palette-value swap (change what a slot looks like):** mutate the live {@link BT.palette } in place with `palette.set(slot, newColor)`. The renderer uploads dirty slots on the next frame; no `paletteSet()` or {@link BT.spritesRefresh } needed. **Palette-layout swap (same colors, different slot positions):** build a new palette with the same colors at new indices, call `paletteSet()`, then call {@link BT.spritesRefresh } so every sprite sheet re-maps its original RGBA pixels against the new slot layout.
@since1.0.3@parampalette - Palette to make active.
paletteSet
(this.Demo.palette: Palette
@type{Palette | null}
palette
);
console.log( `[SpriteDemo] Built shape sheet: ${const canvas: OffscreenCanvascanvas.width}x${const canvas: OffscreenCanvascanvas.height}px, ${const colorCount: numbercolorCount} unique colors`, ); } catch (function (local var) error: unknownerror) { console.error('[SpriteDemo] Failed to build shape sheet:', function (local var) error: unknownerror); return false; } console.log('[SpriteDemo] Initialization complete!'); 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
() {
this.Demo.animTime: numberanimTime +=
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.deltaSeconds: number
Fixed-step seconds per update tick. Equivalent to `1 / BT.targetFPS` when `BT.targetFPS` is finite and positive. Falls back to `1 / 60` when target FPS is non-finite or non-positive.
@since1.0.4@returnsSeconds advanced by one fixed update tick.
deltaSeconds
;
if (!this.Demo.colorCount: numbercolorCount) { return; } } 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
() {
// Clear the whole screen with the shared UI theme's background color.
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.clear: (paletteIndex: number) => void
Sets the frame clear color using a palette index. The renderer uses this color when clearing the full display at the start of the next frame.
@since0.1.0@parampaletteIndex - Palette index for the full-screen clear pass.
clear
(this.Demo.theme: nulltheme.bg);
// Row 1: six shapes - each draw call uses a different source Rect2i. const const shapeY: 14shapeY = 14; const const shapeSpacing: 50shapeSpacing = 50; for (let let i: numberi = 0; let i: numberi < this.Demo.shapeRects: {}shapeRects.length; let i: numberi++) { const const destX: numberdestX = 6 + let i: numberi * const shapeSpacing: 50shapeSpacing;
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.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
(this.Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
, this.Demo.shapeRects: {}shapeRects[let i: numberi], new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const destX: numberdestX, const shapeY: 14shapeY), 0);
import uiui.caption(const destX: numberdestX, const shapeY: 14shapeY + 22, const SHAPE_NAMES: {}SHAPE_NAMES[let i: numberi], { color: stringcolor: 'dim' }); } import uiui.caption(6, 58, 'Source rects - one region per shape', { color: stringcolor: 'dim' }); // Row 2: palette offsets on the star shape (offset shifts every pixel index). const const n: numbern = this.Demo.colorCount: numbercolorCount; const const themeY: 78themeY = 78; const const themeSpacing: 72themeSpacing = 72;
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.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
(this.Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
, this.Demo.themeRect: Rect2i | null
@type{Rect2i | null}
themeRect
, 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
(8, const themeY: 78themeY), 0);
import uiui.caption(6, const themeY: 78themeY + 22, 'Original', { color: stringcolor: 'dim' });
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.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
(this.Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
, this.Demo.themeRect: Rect2i | null
@type{Rect2i | null}
themeRect
, 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
(8 + const themeSpacing: 72themeSpacing, const themeY: 78themeY), const n: numbern);
import uiui.caption(6 + const themeSpacing: 72themeSpacing, const themeY: 78themeY + 22, 'Fire', { color: stringcolor: 'dim' });
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.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
(this.Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
, this.Demo.themeRect: Rect2i | null
@type{Rect2i | null}
themeRect
, 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
(8 + const themeSpacing: 72themeSpacing * 2, const themeY: 78themeY), const n: numbern * 2);
import uiui.caption(6 + const themeSpacing: 72themeSpacing * 2, const themeY: 78themeY + 22, 'Ice', { color: stringcolor: 'dim' });
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.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
(this.Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
, this.Demo.themeRect: Rect2i | null
@type{Rect2i | null}
themeRect
, 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
(8 + const themeSpacing: 72themeSpacing * 3, const themeY: 78themeY), const n: numbern * 3);
import uiui.caption(6 + const themeSpacing: 72themeSpacing * 3, const themeY: 78themeY + 22, 'Void', { color: stringcolor: 'dim' }); this.Demo.renderCodeSnippet(): void
The "how you would load a real PNG" cheat sheet, as a bordered kit panel anchored to the bottom-left corner of the screen.
renderCodeSnippet
();
} /** * The "how you would load a real PNG" cheat sheet, as a bordered kit panel anchored * to the bottom-left corner of the screen. */ Demo.renderCodeSnippet(): void
The "how you would load a real PNG" cheat sheet, as a bordered kit panel anchored to the bottom-left corner of the screen.
renderCodeSnippet
() {
import uiui.begin(import UI_ANCHORSUI_ANCHORS.BOTTOM_LEFT, { y: numbery: 178 }); import uiui.panel('Production PNG load:'); import uiui.label('const indexed = await SpriteSheet', { color: stringcolor: 'info' }); import uiui.label(" .loadIndexed('/sprites/test.png', palette, 10);", { color: stringcolor: 'info' }); import uiui.end(); } } 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
Demonstrates sprite sheets, source rectangles, palette offsets, and opacity pulsing.
@implementsIBTDemo
Demo
);