/*

Image Output: save the frame as a PNG with BT.downloadFrame() or BT.captureFrame().
@description Capture frames as PNG with BT.downloadFrame and BT.captureFrame, at output or display size.
@ogScale integer

Part of the BLIT386 demo series.

Prerequisites:
  - Basics: https://demos.blit386.dev/basics
  - Primitives: https://demos.blit386.dev/primitives

Live version: https://demos.blit386.dev/image-output

Guides:
  - Overlay: https://blit386.dev/docs/guides/overlay#visibility-model

Reference:
  - Rendering: https://blit386.dev/docs/api/rendering#frame-capture
  - Core: https://blit386.dev/docs/api/core#resolution-model
  - Core: https://blit386.dev/docs/api/core#dev-vs-release-mode

---

WHAT YOU WILL SEE
-----------------

A test pattern that is easy to check in an image viewer: six horizontal stripes, each 40
pixels tall, slowly cycling through the rainbow; a grid of single white dots every 20
pixels; a white border around the whole screen; and a small white crosshair in the exact
center. A UI kit panel titled "Image Output" sits in the top-left corner with three
buttons - "Save PNG (S)", "Size: output (Z)", and "Capture Blob (B)" - a status row, and a
FRAME counter that goes up by one every tick.

WHAT YOU WILL LEARN
-------------------

- BT.downloadFrame(filename, { size }) takes a screenshot of the next rendered frame and
  saves it as a PNG file on your computer.
- BT.captureFrame({ size }) takes the same screenshot but hands you the PNG as a Blob (a
  chunk of file data in memory) instead of saving it.
- The size option: 'output' (the default) gives the full drawing buffer, 'display' gives
  exactly one PNG pixel per game pixel.
- displaySize vs drawingBufferSize in configure(): this demo draws on a 320x240 grid but
  asks for a 640x480 drawing buffer, so the two capture sizes produce different images.
- Promises: both calls return a promise, and the demo reacts with .then() when the capture
  finishes and .catch() when it fails.
- Palette animation: update() writes new colors into six palette slots every tick, and
  render() keeps drawing with the same slot numbers.
- isOverlayToggleHintVisible: hiding the engine's overlay hint so it does not end up in
  the saved picture.

WHICH SIZE IS WHICH?
--------------------

Most games never set displaySize or drawingBufferSize and inherit the engine default: a
320x240 game grid drawn into a 640x480 buffer, so every game pixel is a crisp 2x2 block
on screen. This demo spells both sizes out in configure() so the choice is visible.

  - 'output' captures BT.outputSize, the 640x480 buffer, including any display-tier
    post-process effects (CRT, vignette, and the like). This is what the player sees.
  - 'display' captures BT.displaySize, the 320x240 game grid, with no upscale and no
    display-tier effects. Best for pixel-art export and image comparisons.

The "Size" button (key Z) switches between the two, and the status row after a capture
tells you which one was used.

The kit panel is drawn onto the frame like everything else, so it appears in the saved
PNG too. That is on purpose: it doubles as a caption saying which demo made the picture,
and it is what lets touch users save at all.

DEV-MODE SHORTCUTS
------------------

The engine itself adds two capture shortcuts in development builds (running from
`pnpm run dev`, not a production build), for every game, not just this demo: F9 copies
the current frame to the clipboard, and Shift+F9 downloads it as a timestamped PNG file.
Both capture at BT.displaySize, so they match this demo's Save PNG button only while the
Size button says 'display'. HardwareSettings.isFrameCaptureShortcutEnabled turns them on
or off explicitly; the default follows BT.isDevMode. In dev mode the engine also exposes
the whole BT namespace as window.BT (BootstrapOptions.exposeGlobal), so you can type
window.BT.downloadFrame('my-file.png') in the browser console at any time.

TRY THIS
--------

- Press S, or click or tap "Save PNG (S)", to download the current frame. The status row
  turns green with the file name on success, or orange with the error.
- Press Z to switch the size to 'display', save again, and compare the two files: one is
  640x480 with 2x2 blocks, the other is 320x240 with single pixels.
- Press B, or click "Capture Blob (B)", to capture without saving. The status row shows
  the picture's width, height, and byte count read straight from the Blob.
- Press the backquote key (`) to open the engine overlay, then save: the overlay is drawn
  on top of the frame, so it ends up in the PNG too. Press ` again to hide it first when
  you want a clean 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;
    ... 124 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').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. Slot 0 is always transparent, so our own // colors start at 1. These are the scene colors - the test pattern itself. The panel, // buttons, and text draw with the shared UI theme instead (installed by applyTheme() in // init()). const const C_WHITE: 1C_WHITE = 1; // grid dots, border, and crosshairs const const C_BG: 2C_BG = 2; // very dark blue-gray background // Dynamic slots: these six colors change every tick to create the animated rainbow // stripes. We reserve slots 10 through 15, one for each horizontal stripe. update() // calculates the new color and stores it here; render() just uses the slot number. This // is called "palette animation" - the retro trick that made old consoles look alive. const const C_STRIPE_0: 10C_STRIPE_0 = 10; // top stripe; stripes 1-5 follow at C_STRIPE_0 + 1 through + 5 // The two capture sizes the engine understands (the FrameCaptureSize type). Named // constants, so a typo cannot silently pick the wrong one. const const SIZE_OUTPUT: "output"SIZE_OUTPUT = 'output'; // the whole drawing buffer, including display effects const const SIZE_DISPLAY: "display"SIZE_DISPLAY = 'display'; // exact game pixels, one PNG pixel per game pixel // Draws a colorful test pattern and saves the frame as a PNG when the kit's "Save PNG" // button is clicked, tapped, or triggered with the S key. class class DemoDemo { // The palette holds the list of colors the engine uses for drawing. /** @type {Palette | null} */ Demo.palette: Palette | null
@type{Palette | null}
palette
= null;
// Counts how many update() ticks have run since the demo started (goes up by 1 each // tick). We use it to animate the stripes and to show a frame number in the panel. Demo.tick: numbertick = 0; // True while we are waiting for a capture to finish. It stops you from starting two // captures at once and makes render() show "Capturing..." on screen. Demo.capturing: booleancapturing = false; // The text we show after a capture succeeds or fails (the file name, the Blob size, // or an error). Demo.lastCaptureMessage: stringlastCaptureMessage = ''; // Which UI color role to draw that message with: 'accent' (green) for success, 'warm' // (orange) for an error. Demo.lastCaptureColor: stringlastCaptureColor = 'accent'; // The size option we pass to BT.downloadFrame() and BT.captureFrame(). 'output' means // the full drawing buffer (640x480 here); 'display' means one PNG pixel per game // pixel (320x240 here). The Size button switches between them. Demo.captureSize: stringcaptureSize = const SIZE_OUTPUT: "output"SIZE_OUTPUT; // Counts down how many more ticks to show lastCaptureMessage before hiding it. 180 // ticks is 3 seconds at 60 ticks per second. Demo.messageTimer: numbermessageTimer = 0; // Hides the overlay toggle hint so saved screenshots stay clean, and sets a 320x240 // display on a 640x480 drawing buffer so the two capture sizes differ.
Demo.configure(): {
    isOverlayToggleHintVisible: boolean;
    displaySize: Vector2i;
    drawingBufferSize: Vector2i;
}
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, 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 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 panel 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 already includes a 640x480 drawing buffer - an // internal 2x upscale so the picture looks crisp on screen. Here we declare // both sizes ourselves so the demo is easy to read: displaySize is the grid // the game draws on (320x240 pixels). 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),
// Now we ask for a bigger drawing buffer on purpose. This is the "scaled // display" case: the game still draws on a 320x240 grid, but the engine keeps // a 640x480 picture for the screen. BT.outputSize is that 640x480 size, so a // capture with size 'output' is 640x480 and a capture with size 'display' is // 320x240. Declaring it explicitly keeps the choice visible instead of // depending on engine defaults. drawingBufferSize: Vector2idrawingBufferSize: new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(640, 480),
}; } // Sets up the color palette and installs the shared UI theme colors. The buttons // themselves are declared every frame in render(). async Demo.init(): unknowninit() { // Step 1: build the color palette. 256 slots is the 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;
    ... 124 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). // update() replaces them 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, buttons, 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;
    ... 124 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 the status message, and updates the animated // stripe colors in the palette. Runs at a fixed rate (60 times per second). Demo.update(): voidupdate() { // 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 tick counter so the animation 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 go. 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 the // reserved palette slots. render() then just uses the slot numbers. for (let let i: numberi = 0; let i: numberi < 6; let i: numberi++) { // phase is an angle-like value from 0 to 359 that moves as tick increases. // Each stripe adds i * 20 so neighboring stripes do not look identical. The % // 360 (remainder after dividing by 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: the same sine wave shifted 120 degrees around the color // wheel. Shifting each channel separately makes R, G, and B cycle out of // step, which creates the 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() // reads 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));
} } // Draws the animated test pattern and the UI kit panel with the buttons and the // capture status. Runs once per screen refresh. Demo.render(): voidrender() { // Ask the engine how wide and tall our game 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;
    ... 124 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 screen 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;
    ... 124 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 use the // slot number. 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 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;
    ... 124 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);
} // A grid of single white pixels every 20 pixels, so you can check 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;
    ... 124 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;
    ... 124 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;
    ... 124 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;
    ... 124 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 the bottom-left because the // engine keeps that 17x13 corner tappable for toggling the stats overlay, and we // do not want the two to fight over taps; the top-left has no such conflict. 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 made 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 capture 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(): voidsaveFrame(); } // The Size button flips between the two FrameCaptureSize choices. Its label shows // the choice that is active right now, so you can see what the next capture will // use. The label changes, so the button needs a fixed id to stay the same widget. if (import uiui.button(`Size: ${this.Demo.captureSize: stringcaptureSize} (Z)`, { key: stringkey: 'KeyZ', id: stringid: 'size' })) { this.Demo.captureSize: stringcaptureSize = this.Demo.captureSize: stringcaptureSize === const SIZE_OUTPUT: "output"SIZE_OUTPUT ? const SIZE_DISPLAY: "display"SIZE_DISPLAY : const SIZE_OUTPUT: "output"SIZE_OUTPUT; } // The Capture Blob button calls BT.captureFrame() instead of BT.downloadFrame(). // Nothing is saved to disk - the demo gets the PNG data and reads it itself. if (import uiui.button('Capture Blob (B)', { key: stringkey: 'KeyB' }) && !this.Demo.capturing: booleancapturing) { this.Demo.captureBlob(): voidcaptureBlob(); } // One status row below the buttons. 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 capture 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 capture 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. They are engine defaults (every game gets them, see // HardwareSettings.isFrameCaptureShortcutEnabled), not something this demo // wires up itself; it has no button of its own to advertise them, so this // hint is the most relevant place. const const hint1: "Saves the current frame"hint1 = 'Saves the current frame'; const const hint2: "" | "(dev: F9 copies, Shift+F9 saves)"hint2 =
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.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
? '(dev: F9 copies, Shift+F9 saves)' : '';
import uiui.label(const hint1: "Saves the current frame"hint1, { color: stringcolor: 'dim' }); import uiui.label(const hint2: "" | "(dev: F9 copies, Shift+F9 saves)"hint2, { color: stringcolor: 'dim' }); } // Frame counter so you can tell consecutive screenshots apart. import uiui.kv('FRAME', this.Demo.tick: numbertick); import uiui.end(); } // Starts a PNG download of the current frame and remembers the outcome so render() // can show a status message for a few seconds. Demo.saveFrame(): voidsaveFrame() { // Mark the capture as "in flight" so the buttons ignore further presses and // render() shows the "Capturing..." status row. this.Demo.capturing: booleancapturing = true; // Remember which size we asked for. You may press Z while we wait. const const size: stringsize = this.Demo.captureSize: stringcaptureSize; // 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;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.downloadFrame: (filename?: string, options?: FrameCaptureOptions) => 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. Takes the same `options` as {@link BT.captureFrame } .
@since1.0.3@changed1.7.1 Accepts `{ size: 'display' }` to download at logical `BT.displaySize`.@paramfilename - Target download filename.@paramoptions - Capture options; `size` defaults to `'output'`.@exampleawait BT.downloadFrame(); await BT.downloadFrame('screenshot-001.png'); await BT.downloadFrame('exact.png', { size: 'display' });
downloadFrame
('blit386-capture.png', { size: stringsize })
.then(() => { // Success: show a friendly message in green. this.Demo.showMessage(text: any, color: any): voidshowMessage(`Saved: blit386-capture.png (${const size: stringsize})`, 'accent'); return null; }) .catch((err: anyerr) => { // Failure: show the error in orange and log the details for developers. this.Demo.showMessage(text: any, color: any): voidshowMessage(`Error: ${err: anyerr.message}`, 'warm'); console.error('[Demo] Capture failed:', err: anyerr); }); } // Captures the current frame as a Blob with BT.captureFrame() and reads the Blob's // byte count and picture dimensions for the status row. Demo.captureBlob(): voidcaptureBlob() { // Same "in flight" flag as saveFrame(), so only one capture runs at a time. this.Demo.capturing: booleancapturing = true; // Remember which size we asked for. You may press Z while we wait. const const size: stringsize = this.Demo.captureSize: stringcaptureSize; // BT.captureFrame() resolves with a Blob: the PNG file held in memory. The // options object picks the size, exactly like BT.downloadFrame().
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.captureFrame: (options?: FrameCaptureOptions) => Promise<Blob>
Captures the next rendered frame as a PNG blob. The returned promise resolves after the next render pass has completed. By default the PNG matches {@link BT.outputSize } (`drawingBufferSize ?? displaySize`), including display-tier post-process effects. Pass `{ size: 'display' }` for an exact 1:1 frame at logical {@link BT.displaySize } , resolved straight from the palette-indexed scene buffer - no upscale and no display-tier effects (scanlines, vignette, bloom) - which is what tests and agents want for pixel comparisons. The display-size capture has its own pending slot, so it never contends with the F9 / Shift+F9 dev shortcuts.
@since1.0.3@changed1.7.1 Accepts `{ size: 'display' }` to capture at logical `BT.displaySize`.@changed1.8.0 While `BT.renderAt` holds the loop stopped, re-renders the seeked frame to capture it instead of waiting for the next loop frame. Runs after every `BT.renderAt` called before it, so an un-awaited seek's frame is the one captured.@paramoptions - Capture options; `size` defaults to `'output'`.@returnsPNG image data for the captured frame.@exampleconst blob = await BT.captureFrame(); const url = URL.createObjectURL(blob); console.log('Captured frame:', url); // Exact game pixels, one PNG pixel per logical pixel: const exact = await BT.captureFrame({ size: 'display' });
captureFrame
({ size: stringsize })
.then(async (blob: anyblob) => { // createImageBitmap() decodes the PNG so we can read its real width and // height. This proves the Blob holds the size we asked for. const const image: anyimage = await createImageBitmap(blob: anyblob); const const message: stringmessage = `Blob ${const size: stringsize}: ${const image: anyimage.width}x${const image: anyimage.height}, ${blob: anyblob.size} bytes`; // Free the decoded picture; we only needed its dimensions. const image: anyimage.close(); this.Demo.showMessage(text: any, color: any): voidshowMessage(const message: stringmessage, 'accent'); return null; }) .catch((err: anyerr) => { // Failure: show the error in orange and log the details for developers. this.Demo.showMessage(text: any, color: any): voidshowMessage(`Error: ${err: anyerr.message}`, 'warm'); console.error('[Demo] Blob capture failed:', err: anyerr); }); } // Ends a capture: shows a status message for three seconds and unlocks the buttons. // `color` is a UI color role: 'accent' (green) for success, 'warm' (orange) for an // error. Demo.showMessage(text: any, color: any): voidshowMessage(text: anytext, color: anycolor) { this.Demo.lastCaptureMessage: stringlastCaptureMessage = text: anytext; this.Demo.lastCaptureColor: stringlastCaptureColor = color: anycolor; this.Demo.messageTimer: numbermessageTimer = 180; // 3 seconds at 60 ticks per second this.Demo.capturing: booleancapturing = false; } } // 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 DemoDemo);