// 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.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 } 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 | nullLooks 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.indexFromPaletteMap(paletteMap: {}- Array of palette indices (null = transparent).paletteMap, code: number- The paint code from the grid.code) {
if (code: number- The paint code from the grid.code < 1 || code: number- The paint code from the grid.code >= paletteMap: {}- Array of palette indices (null = transparent).paletteMap.length) {
return null;
}
return paletteMap: {}- Array of palette indices (null = transparent).paletteMap[code: number- The paint code from the grid.code];
}
/**
* Teaches pixel grids, nested loops, screen mapping, and a tiny procedural pattern.
*
* @implements {IBTDemo}
*/
class class DemoTeaches pixel grids, nested loops, screen mapping, and a tiny procedural pattern.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 | nullpalette = 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).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.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 | 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(32);
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(28, 32, 48)); // deep gray-blue background
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_TAG: 3C_TAG, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(200, 210, 230)); // pale blue-white overlay bar / chart accent
// Heart sprite colors.
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_HEART_OUTLINE: 5C_HEART_OUTLINE, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(110, 10, 30)); // dark red outline
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_HEART_FILL: 6C_HEART_FILL, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.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: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_CHECKER_A: 10C_CHECKER_A, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(255, 0, 0)); // start as red
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_CHECKER_B: 11C_CHECKER_B, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.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: Palettepalette, 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) => 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;
}
/**
* 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(): voidFixed-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/basicsupdate() {
// 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: numberFixed-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.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 | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_CHECKER_A: 10C_CHECKER_A, class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32.Color32.red: Color32Pure red color (255, 0, 0, 255).
Cached frozen singleton - do not modify.red.Color32.lerp(other: Color32, t: number): Color32Linearly interpolates between this color and another.
Useful for color transitions and gradients.lerp(class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32.Color32.yellow: Color32Yellow color (255, 255, 0, 255).
Cached frozen singleton - do not modify.yellow, const wave: numberwave));
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_CHECKER_B: 11C_CHECKER_B, class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32.Color32.blue: Color32Pure blue color (0, 0, 255, 255).
Cached frozen singleton - do not modify.blue.Color32.lerp(other: Color32, t: number): Color32Linearly interpolates between this color and another.
Useful for color transitions and gradients.lerp(class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32.Color32.cyan: Color32Cyan color (0, 255, 255, 255).
Cached frozen singleton - do not modify.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(): voidDraws 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) => 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);
this.Demo.renderHeartSection(): voidLabels and draws the 8x8 heart on the left.renderHeartSection();
// Checkerboard below, with colors that shift using animTime.
this.Demo.renderCheckerPatternSection(): voidDraws 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): voidTurns 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.drawGridWithScaledPixels(grid: {}- Rows of paint codes; grid[row][col] matches graph-paper rows/columns.grid, paletteMap: {}- paletteMap[code] is the palette index, or null to skip.paletteMap, originX: number- Left edge where column 0 should appear on screen.originX, originY: number- Top edge where row 0 should appear on screen.originY, scale: number- 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.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.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 | nullLooks 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.indexFromPaletteMap(paletteMap: {}- 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.originX + let col: numbercol * scale: number- 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.originY + let row: numberrow * scale: number- 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.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.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) => 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(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(): voidLabels 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): voidTurns 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.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(): voidDraws 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) => 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(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.bootstrap(class DemoTeaches pixel grids, nested loops, screen mapping, and a tiny procedural pattern.Demo);