// Image Output: demonstrates BT.downloadFrame().
// @description Take a screenshot of whatever is on screen with BT.downloadFrame and save it straight out as a PNG.
//
// BT.downloadFrame() takes a screenshot of whatever is currently on screen and saves
// it as a PNG image file to your computer. Click or tap the "Save PNG" button from the
// shared UI kit (or press S) to download the current frame - so the demo works on
// touch screens too. Note: the kit panel is drawn on screen, so it appears in the
// saved PNG as well. That is fine for this demo - see the comment in render().
//
// Dev-mode extras, built into the engine itself (every demo gets these for free,
// not just this one): while BT.isDevMode is true (running from `pnpm run dev`, not
// a production build), pressing F9 copies the current frame to the OS clipboard, and
// Shift+F9 saves it as a timestamped file - neither needs a button click first. See
// HardwareSettings.isFrameCaptureShortcutEnabled.
// Note for demos other than this one: both shortcuts save at the logical
// BT.displaySize, not BT.outputSize, so they stay pixel-for-pixel even when a demo
// sets a larger drawingBufferSize for display-tier post-process effects (CRT,
// vignette, and the like) - those effects are not included in either shortcut's
// output. This demo has no drawingBufferSize, so its own shortcut output is
// unaffected and still matches the Save PNG button above.
// The engine also exposes the whole BT namespace as window.BT in dev mode
// (BootstrapOptions.exposeGlobal, on by default), so you can run
// window.BT.downloadFrame('my-file.png') straight from the browser console at any time.
//
// Prerequisites: Basics (https://demos.blit386.dev/basics).
// Guide: https://blit386.dev/docs/api/rendering#frame-capture

import { function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
,
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
, class Color32
Mutable 32-bit RGBA color value with 8-bit channels.
@since0.1.0
Color32
, class 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
, 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 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 */ // Every color used for drawing is stored in a numbered "palette" slot. // Think of each slot like a labeled paint jar on an artist's shelf. // Index 0 is always transparent (invisible). Our custom colors start at 1. // These are the SCENE colors - the test pattern itself. All the UI text and the // Save button draw with the shared UI theme instead (installed by applyTheme() in init()). const const C_WHITE: 1C_WHITE = 1; // White: grid dots, border, and crosshairs const const C_BG: 2C_BG = 2; // Very dark blue-gray: the background color // Dynamic slots: these six colors change every frame to create the animated rainbow stripes. // We pre-allocate (reserve) index slots 10 through 15, one for each horizontal stripe. // In update() we calculate the new color and store it here; render() just uses the index. // This is called "palette animation" - the retro trick that made old consoles look alive! const const C_STRIPE_0: 10C_STRIPE_0 = 10; // Animated color for the top stripe (stripe 0) // Stripes 1-5 follow at C_STRIPE_0 + 1 through C_STRIPE_0 + 5 /** * Image output demo. * Draws a colorful test pattern and saves the frame to PNG when the kit's * "Save PNG" button is clicked, tapped, or triggered with the S key. * * @implements {IBTDemo} */ class class Demo
Image output demo. Draws a colorful test pattern and saves the frame to PNG when the kit's "Save PNG" button is clicked, tapped, or triggered with the S key.
@implementsIBTDemo
Demo
{
// palette holds the list of colors the engine uses for drawing. /** @type {Palette | null} */ Demo.palette: Palette | null
@type{Palette | null}
palette
= null;
// tick counts how many update steps have run since the demo started (goes up by 1 each update). // We use it to animate the gradient stripes and to show a frame number in the panel. Demo.tick: numbertick = 0; // capturing is true while we are waiting for BT.downloadFrame() to finish saving the file. // We use it so you cannot trigger two saves at once and to show "Capturing..." on screen. Demo.capturing: booleancapturing = false; // lastCaptureMessage holds the text we show after a save succeeds or fails (for example the file name or an error). Demo.lastCaptureMessage: stringlastCaptureMessage = ''; // lastCaptureColor remembers which UI color role to draw that message with: // 'accent' (green) for a successful save, 'warm' (orange) for an error. Demo.lastCaptureColor: stringlastCaptureColor = 'accent'; // messageTimer counts down how many more frames to show lastCaptureMessage before hiding it. // 180 frames is 3 seconds at 60 FPS, then the message disappears. Demo.messageTimer: numbermessageTimer = 0; /** * Hides the overlay toggle hint so saved screenshots stay clean, and pins the * display size so BT.downloadFrame() saves raw, 1:1 pixels. * * @returns {Partial<HardwareSettings>} Demo hardware settings. */ Demo.configure(): Partial<HardwareSettings>
Hides the overlay toggle hint so saved screenshots stay clean, and pins the display size so BT.downloadFrame() saves raw, 1:1 pixels.
@returnsDemo hardware settings.
configure
() {
return { // The engine usually draws a small "~" hint in the bottom-left corner to // tell people they can press the Backquote key (`) to open the stats // overlay. This demo's whole point is saving a picture with // BT.downloadFrame(), and the overlay is drawn on top of everything, so // that hint would end up baked into the saved PNG. We hide the hint to keep // captures tidy. (The kit's Save panel DOES appear in the capture - a // deliberate trade-off so touch users can save at all; see render().) // The overlay still works on demand: press ` to show it and ` again to // hide it before you capture. The bottom-left 17x13 corner also stays // tappable to toggle it, which is why our UI panel avoids that corner // (it sits in the top-left instead). isOverlayToggleHintVisible: booleanisOverlayToggleHintVisible: false, // Most demos leave displaySize unset and let the engine fill in its // default hardware profile, which includes a 640x480 "drawing buffer" - // an internal 2x upscale so the picture looks crisp on screen. Save PNG // would then download that upscaled 640x480 buffer instead of the demo's // real 320x240 canvas. Declaring displaySize here (even though 320x240 is // already the default value) tells the engine "this demo picked its own // sizes on purpose," which turns that automatic upscale off. The picture // still looks sharp on screen - your browser scales the smaller image up // using the same nearest-neighbor technique the engine used internally - // but the saved PNG now has exactly one file pixel per canvas pixel, // which is what "pixel-perfect" means for retro pixel art. displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(320, 240),
}; } /** * Sets up the color palette and installs the shared UI theme colors. * The Save button itself is declared every frame in render(). * * @returns {Promise<boolean>} Resolves to `true` when the demo is ready to run. */ async Demo.init(): Promise<boolean>
Sets up the color palette and installs the shared UI theme colors. The Save button itself is declared every frame in render().
@returnsResolves to `true` when the demo is ready to run.
init
() {
// Step 1: build the color palette // Create a palette with room for 256 colors. 256 is a classic retro amount. 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);
// Store the static scene colors we always need. this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_WHITE: 1C_WHITE, 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
(255, 255, 255)); // pure white
this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_BG: 2C_BG, 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
(20, 20, 30)); // very dark blue-gray background
// Pre-fill the six animated stripe slots with a starting color (dark gray). // They will be updated properly in update() on the very first tick. // Filling them now prevents any "empty slot" glitches on the first frame. for (let let i: numberi = 0; let i: numberi < 6; let i: numberi++) { this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_STRIPE_0: 10C_STRIPE_0 + let i: numberi, 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
(40, 40, 40));
} // Step 2: install the shared UI theme. applyTheme() writes the twelve UI kit // colors into high palette slots (240-251 by default), far above our scene // slots 1-15. Every kit widget (the panel, button, labels) draws with these // colors automatically - this demo never needs the slot numbers itself, so // we call applyTheme() only for that side effect and ignore its return value. import applyThemeapplyTheme(this.Demo.palette: Palette
@type{Palette | null}
palette
);
// Tell the engine "use this palette from now on."
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
);
return true; } /** * Advances the demo clock, expires transient status messages, and updates * the animated stripe colors in the palette. * Runs at a fixed rate (60 times per second). */ Demo.update(): void
Advances the demo clock, expires transient status messages, and updates the animated stripe colors in the palette. Runs at a fixed rate (60 times per second).
update
() {
// Let the UI kit do its per-tick housekeeping first: it latches keyboard // shortcuts (like the Save button's S binding) and tracks touch contacts. // This must be the first line of update() so nothing misses a key press. import uiui.tick(); // Bump the frame counter so animations and the on-screen "FRAME" row keep changing. this.Demo.tick: numbertick++; // If we are showing a success or error message, count down until it should disappear. if (this.Demo.messageTimer: numbermessageTimer > 0) { this.Demo.messageTimer: numbermessageTimer--; } // Palette animation for the six horizontal stripes // Instead of computing colors inside render(), we compute them here in update() // and store the results in reserved palette slots. render() then just uses the index numbers. // This is the classic "palette animation" technique - retro hardware did the same thing! for (let let i: numberi = 0; let i: numberi < 6; let i: numberi++) { // phase is an angle-like value 0-359 that moves as tick increases. // Each stripe adds i * 20 so neighboring stripes do not look identical. // The % 360 keeps the value from growing forever (it wraps back to 0 at 360). const const phase: numberphase = (this.Demo.tick: numbertick + let i: numberi * 20) % 360; // Red channel: Math.sin returns a wave between -1 and +1. // Multiplying by 127 and adding 127 shifts that to a range of 0 to 254. // Math.PI / 180 converts degrees to radians, which is what Math.sin expects. const const r: anyr = Math.floor(127 + 127 * Math.sin((const phase: numberphase * Math.PI) / 180)); // Green channel: same sine wave but shifted 120 degrees around the color wheel. // Shifting each channel separately makes R, G, and B cycle out of step, // which creates a rainbow effect as the phase changes. const const g: anyg = Math.floor(127 + 127 * Math.sin(((const phase: numberphase + 120) * Math.PI) / 180)); // Blue channel: shifted 240 degrees so all three are evenly spaced. const const b: anyb = Math.floor(127 + 127 * Math.sin(((const phase: numberphase + 240) * Math.PI) / 180)); // Store the computed color in the palette slot for this stripe. // render() will read C_STRIPE_0 + i to draw each stripe - no Color32 needed there! this.Demo.palette: Palette | null
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_STRIPE_0: 10C_STRIPE_0 + let i: numberi, 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
(const r: anyr, const g: anyg, const b: anyb));
} } /** * Renders the animated gradient test pattern and the UI kit panel with the * Save button and capture status. Runs once per screen refresh. */ Demo.render(): void
Renders the animated gradient test pattern and the UI kit panel with the Save button and capture status. Runs once per screen refresh.
render
() {
// Ask the engine how wide and tall our virtual screen is in pixels. const const screen: Vector2iscreen =
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
.displaySize: Vector2i
Active logical render resolution in pixels. This is the game/simulation coordinate space configured by the demo, not the canvas element's CSS size. Each read returns a clone.
@since1.0.4@returnsConfigured logical size, or `Vector2i.zero()` before initialization.
displaySize
;
// Fill the whole framebuffer with the dark background before drawing anything else. // C_BG is just a number (2); the palette knows it means dark blue-gray.
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.clear: (paletteIndex: number) => void
Sets the frame clear color using a palette index. The renderer uses this color when clearing the full display at the start of the next frame.
@since0.1.0@parampaletteIndex - Palette index for the full-screen clear pass.
clear
(const C_BG: 2C_BG);
// Draw six horizontal stripes using the animated palette slots we updated in update(). // Each stripe's color was already computed and stored - we just reference the index. const const stripeHeight: 40stripeHeight = 40; for (let let i: numberi = 0; let i: numberi < 6; let i: numberi++) { // y is the top edge of this stripe in pixels. const const y: numbery = let i: numberi * const stripeHeight: 40stripeHeight; // C_STRIPE_0 + i picks the correct palette slot for this stripe (10, 11, 12, ...).
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
.drawRectFill: (rect: Rect2i, paletteIndex: number) => void
Draws a filled rectangle.
@since0.1.0@paramrect - Rectangle bounds in display coordinates.@parampaletteIndex - Palette color index.
drawRectFill
(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(0, const y: numbery, const screen: Vector2iscreen.Vector2i.x: number
Horizontal component (defaults to 0).
x
, const stripeHeight: 40stripeHeight), const C_STRIPE_0: 10C_STRIPE_0 + let i: numberi);
} // Draw a grid of single white pixels every 20 pixels so you can see alignment when you open the PNG. for (let let x: numberx = 0; let x: numberx < const screen: Vector2iscreen.Vector2i.x: number
Horizontal component (defaults to 0).
x
; let x: numberx += 20) {
for (let let y: numbery = 0; let y: numbery < const screen: Vector2iscreen.Vector2i.y: number
Vertical component (defaults to 0).
y
; let y: numbery += 20) {
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.drawPixel: (posOrX: Vector2i | number, yOrColor: number, maybeColor?: number) => void
Draws a single pixel. Accepts either: - `(posOrX: Vector2i, yOrColor: number)` where `yOrColor` is the palette index. - `(posOrX: number, yOrColor: number, maybeColor: number)` for `(x, y, paletteIndex)`.
@since0.1.0@paramposOrX - Pixel position as `Vector2i`, or x coordinate when using numeric overload.@paramyOrColor - Palette index for vector overload, or y coordinate for numeric overload.@parammaybeColor - Palette index when using numeric overload.
drawPixel
(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
(let x: numberx, let y: numbery), const C_WHITE: 1C_WHITE);
} } // Outline the entire display with a white rectangle so the edges are obvious in the screenshot.
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
.drawRect: (rect: Rect2i, paletteIndex: number) => void
Draws an unfilled rectangle outline.
@since0.1.0@paramrect - Rectangle bounds in display coordinates.@parampaletteIndex - Palette color index.
drawRect
(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(0, 0, const screen: Vector2iscreen.Vector2i.x: number
Horizontal component (defaults to 0).
x
, const screen: Vector2iscreen.Vector2i.y: number
Vertical component (defaults to 0).
y
), const C_WHITE: 1C_WHITE);
// Crosshairs at the exact center: cx is half the width, cy is half the height (floored to whole pixels). const const cx: anycx = Math.floor(const screen: Vector2iscreen.Vector2i.x: number
Horizontal component (defaults to 0).
x
/ 2);
const const cy: anycy = Math.floor(const screen: Vector2iscreen.Vector2i.y: number
Vertical component (defaults to 0).
y
/ 2);
// Horizontal line through the center (20 pixels left and right of center).
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.drawLine: (p0: Vector2i, p1: Vector2i, paletteIndex: number) => void
Draws a pixel-perfect line between two points. Uses rasterized line drawing without antialiasing.
@since0.1.0@paramp0 - Start position in display coordinates.@paramp1 - End position in display coordinates.@parampaletteIndex - Palette color index.
drawLine
(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 cx: anycx - 20, const cy: anycy), 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 cx: anycx + 20, const cy: anycy), const C_WHITE: 1C_WHITE);
// Vertical line through the center (20 pixels up and down from center).
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.drawLine: (p0: Vector2i, p1: Vector2i, paletteIndex: number) => void
Draws a pixel-perfect line between two points. Uses rasterized line drawing without antialiasing.
@since0.1.0@paramp0 - Start position in display coordinates.@paramp1 - End position in display coordinates.@parampaletteIndex - Palette color index.
drawLine
(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 cx: anycx, const cy: anycy - 20), 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 cx: anycx, const cy: anycy + 20), const C_WHITE: 1C_WHITE);
// UI kit panel in the top-left corner. We avoid bottom-left specifically // because the engine keeps that 17x13 corner tappable for toggling the stats // overlay, and we do not want the two to fight over taps; top-left has no // such conflict. // Note: this panel is drawn onto the frame, so it WILL be part of the saved // PNG. That is acceptable here - it even doubles as a caption telling you // which demo produced the screenshot. import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT); import uiui.panel('Image Output'); // The Save button. ui.button() returns true only on the single frame it was // clicked, tapped, or its bound key (S) was pressed - so holding S does not // spam downloads. We also ignore it while a save is already running. // Bound to S rather than Space: Space is the browser's page-scroll key, and // this demo does not opt into isCapturingKeyboardScroll, so a Space press // would both save AND scroll the page - confusing on a page taller than the // canvas. if (import uiui.button('Save PNG (S)', { key: stringkey: 'KeyS' }) && !this.Demo.capturing: booleancapturing) { this.Demo.saveFrame(): void
Starts an asynchronous PNG download of the current frame and remembers the outcome so render() can show a status message for a few seconds.
saveFrame
();
} // One status row below the button. We always draw a row (even when idle) so // the panel does not jump in size when a message appears or disappears. if (this.Demo.capturing: booleancapturing) { // A save is in flight - the browser is busy reading the canvas. import uiui.label('Capturing...', { color: stringcolor: 'info' }); } else if (this.Demo.messageTimer: numbermessageTimer > 0) { // A save just finished - show the result in green (success) or orange (error). import uiui.label(this.Demo.lastCaptureMessage: stringlastCaptureMessage, { color: stringcolor: this.Demo.lastCaptureColor: stringlastCaptureColor }); } else { // Nothing happening - a quiet hint in dim gray. In dev mode (running from // `pnpm run dev`, not a production build) we also mention the F9/Shift+F9 // shortcuts - engine defaults (every demo gets them, see // HardwareSettings.isFrameCaptureShortcutEnabled), not something this demo // wires up itself. It has no visible button of its own to advertise them, so // this is the most relevant place to mention them. const const hint: "Saves the current frame (dev: F9 copies, Shift+F9 saves)" | "Saves the current frame"hint =
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
.isDevMode: boolean
Whether this is a development build, not a release build. Resolves from the `blit386/vite` plugin's injected runtime marker, falling back to a live Vite HMR context, otherwise release. (The underlying resolver also accepts an explicit override that always wins over both; nothing in the public `BT` surface supplies one today.) This is UX/DX gating, not DRM - any consumer can flip the underlying global by hand.
@since1.5.0@returns`true` for a dev build, `false` for release.
isDevMode
? 'Saves the current frame (dev: F9 copies, Shift+F9 saves)' : 'Saves the current frame'; import uiui.label(const hint: "Saves the current frame (dev: F9 copies, Shift+F9 saves)" | "Saves the current frame"hint, { color: stringcolor: 'dim' }); } // Frame counter so you can tell consecutive screenshots apart. import uiui.kv('FRAME', this.Demo.tick: numbertick); import uiui.end(); } /** * Starts an asynchronous PNG download of the current frame and remembers * the outcome so render() can show a status message for a few seconds. */ Demo.saveFrame(): void
Starts an asynchronous PNG download of the current frame and remembers the outcome so render() can show a status message for a few seconds.
saveFrame
() {
// Mark the save as "in flight" so the button ignores further presses // and render() shows the "Capturing..." status row. this.Demo.capturing: booleancapturing = true; // BT.downloadFrame() reads the canvas and asks the browser to save a file. // Most browsers open a "Save as" dialog or drop the file straight into your // Downloads folder (depends on your browser settings). The demo cannot pick // the folder for you - that is normal browser security.
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
.downloadFrame: (filename?: string) => Promise<void>
Captures the next rendered frame and downloads it from the browser. Convenience wrapper around {@link BT.captureFrame } that creates a temporary object URL and clicks a synthetic anchor element.
@since1.0.3@paramfilename - Target download filename.@exampleawait BT.downloadFrame(); await BT.downloadFrame('screenshot-001.png');
downloadFrame
('blit386-capture.png')
.then(() => { // Success: remember a friendly message and show it in green. this.Demo.lastCaptureMessage: stringlastCaptureMessage = 'Saved: blit386-capture.png'; this.Demo.lastCaptureColor: stringlastCaptureColor = 'accent'; this.Demo.messageTimer: numbermessageTimer = 180; // 3 seconds at 60 FPS this.Demo.capturing: booleancapturing = false; return null; }) .catch((err: anyerr) => { // Failure: show the error in orange and log details for developers. this.Demo.lastCaptureMessage: stringlastCaptureMessage = `Error: ${err: anyerr.message}`; this.Demo.lastCaptureColor: stringlastCaptureColor = 'warm'; this.Demo.messageTimer: numbermessageTimer = 180; this.Demo.capturing: booleancapturing = false; console.error('[Demo] Capture failed:', err: anyerr); }); } } // Hand the Demo class to BLIT386 to start the demo loop. 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
Image output demo. Draws a colorful test pattern and saves the frame to PNG when the kit's "Save PNG" button is clicked, tapped, or triggered with the S key.
@implementsIBTDemo
Demo
);