// 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.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 Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32, class Rect2iInteger rectangle for pixel-perfect bounds and regions.
Used throughout the engine for sprite regions, display-space bounds, and
zero-allocation geometry helpers. Both convenience getters and allocation-free
`*To()` helpers are provided so callers can choose between readability and
hot-path efficiency.Rect2i, class Vector2iInteger 2D vector for pixel-perfect positioning.
Used for points, sizes, directions, and camera offsets throughout the engine.
The API includes both allocation-free `*To()` / `*InPlace()` variants and
convenience methods that return new vectors.Vector2i } from 'blit386';
import { import 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 DemoImage 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.Demo {
// palette holds the list of colors the engine uses for drawing.
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = 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.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): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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().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 | nullpalette = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteCreate: (size?: number) => PaletteCreates a standalone palette instance.paletteCreate(256);
// Store the static scene colors we always need.
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_WHITE: 1C_WHITE, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(255, 255, 255)); // pure white
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_BG: 2C_BG, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.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: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_STRIPE_0: 10C_STRIPE_0 + let i: numberi, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.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: Palettepalette);
// 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) => voidStores the active engine palette.
Use this to swap the **entire palette** (e.g. switch between a day and night
theme). After this call the renderer uploads the new palette uniform on the
next frame.
**Palette-value swap (change what a slot looks like):** mutate the live
{@link
BT.palette
}
in place with `palette.set(slot, newColor)`. The renderer
uploads dirty slots on the next frame; no `paletteSet()` or
{@link
BT.spritesRefresh
}
needed.
**Palette-layout swap (same colors, different slot positions):** build a new
palette with the same colors at new indices, call `paletteSet()`, then call
{@link
BT.spritesRefresh
}
so every sprite sheet re-maps its original RGBA
pixels against the new slot layout.paletteSet(this.Demo.palette: Palettepalette);
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(): voidAdvances 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 | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_STRIPE_0: 10C_STRIPE_0 + let i: numberi, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.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(): voidRenders 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: Vector2iActive logical render resolution in pixels.
This is the game/simulation coordinate space configured by the demo, not
the canvas element's CSS size. Each read returns a clone.displaySize;
// 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) => voidSets the frame clear color using a palette index.
The renderer uses this color when clearing the full display at the start
of the next frame.clear(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) => voidDraws a filled rectangle.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(0, const y: numbery, const screen: Vector2iscreen.Vector2i.x: numberHorizontal 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: numberHorizontal component (defaults to 0).x; let x: numberx += 20) {
for (let let y: numbery = 0; let y: numbery < const screen: Vector2iscreen.Vector2i.y: numberVertical 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) => voidDraws 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)`.drawPixel(new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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) => voidDraws an unfilled rectangle outline.drawRect(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(0, 0, const screen: Vector2iscreen.Vector2i.x: numberHorizontal component (defaults to 0).x, const screen: Vector2iscreen.Vector2i.y: numberVertical 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: numberHorizontal component (defaults to 0).x / 2);
const const cy: anycy = Math.floor(const screen: Vector2iscreen.Vector2i.y: numberVertical 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) => voidDraws a pixel-perfect line between two points.
Uses rasterized line drawing without antialiasing.drawLine(new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const cx: anycx - 20, const cy: anycy), new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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) => voidDraws a pixel-perfect line between two points.
Uses rasterized line drawing without antialiasing.drawLine(new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const cx: anycx, const cy: anycy - 20), new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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(): voidStarts 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: booleanWhether 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.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(): voidStarts 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.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.bootstrap(class DemoImage 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.Demo);