// Pixel Art Demo - draw tiny pictures from number grids and from math patterns.
// @description Draw tiny pictures from number grids and from math patterns, one pixel at a time, the way old games did.
//
// Written for young learners (around 12).
//
// We learned about the demo lifecycle, coordinates, and clearing the screen in the Basics demo:
// https://demos.blit386.dev/basics
//
// Prerequisites: https://demos.blit386.dev/basics , https://demos.blit386.dev/primitives ,
// https://demos.blit386.dev/colors
// Live version: https://demos.blit386.dev/pixel-art
//
// This demo shows:
//   - A 2D array (grid) of small numbers that stand for colors, like a paint-by-number on graph paper
//   - Nested loops: an outer loop for each row and an inner loop for each column, like reading a book
//     line by line and word by word
//   - How grid row/column indices turn into x/y positions on the screen
//   - Why we sometimes draw many BT.drawPixel calls in a small block to make one "big" chunky pixel
//   - A pattern drawn only with loops and math (no picture array), with colors that move over time
//
// The section captions above each artwork are drawn with ui.caption() from the shared UI kit
// (src/shared/ui.js), in the same amber header color every other demo in the series uses.
// The artwork itself is the lesson and is still drawn pixel by pixel below.

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 } from './shared/ui.js'; /** @typedef {import('blit386').IBTDemo} IBTDemo */ /** @typedef {import('blit386').HardwareSettings} HardwareSettings */ /** @typedef {import('blit386').Palette} Palette */ // Every color used for drawing gets a numbered slot in the palette (like a numbered paint jar). // Index 0 is always transparent. Custom colors start at 1. const const C_WHITE: 1C_WHITE = 1; // Pure white: font base color const const C_BG: 2C_BG = 2; // Deep gray-blue: fills the screen background const const C_TAG: 3C_TAG = 3; // Pale blue-white: only colors the overlay bar and timing-chart marks (see configure()) // Heart sprite colors (used by HEART_PALETTE_MAP below). const const C_HEART_OUTLINE: 5C_HEART_OUTLINE = 5; // Dark red: the heart's outline pixels const const C_HEART_FILL: 6C_HEART_FILL = 6; // Bright red: the heart's interior pixels // Checker pattern colors: these are updated every frame in update() so the colors move. const const C_CHECKER_A: 10C_CHECKER_A = 10; // Dynamic: lerp between red and yellow const const C_CHECKER_B: 11C_CHECKER_B = 11; // Dynamic: lerp between blue and cyan // HEART_GRID is an 8 by 8 table of small integers. // Think of graph paper: each cell is one tiny part of the picture. // 0 means "leave empty" (we skip drawing there so the background shows through). // 1 and 2 pick palette entries from HEART_PALETTE_MAP below (outline and fill). const const HEART_GRID: {}HEART_GRID = [ [0, 1, 1, 0, 0, 1, 1, 0], [1, 2, 1, 1, 1, 2, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1], [0, 1, 1, 1, 1, 1, 1, 0], [0, 1, 1, 1, 1, 1, 1, 0], [0, 0, 1, 1, 1, 1, 0, 0], [0, 0, 0, 1, 1, 0, 0, 0], ]; // HEART_PALETTE_MAP lines up with the numbers in the grid. // paletteMap[1] is the palette index for code 1, paletteMap[2] for code 2. // Index 0 is null because 0 means "no paint" in the grid (we skip those cells). // These are PALETTE INDEX NUMBERS, not Color32 objects. The palette already knows the actual colors. const const HEART_PALETTE_MAP: {}HEART_PALETTE_MAP = [null, const C_HEART_OUTLINE: 5C_HEART_OUTLINE, const C_HEART_FILL: 6C_HEART_FILL]; /** * Looks up the palette index for a paint code. * The grid only uses small integers we authored, not user input. * This is the one place that validates grid codes: 0 (empty) and anything * outside the map both come back as null, so callers only need one check. * * @param {(number | null)[]} paletteMap - Array of palette indices (null = transparent). * @param {number} code - The paint code from the grid. * @returns {number | null} A palette index, or null if the code means "empty." */ function function indexFromPaletteMap(paletteMap: (number | null)[], code: number): number | null
Looks up the palette index for a paint code. The grid only uses small integers we authored, not user input. This is the one place that validates grid codes: 0 (empty) and anything outside the map both come back as null, so callers only need one check.
@parampaletteMap - Array of palette indices (null = transparent).@paramcode - The paint code from the grid.@returnsA palette index, or null if the code means "empty."
indexFromPaletteMap
(paletteMap: {}
- Array of palette indices (null = transparent).
@parampaletteMap - Array of palette indices (null = transparent).
paletteMap
, code: number
- The paint code from the grid.
@paramcode - The paint code from the grid.
code
) {
if (code: number
- The paint code from the grid.
@paramcode - The paint code from the grid.
code
< 1 || code: number
- The paint code from the grid.
@paramcode - The paint code from the grid.
code
>= paletteMap: {}
- Array of palette indices (null = transparent).
@parampaletteMap - Array of palette indices (null = transparent).
paletteMap
.length) {
return null; } return paletteMap: {}
- Array of palette indices (null = transparent).
@parampaletteMap - Array of palette indices (null = transparent).
paletteMap
[code: number
- The paint code from the grid.
@paramcode - The paint code from the grid.
code
];
} /** * Teaches pixel grids, nested loops, screen mapping, and a tiny procedural pattern. * * @implements {IBTDemo} */ class class Demo
Teaches pixel grids, nested loops, screen mapping, and a tiny procedural pattern.
@implementsIBTDemo
Demo
{
// animTime counts seconds of game time if every update tick is exactly 1/60 of a second. // We only change it inside update(), so it stays smooth even when render() runs at odd rates. Demo.animTime: numberanimTime = 0; // palette holds all the colors this demo uses. /** @type {Palette | null} */ Demo.palette: Palette | null
@type{Palette | null}
palette
= null;
// Where the shared UI theme colors landed in the palette, filled by applyTheme() in // init(). The UI kit draws the section captions with these slots. Demo.theme: nulltheme = null; /** * Optional engine settings. We keep the default 320x240 screen and ask for 16 * palette swatches per row so the overlay grid lines up with the 8-cell-wide heart art * (two grid columns per art cell gives comfortable visual alignment). * * @returns {Partial<HardwareSettings>} */ Demo.configure(): Partial<HardwareSettings>
Optional engine settings. We keep the default 320x240 screen and ask for 16 palette swatches per row so the overlay grid lines up with the 8-cell-wide heart art (two grid columns per art cell gives comfortable visual alignment).
@returns
configure
() {
return { isOverlayPaletteEnabled: booleanisOverlayPaletteEnabled: true, overlayPaletteColumns: numberoverlayPaletteColumns: 16, isOverlayVisibleAtStart: booleanisOverlayVisibleAtStart: true,
overlayStyle: {
    barPaletteIndex: number;
    textPaletteIndex: number;
    gapPaletteIndex: number;
}
overlayStyle
: {
barPaletteIndex: numberbarPaletteIndex: const C_TAG: 3C_TAG, textPaletteIndex: numbertextPaletteIndex: const C_BG: 2C_BG, gapPaletteIndex: numbergapPaletteIndex: const C_BG: 2C_BG, },
overlayTimingChartStyle: {
    updateBarPaletteIndex: number;
    renderBarPaletteIndex: number;
    warningPaletteIndex: number;
    errorPaletteIndex: number;
    tagPaletteIndex: number;
}
overlayTimingChartStyle
: {
updateBarPaletteIndex: numberupdateBarPaletteIndex: const C_BG: 2C_BG, renderBarPaletteIndex: numberrenderBarPaletteIndex: const C_TAG: 3C_TAG, warningPaletteIndex: numberwarningPaletteIndex: const C_TAG: 3C_TAG, errorPaletteIndex: numbererrorPaletteIndex: 4, // Slot 4 has no named constant - this demo leaves it unassigned. tagPaletteIndex: numbertagPaletteIndex: const C_BG: 2C_BG, }, }; } /** * Sets up the palette. * * @returns {Promise<boolean>} */ async Demo.init(): Promise<boolean>
Sets up the palette.
@returns
init
() {
// Set up the color palette // Think of this as laying out paint on an artist's palette tray before starting a painting. // Every color we might use gets a number. We set them all up before drawing begins. // Thirty-two slots: the artwork only uses indices 1 through 11, and the shared UI // theme takes 12 more (slots 20-31, installed below), so 32 fits everything. // configure() sets overlayPaletteColumns: 16 so the overlay swatch grid matches the // 8-column-wide heart grid (two art cells per overlay column). 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
(32);
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
(28, 32, 48)); // deep gray-blue background
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_TAG: 3C_TAG, 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
(200, 210, 230)); // pale blue-white overlay bar / chart accent
// Heart sprite colors. 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_HEART_OUTLINE: 5C_HEART_OUTLINE, 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
(110, 10, 30)); // dark red outline
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_HEART_FILL: 6C_HEART_FILL, 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
(230, 55, 75)); // bright red fill
// Pre-fill dynamic checker colors with a starting value. // update() will overwrite these on the very first tick. 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_CHECKER_A: 10C_CHECKER_A, 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, 0, 0)); // start as red
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_CHECKER_B: 11C_CHECKER_B, 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
(0, 0, 255)); // start as blue
// The overlay styles in configure() reuse C_TAG and C_BG, both set above. // Install the shared UI theme the kit draws the section captions with. // It writes 12 colors starting at the slot we pass - slots 20..31 here, which // stay clear of the artwork colors (1-11) and fill the top of our 32-slot // palette exactly. Must happen before BT.paletteSet() below. this.Demo.theme: nulltheme = import applyThemeapplyTheme(this.Demo.palette: Palette
@type{Palette | null}
palette
, 20);
// Tell the engine to use this palette for all drawing.
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; } /** * Fixed-step clock. Advances animTime and updates the animated checker colors in the palette. * See the Basics article for why update() and render() are separate steps: * https://demos.blit386.dev/basics */ Demo.update(): void
Fixed-step clock. Advances animTime and updates the animated checker colors in the palette. See the Basics article for why update() and render() are separate steps: https://demos.blit386.dev/basics
update
() {
// Add one tick's worth of time. If targetFPS is 60, each tick is about 1/60 second. this.Demo.animTime: numberanimTime +=
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.deltaSeconds: number
Fixed-step seconds per update tick. Equivalent to `1 / BT.targetFPS` when `BT.targetFPS` is finite and positive. Falls back to `1 / 60` when target FPS is non-finite or non-positive.
@since1.0.4@returnsSeconds advanced by one fixed update tick.
deltaSeconds
;
// Update the checker pattern colors // The checker squares use "lerp" (short for linear interpolation - smoothly blending // between two colors). wave goes from 0 to 1 and back using Math.sin. // At wave=0 colorA is red; at wave=1 it is yellow. At wave=0 colorB is blue; at 1 it is cyan. // Both colors shift at the same time but in opposite directions, so they always contrast. const const wave: numberwave = (Math.sin(this.Demo.animTime: numberanimTime * 2) + 1) * 0.5; 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_CHECKER_A: 10C_CHECKER_A, class Color32
Mutable 32-bit RGBA color value with 8-bit channels.
@since0.1.0
Color32
.Color32.red: Color32
Pure red color (255, 0, 0, 255). Cached frozen singleton - do not modify.
@returnsThe shared red Color32 instance.
red
.Color32.lerp(other: Color32, t: number): Color32
Linearly interpolates between this color and another. Useful for color transitions and gradients.
@paramother - Target color to blend toward.@paramt - Interpolation factor (0.0 = this color, 1.0 = other color).@returnsNew color blended between this and other.
lerp
(class Color32
Mutable 32-bit RGBA color value with 8-bit channels.
@since0.1.0
Color32
.Color32.yellow: Color32
Yellow color (255, 255, 0, 255). Cached frozen singleton - do not modify.
@returnsThe shared yellow Color32 instance.
yellow
, const wave: numberwave));
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_CHECKER_B: 11C_CHECKER_B, class Color32
Mutable 32-bit RGBA color value with 8-bit channels.
@since0.1.0
Color32
.Color32.blue: Color32
Pure blue color (0, 0, 255, 255). Cached frozen singleton - do not modify.
@returnsThe shared blue Color32 instance.
blue
.Color32.lerp(other: Color32, t: number): Color32
Linearly interpolates between this color and another. Useful for color transitions and gradients.
@paramother - Target color to blend toward.@paramt - Interpolation factor (0.0 = this color, 1.0 = other color).@returnsNew color blended between this and other.
lerp
(class Color32
Mutable 32-bit RGBA color value with 8-bit channels.
@since0.1.0
Color32
.Color32.cyan: Color32
Cyan color (0, 255, 255, 255). Cached frozen singleton - do not modify.
@returnsThe shared cyan Color32 instance.
cyan
, 1 - const wave: numberwave));
} /** * Draws the whole frame: section labels, the number-grid sprite, and the checker pattern. * FPS and tick stats live in the engine overlay (toggle with Backquote), not on the canvas. */ Demo.render(): void
Draws the whole frame: section labels, the number-grid sprite, and the checker pattern. FPS and tick stats live in the engine overlay (toggle with Backquote), not on the canvas.
render
() {
// Clear to the deep gray-blue background so light pixel art pops.
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);
this.Demo.renderHeartSection(): void
Labels and draws the 8x8 heart on the left.
renderHeartSection
();
// Checkerboard below, with colors that shift using animTime. this.Demo.renderCheckerPatternSection(): void
Draws a checkerboard using only math inside nested loops - no picture array. Colors slide around based on animTime (updated in update()) so you can see the clock moving.
renderCheckerPatternSection
();
} /** * Turns a 2D number grid into chunky pixels on screen. * * Nested loops: the outer `for` walks row = 0, 1, 2... (which horizontal strip of the grid). * The inner `for` walks col = 0, 1, 2... inside that row (like reading left to right). * That is the usual "loop inside a loop" mental model: finish one full row before moving down. * * BT.drawPixel() paints exactly one screen cell. To make each design cell bigger, we use two * more small loops (dx and dy) that stamp a scale-by-scale block of pixels. Another valid way * is BT.drawRectFill() with width and height equal to scale - same math, one call per cell. * * @param {number[][]} grid - Rows of paint codes; grid[row][col] matches graph-paper rows/columns. * @param {(number | null)[]} paletteMap - paletteMap[code] is the palette index, or null to skip. * @param {number} originX - Left edge where column 0 should appear on screen. * @param {number} originY - Top edge where row 0 should appear on screen. * @param {number} scale - How many screen pixels wide/tall each grid cell becomes. */ Demo.drawGridWithScaledPixels(grid: number[][], paletteMap: (number | null)[], originX: number, originY: number, scale: number): void
Turns a 2D number grid into chunky pixels on screen. Nested loops: the outer `for` walks row = 0, 1, 2... (which horizontal strip of the grid). The inner `for` walks col = 0, 1, 2... inside that row (like reading left to right). That is the usual "loop inside a loop" mental model: finish one full row before moving down. BT.drawPixel() paints exactly one screen cell. To make each design cell bigger, we use two more small loops (dx and dy) that stamp a scale-by-scale block of pixels. Another valid way is BT.drawRectFill() with width and height equal to scale - same math, one call per cell.
@paramgrid - Rows of paint codes; grid[row][col] matches graph-paper rows/columns.@parampaletteMap - paletteMap[code] is the palette index, or null to skip.@paramoriginX - Left edge where column 0 should appear on screen.@paramoriginY - Top edge where row 0 should appear on screen.@paramscale - How many screen pixels wide/tall each grid cell becomes.
drawGridWithScaledPixels
(grid: {}
- Rows of paint codes; grid[row][col] matches graph-paper rows/columns.
@paramgrid - Rows of paint codes; grid[row][col] matches graph-paper rows/columns.
grid
, paletteMap: {}
- paletteMap[code] is the palette index, or null to skip.
@parampaletteMap - paletteMap[code] is the palette index, or null to skip.
paletteMap
, originX: number
- Left edge where column 0 should appear on screen.
@paramoriginX - Left edge where column 0 should appear on screen.
originX
, originY: number
- Top edge where row 0 should appear on screen.
@paramoriginY - Top edge where row 0 should appear on screen.
originY
, scale: number
- How many screen pixels wide/tall each grid cell becomes.
@paramscale - How many screen pixels wide/tall each grid cell becomes.
scale
) {
// Outer loop: which row of the design (top row is row 0). for (let let row: numberrow = 0; let row: numberrow < grid: {}
- Rows of paint codes; grid[row][col] matches graph-paper rows/columns.
@paramgrid - Rows of paint codes; grid[row][col] matches graph-paper rows/columns.
grid
.length; let row: numberrow++) {
// One row of the picture as a normal JavaScript array. const const rowCodes: anyrowCodes = grid: {}
- Rows of paint codes; grid[row][col] matches graph-paper rows/columns.
@paramgrid - Rows of paint codes; grid[row][col] matches graph-paper rows/columns.
grid
[let row: numberrow];
// Inner loop: move across that row from left to right. for (let let col: numbercol = 0; let col: numbercol < const rowCodes: anyrowCodes.length; let col: numbercol++) { // Read the paint code for this cell, like looking up a coordinate on graph paper. const const code: anycode = const rowCodes: anyrowCodes[let col: numbercol]; // Ask the helper which palette index this code means. It answers null for // code 0 ("no ink here") and for any code outside the map, so this single // check is all the guarding we need - skip and the background stays visible. const const paletteIndex: number | nullpaletteIndex = function indexFromPaletteMap(paletteMap: (number | null)[], code: number): number | null
Looks up the palette index for a paint code. The grid only uses small integers we authored, not user input. This is the one place that validates grid codes: 0 (empty) and anything outside the map both come back as null, so callers only need one check.
@parampaletteMap - Array of palette indices (null = transparent).@paramcode - The paint code from the grid.@returnsA palette index, or null if the code means "empty."
indexFromPaletteMap
(paletteMap: {}
- paletteMap[code] is the palette index, or null to skip.
@parampaletteMap - paletteMap[code] is the palette index, or null to skip.
paletteMap
, const code: anycode);
if (const paletteIndex: number | nullpaletteIndex === null) { continue; } // Map grid (col, row) to the top-left corner of this cell on the virtual screen. // Column affects x (sideways), row affects y (down the screen). const const baseX: numberbaseX = originX: number
- Left edge where column 0 should appear on screen.
@paramoriginX - Left edge where column 0 should appear on screen.
originX
+ let col: numbercol * scale: number
- How many screen pixels wide/tall each grid cell becomes.
@paramscale - How many screen pixels wide/tall each grid cell becomes.
scale
;
const const baseY: numberbaseY = originY: number
- Top edge where row 0 should appear on screen.
@paramoriginY - Top edge where row 0 should appear on screen.
originY
+ let row: numberrow * scale: number
- How many screen pixels wide/tall each grid cell becomes.
@paramscale - How many screen pixels wide/tall each grid cell becomes.
scale
;
// Tiny inner loops fill a scale-by-scale square with individual drawPixel calls. for (let let dy: numberdy = 0; let dy: numberdy < scale: number
- How many screen pixels wide/tall each grid cell becomes.
@paramscale - How many screen pixels wide/tall each grid cell becomes.
scale
; let dy: numberdy++) {
for (let let dx: numberdx = 0; let dx: numberdx < scale: number
- How many screen pixels wide/tall each grid cell becomes.
@paramscale - How many screen pixels wide/tall each grid cell becomes.
scale
; let dx: numberdx++) {
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
(const baseX: numberbaseX + let dx: numberdx, const baseY: numberbaseY + let dy: numberdy), const paletteIndex: numberpaletteIndex);
} } } } } /** * Labels and draws the 8x8 heart on the left. */ Demo.renderHeartSection(): void
Labels and draws the 8x8 heart on the left.
renderHeartSection
() {
// Print the section caption with ui.caption() from the shared UI kit - the same // widget every demo in the series uses, so all captions look identical everywhere. import uiui.caption(12, 48, 'Heart 8x8 (number grid, no external bitmap used)'); // scale = 4 makes the 8-cell-wide picture use 32 virtual pixels of width. const const scale: 4scale = 4; const const originX: 12originX = 12; const const originY: 63originY = 63; this.Demo.drawGridWithScaledPixels(grid: number[][], paletteMap: (number | null)[], originX: number, originY: number, scale: number): void
Turns a 2D number grid into chunky pixels on screen. Nested loops: the outer `for` walks row = 0, 1, 2... (which horizontal strip of the grid). The inner `for` walks col = 0, 1, 2... inside that row (like reading left to right). That is the usual "loop inside a loop" mental model: finish one full row before moving down. BT.drawPixel() paints exactly one screen cell. To make each design cell bigger, we use two more small loops (dx and dy) that stamp a scale-by-scale block of pixels. Another valid way is BT.drawRectFill() with width and height equal to scale - same math, one call per cell.
@paramgrid - Rows of paint codes; grid[row][col] matches graph-paper rows/columns.@parampaletteMap - paletteMap[code] is the palette index, or null to skip.@paramoriginX - Left edge where column 0 should appear on screen.@paramoriginY - Top edge where row 0 should appear on screen.@paramscale - How many screen pixels wide/tall each grid cell becomes.
drawGridWithScaledPixels
(const HEART_GRID: {}HEART_GRID, const HEART_PALETTE_MAP: {}HEART_PALETTE_MAP, const originX: 12originX, const originY: 63originY, const scale: 4scale);
} /** * Draws a checkerboard using only math inside nested loops - no picture array. * Colors slide around based on animTime (updated in update()) so you can see the clock moving. */ Demo.renderCheckerPatternSection(): void
Draws a checkerboard using only math inside nested loops - no picture array. Colors slide around based on animTime (updated in update()) so you can see the clock moving.
renderCheckerPatternSection
() {
import uiui.caption(12, 97, 'Checkerboard (loops + math, no grid array)'); // How many squares along each side. const const cells: 8cells = 8; // Pixel size of one checker square on the virtual 320x240 surface. const const cellSize: 10cellSize = 10; // Top-left corner of the whole checker region. const const startX: 12startX = 12; const const startY: 112startY = 112; // Outer loop picks the row of squares; inner loop picks the column - same nested idea as the art. for (let let row: numberrow = 0; let row: numberrow < const cells: 8cells; let row: numberrow++) { for (let let col: numbercol = 0; let col: numbercol < const cells: 8cells; let col: numbercol++) { // Checker rule: neighbors must look different, like a chessboard. // % is "remainder after division": (row + col) % 2 is 0 for even sums, 1 for odd sums. // Adding row and col flips the remainder on every step to the right or down, so no two // touching squares share the same color. update() already refreshed C_CHECKER_A/B. const const fill: 10 | 11fill = (let row: numberrow + let col: numbercol) % 2 === 0 ? const C_CHECKER_A: 10C_CHECKER_A : const C_CHECKER_B: 11C_CHECKER_B; // Rect2i(x, y, width, height) describes a solid rectangle in pixel space. const const x: numberx = const startX: 12startX + let col: numbercol * const cellSize: 10cellSize; const const y: numbery = const startY: 112startY + let row: numberrow * const cellSize: 10cellSize;
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
(const x: numberx, const y: numbery, const cellSize: 10cellSize, const cellSize: 10cellSize), const fill: 10 | 11fill);
} } } } 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
Teaches pixel grids, nested loops, screen mapping, and a tiny procedural pattern.
@implementsIBTDemo
Demo
);