// Sprites: how to draw images (sprites) on screen using BLIT386.
// @description Draw images from a programmatic sprite sheet, using source rectangles and palette offsets to vary them.
//
// Prerequisites: Basics (https://demos.blit386.dev/basics),
// Primitives (https://demos.blit386.dev/primitives),
// Colors (https://demos.blit386.dev/colors).
// Guide: https://blit386.dev/docs/api/rendering#sprites
//
// A "sprite" is a 2D image used in a game - like a character, a coin, or an enemy.
// In BLIT386, sprites are stored in a "sprite sheet": one big image that
// contains many small sprites arranged in a grid. You draw individual sprites by
// telling the engine which rectangular region (a Rect2i "source rect") to copy.
//
// This demo builds a six-shape sheet on an offscreen canvas, then shows:
// 1. BT.drawSprite() with different source regions (one shape per cell).
// 2. Palette offsets - shifting every pixel index to a different color block.
// 3. Opacity pulsing - rewriting palette alpha slots in update().
//
// Captions and the code panel are drawn with the shared UI kit (src/shared/ui.js), which
// installs its own twelve UI colors high in the palette (slots 240-251) via applyTheme().
//
// In a real project you would load PNGs from disk instead:
// await SpriteSheet.load('/sprites/hero.png')
// await SpriteSheet.loadIndexed('/sprites/hero.png', palette, startSlot)
//
// HOW PALETTE OFFSETS WORK FOR SPRITES:
//
// After calling sheet.indexize(palette), each pixel in the sprite is stored
// as a palette index number. When you draw the sprite:
//
// BT.drawSprite(sheet, src, pos, 0) - uses original colors
// BT.drawSprite(sheet, src, pos, colorCount) - shifts ALL pixel indices up by colorCount
//
// If the original colors are at palette[10..14], offset=5 shifts every pixel
// to use palette[15..19] - a completely different color theme!
// This is how retro games did "team colors" and environmental lighting.
//
// Palette Presets demo explores the palette system in depth:
// https://demos.blit386.dev/palette-presets
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, type Rect2i = Rect2i
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.Rect2i, type SpriteSheet = SpriteSheet
class SpriteSheet
Sprite-sheet wrapper around a loaded image asset.
The class keeps the original image available for CPU-side inspection while
lazily creating and caching a GPU texture for rendering. When possible,
`load()` also pre-decodes the source into an `ImageBitmap` so texture uploads
preserve pixel-art alpha and color values more reliably.
After calling `indexize()`, the sheet stores palette indices rather than RGBA
data. The GPU texture becomes an `r8uint` format uploaded via `writeTexture`.
The original RGBA bytes are retained so `reindexize()` can re-convert without
reloading the image.SpriteSheet, 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 canvasToImagecanvasToImage, import registerCanvasColorsregisterCanvasColors } from './shared/canvas-sprites.js';
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 */
/** @typedef {import('blit386').SpriteSheet} SpriteSheet */
/** @typedef {import('blit386').Rect2i} Rect2i */
// Where in the palette the sprite's original colors start. The sprite uses two colors
// (fill + stroke), and the recolored theme blocks below stack up to about slot 19, so
// everything above that stays free for the shared UI theme (slots 240-251).
const const COLOR_BASE: 10COLOR_BASE = 10;
// Each shape cell in the programmatic sheet is 20x20 pixels.
const const SHAPE_CELL: 20SHAPE_CELL = 20;
const const SHAPE_COLS: 3SHAPE_COLS = 3;
const const SHAPE_ROWS: 2SHAPE_ROWS = 2;
// One name per shape cell, in the same order drawShapeInCell() paints them.
// This single list drives both the sheet builder (how many cells to draw) and the
// captions under each shape in render(). The captions sit 50 pixels apart, so the
// longer names are shortened ('Tri', 'Gem') to keep each label inside its column.
const const SHAPE_NAMES: {}SHAPE_NAMES = ['Square', 'Circle', 'Tri', 'Star', 'Heart', 'Gem'];
// Palette slots of the shared UI theme. applyTheme() in init() writes the twelve UI kit
// colors into slots 240-251 (its default start slot). configure() runs BEFORE init(), so
// the overlay styles below cannot read this.theme yet - these constants spell out where
// each theme color will land once init() runs.
const const UI_BG: 240UI_BG = 240; // 'ui_bg' - deep navy screen background
const const UI_TEXT: 244UI_TEXT = 244; // 'ui_text' - off-white primary text
const const UI_DIM: 245UI_DIM = 245; // 'ui_text_dim' - secondary gray text
const const UI_BAR: 246UI_BAR = 246; // 'ui_bar' - bar chart color
const const UI_INFO: 248UI_INFO = 248; // 'ui_info' - code blue
// The exact two colors drawShapeInCell() paints with (see fill/stroke below).
// The canvas smooths shape edges automatically (anti-aliasing), which blends these two
// colors - and the transparent background - together one pixel at a time. Read back from
// the canvas, that blending produces dozens of barely-different colors along every curve,
// which would each want their own palette slot. Since our palette can only hold 256 colors
// total, and this demo needs room for several recolored copies of the same shape, we snap
// every blended edge pixel back to whichever of these two colors it is closer to. This
// keeps the palette usage small and predictable no matter how smooth the edges look.
// drawShapeInCell() reads these same two Color32 values (via toHex()) for its canvas
// fillStyle/strokeStyle, so the paint colors and the quantization targets can never drift apart.
const const FILL_COLOR: Color32FILL_COLOR = new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(0x55, 0x99, 0xee);
const const STROKE_COLOR: Color32STROKE_COLOR = new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(0xff, 0xff, 0xff);
/**
* Finds whichever of FILL_COLOR/STROKE_COLOR is closer to a given pixel color.
* "Closer" here means smaller distance in RGB space - treating red, green, and blue like
* three coordinates and measuring the straight-line distance between two colors, the same
* way you would measure distance between two points on a map.
*
* @param {number} r
* @param {number} g
* @param {number} b
* @returns {Color32}
*/
function function nearestShapeColor(r: number, g: number, b: number): Color32Finds whichever of FILL_COLOR/STROKE_COLOR is closer to a given pixel color.
"Closer" here means smaller distance in RGB space - treating red, green, and blue like
three coordinates and measuring the straight-line distance between two colors, the same
way you would measure distance between two points on a map.nearestShapeColor(r: numberr, g: numberg, b: numberb) {
const const distanceToFill: numberdistanceToFill = (r: numberr - const FILL_COLOR: Color32FILL_COLOR.Color32.r: numberRed channel (0-255).r) ** 2 + (g: numberg - const FILL_COLOR: Color32FILL_COLOR.Color32.g: numberGreen channel (0-255).g) ** 2 + (b: numberb - const FILL_COLOR: Color32FILL_COLOR.Color32.b: numberBlue channel (0-255).b) ** 2;
const const distanceToStroke: numberdistanceToStroke = (r: numberr - const STROKE_COLOR: Color32STROKE_COLOR.Color32.r: numberRed channel (0-255).r) ** 2 + (g: numberg - const STROKE_COLOR: Color32STROKE_COLOR.Color32.g: numberGreen channel (0-255).g) ** 2 + (b: numberb - const STROKE_COLOR: Color32STROKE_COLOR.Color32.b: numberBlue channel (0-255).b) ** 2;
return const distanceToFill: numberdistanceToFill <= const distanceToStroke: numberdistanceToStroke ? const FILL_COLOR: Color32FILL_COLOR : const STROKE_COLOR: Color32STROKE_COLOR;
}
// Below this alpha, an anti-aliased edge pixel is mostly background - treat it as fully
// transparent instead of a faint smudge of shape color.
const const ALPHA_OPAQUE_THRESHOLD: 128ALPHA_OPAQUE_THRESHOLD = 128;
/**
* Rewrites every pixel of the canvas so it is either fully transparent or one of the exact
* design colors (see FILL_COLOR/STROKE_COLOR above). sheet.indexize() later requires each
* pixel to match a palette entry exactly, so the smooth, blended edges anti-aliasing draws
* must be snapped to flat colors here - otherwise indexize() would reject every blended
* edge pixel as "not in the palette".
*
* @param {OffscreenCanvasRenderingContext2D} ctx
* @param {number} w
* @param {number} h
*/
function function quantizeCanvasToShapeColors(ctx: OffscreenCanvasRenderingContext2D, w: number, h: number): voidRewrites every pixel of the canvas so it is either fully transparent or one of the exact
design colors (see FILL_COLOR/STROKE_COLOR above). sheet.indexize() later requires each
pixel to match a palette entry exactly, so the smooth, blended edges anti-aliasing draws
must be snapped to flat colors here - otherwise indexize() would reject every blended
edge pixel as "not in the palette".quantizeCanvasToShapeColors(ctx: OffscreenCanvasRenderingContext2Dctx, w: numberw, h: numberh) {
const const imageData: anyimageData = ctx: OffscreenCanvasRenderingContext2Dctx.getImageData(0, 0, w: numberw, h: numberh);
const const data: anydata = const imageData: anyimageData.data;
for (let let i: numberi = 0; let i: numberi < const data: anydata.length; let i: numberi += 4) {
if (const data: anydata[let i: numberi + 3] < const ALPHA_OPAQUE_THRESHOLD: 128ALPHA_OPAQUE_THRESHOLD) {
const data: anydata[let i: numberi] = 0;
const data: anydata[let i: numberi + 1] = 0;
const data: anydata[let i: numberi + 2] = 0;
const data: anydata[let i: numberi + 3] = 0;
continue;
}
const { const r: numberRed channel (0-255).r, const g: numberGreen channel (0-255).g, const b: numberBlue channel (0-255).b } = function nearestShapeColor(r: number, g: number, b: number): Color32Finds whichever of FILL_COLOR/STROKE_COLOR is closer to a given pixel color.
"Closer" here means smaller distance in RGB space - treating red, green, and blue like
three coordinates and measuring the straight-line distance between two colors, the same
way you would measure distance between two points on a map.nearestShapeColor(const data: anydata[let i: numberi], const data: anydata[let i: numberi + 1], const data: anydata[let i: numberi + 2]);
const data: anydata[let i: numberi] = const r: numberRed channel (0-255).r;
const data: anydata[let i: numberi + 1] = const g: numberGreen channel (0-255).g;
const data: anydata[let i: numberi + 2] = const b: numberBlue channel (0-255).b;
const data: anydata[let i: numberi + 3] = 255;
}
ctx: OffscreenCanvasRenderingContext2Dctx.putImageData(const imageData: anyimageData, 0, 0);
}
/**
* Draws one filled shape centered inside a square cell.
*
* @param {OffscreenCanvasRenderingContext2D} ctx
* @param {number} cellX - Left edge of the cell in sheet pixels.
* @param {number} cellY - Top edge of the cell in sheet pixels.
* @param {number} kind - 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.
*/
function function drawShapeInCell(ctx: OffscreenCanvasRenderingContext2D, cellX: number, cellY: number, kind: number): voidDraws one filled shape centered inside a square cell.drawShapeInCell(ctx: OffscreenCanvasRenderingContext2Dctx, cellX: number- Left edge of the cell in sheet pixels.cellX, cellY: number- Top edge of the cell in sheet pixels.cellY, kind: number- 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.kind) {
const const cx: numbercx = cellX: number- Left edge of the cell in sheet pixels.cellX + const SHAPE_CELL: 20SHAPE_CELL / 2;
const const cy: numbercy = cellY: number- Top edge of the cell in sheet pixels.cellY + const SHAPE_CELL: 20SHAPE_CELL / 2;
ctx: OffscreenCanvasRenderingContext2Dctx.fillStyle = const FILL_COLOR: Color32FILL_COLOR.Color32.toHex(): stringConverts color to CSS hex string format.toHex();
ctx: OffscreenCanvasRenderingContext2Dctx.strokeStyle = const STROKE_COLOR: Color32STROKE_COLOR.Color32.toHex(): stringConverts color to CSS hex string format.toHex();
ctx: OffscreenCanvasRenderingContext2Dctx.lineWidth = 1;
if (kind: number- 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.kind === 0) {
// Square
ctx: OffscreenCanvasRenderingContext2Dctx.fillRect(cellX: number- Left edge of the cell in sheet pixels.cellX + 4, cellY: number- Top edge of the cell in sheet pixels.cellY + 4, const SHAPE_CELL: 20SHAPE_CELL - 8, const SHAPE_CELL: 20SHAPE_CELL - 8);
ctx: OffscreenCanvasRenderingContext2Dctx.strokeRect(cellX: number- Left edge of the cell in sheet pixels.cellX + 4, cellY: number- Top edge of the cell in sheet pixels.cellY + 4, const SHAPE_CELL: 20SHAPE_CELL - 8, const SHAPE_CELL: 20SHAPE_CELL - 8);
} else if (kind: number- 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.kind === 1) {
// Circle
ctx: OffscreenCanvasRenderingContext2Dctx.beginPath();
ctx: OffscreenCanvasRenderingContext2Dctx.arc(const cx: numbercx, const cy: numbercy, const SHAPE_CELL: 20SHAPE_CELL / 2 - 4, 0, Math.PI * 2);
ctx: OffscreenCanvasRenderingContext2Dctx.fill();
ctx: OffscreenCanvasRenderingContext2Dctx.stroke();
} else if (kind: number- 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.kind === 2) {
// Triangle
ctx: OffscreenCanvasRenderingContext2Dctx.beginPath();
ctx: OffscreenCanvasRenderingContext2Dctx.moveTo(const cx: numbercx, cellY: number- Top edge of the cell in sheet pixels.cellY + 3);
ctx: OffscreenCanvasRenderingContext2Dctx.lineTo(cellX: number- Left edge of the cell in sheet pixels.cellX + const SHAPE_CELL: 20SHAPE_CELL - 3, cellY: number- Top edge of the cell in sheet pixels.cellY + const SHAPE_CELL: 20SHAPE_CELL - 3);
ctx: OffscreenCanvasRenderingContext2Dctx.lineTo(cellX: number- Left edge of the cell in sheet pixels.cellX + 3, cellY: number- Top edge of the cell in sheet pixels.cellY + const SHAPE_CELL: 20SHAPE_CELL - 3);
ctx: OffscreenCanvasRenderingContext2Dctx.closePath();
ctx: OffscreenCanvasRenderingContext2Dctx.fill();
ctx: OffscreenCanvasRenderingContext2Dctx.stroke();
} else if (kind: number- 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.kind === 3) {
// Five-point star
ctx: OffscreenCanvasRenderingContext2Dctx.beginPath();
for (let let i: numberi = 0; let i: numberi < 5; let i: numberi++) {
const const outerAngle: numberouterAngle = (let i: numberi / 5) * Math.PI * 2 - Math.PI / 2;
const const innerAngle: numberinnerAngle = const outerAngle: numberouterAngle + Math.PI / 5;
const const outerR: numberouterR = const SHAPE_CELL: 20SHAPE_CELL / 2 - 2;
const const innerR: numberinnerR = const outerR: numberouterR * 0.45;
ctx: OffscreenCanvasRenderingContext2Dctx.lineTo(const cx: numbercx + Math.cos(const outerAngle: numberouterAngle) * const outerR: numberouterR, const cy: numbercy + Math.sin(const outerAngle: numberouterAngle) * const outerR: numberouterR);
ctx: OffscreenCanvasRenderingContext2Dctx.lineTo(const cx: numbercx + Math.cos(const innerAngle: numberinnerAngle) * const innerR: numberinnerR, const cy: numbercy + Math.sin(const innerAngle: numberinnerAngle) * const innerR: numberinnerR);
}
ctx: OffscreenCanvasRenderingContext2Dctx.closePath();
ctx: OffscreenCanvasRenderingContext2Dctx.fill();
ctx: OffscreenCanvasRenderingContext2Dctx.stroke();
} else if (kind: number- 0 square, 1 circle, 2 triangle, 3 star, 4 heart, 5 diamond.kind === 4) {
// Heart (two circles plus a triangle wedge)
ctx: OffscreenCanvasRenderingContext2Dctx.beginPath();
ctx: OffscreenCanvasRenderingContext2Dctx.arc(const cx: numbercx - 4, const cy: numbercy - 2, 5, 0, Math.PI * 2);
ctx: OffscreenCanvasRenderingContext2Dctx.arc(const cx: numbercx + 4, const cy: numbercy - 2, 5, 0, Math.PI * 2);
ctx: OffscreenCanvasRenderingContext2Dctx.fill();
ctx: OffscreenCanvasRenderingContext2Dctx.beginPath();
ctx: OffscreenCanvasRenderingContext2Dctx.moveTo(cellX: number- Left edge of the cell in sheet pixels.cellX + 3, const cy: numbercy);
ctx: OffscreenCanvasRenderingContext2Dctx.lineTo(const cx: numbercx, cellY: number- Top edge of the cell in sheet pixels.cellY + const SHAPE_CELL: 20SHAPE_CELL - 3);
ctx: OffscreenCanvasRenderingContext2Dctx.lineTo(cellX: number- Left edge of the cell in sheet pixels.cellX + const SHAPE_CELL: 20SHAPE_CELL - 3, const cy: numbercy);
ctx: OffscreenCanvasRenderingContext2Dctx.closePath();
ctx: OffscreenCanvasRenderingContext2Dctx.fill();
ctx: OffscreenCanvasRenderingContext2Dctx.stroke();
} else {
// Diamond
ctx: OffscreenCanvasRenderingContext2Dctx.beginPath();
ctx: OffscreenCanvasRenderingContext2Dctx.moveTo(const cx: numbercx, cellY: number- Top edge of the cell in sheet pixels.cellY + 3);
ctx: OffscreenCanvasRenderingContext2Dctx.lineTo(cellX: number- Left edge of the cell in sheet pixels.cellX + const SHAPE_CELL: 20SHAPE_CELL - 3, const cy: numbercy);
ctx: OffscreenCanvasRenderingContext2Dctx.lineTo(const cx: numbercx, cellY: number- Top edge of the cell in sheet pixels.cellY + const SHAPE_CELL: 20SHAPE_CELL - 3);
ctx: OffscreenCanvasRenderingContext2Dctx.lineTo(cellX: number- Left edge of the cell in sheet pixels.cellX + 3, const cy: numbercy);
ctx: OffscreenCanvasRenderingContext2Dctx.closePath();
ctx: OffscreenCanvasRenderingContext2Dctx.fill();
ctx: OffscreenCanvasRenderingContext2Dctx.stroke();
}
}
/**
* Builds a 3x2 sprite sheet with six shapes on an offscreen canvas.
*
* @returns {{ canvas: OffscreenCanvas, ctx: OffscreenCanvasRenderingContext2D, rects: Rect2i[] }}
*/
function function buildShapeSheet(): {
canvas: OffscreenCanvas;
ctx: OffscreenCanvasRenderingContext2D;
rects: Rect2i[];
}
Builds a 3x2 sprite sheet with six shapes on an offscreen canvas.buildShapeSheet() {
const const sheetW: numbersheetW = const SHAPE_COLS: 3SHAPE_COLS * const SHAPE_CELL: 20SHAPE_CELL;
const const sheetH: numbersheetH = const SHAPE_ROWS: 2SHAPE_ROWS * const SHAPE_CELL: 20SHAPE_CELL;
const const canvas: anycanvas = new OffscreenCanvas(const sheetW: numbersheetW, const sheetH: numbersheetH);
const const ctx: anyctx = const canvas: anycanvas.getContext('2d');
if (!const ctx: anyctx) {
throw new Error('Could not create 2D context for shape sheet');
}
// Clear to transparent so unused pixels stay invisible.
const ctx: anyctx.clearRect(0, 0, const sheetW: numbersheetW, const sheetH: numbersheetH);
const const rects: {}rects = [];
// One cell per entry in the shared SHAPE_NAMES list (the same list captions use).
for (let let i: numberi = 0; let i: numberi < const SHAPE_NAMES: {}SHAPE_NAMES.length; let i: numberi++) {
const const col: numbercol = let i: numberi % const SHAPE_COLS: 3SHAPE_COLS;
const const row: anyrow = Math.floor(let i: numberi / const SHAPE_COLS: 3SHAPE_COLS);
const const cellX: numbercellX = const col: numbercol * const SHAPE_CELL: 20SHAPE_CELL;
const const cellY: numbercellY = const row: anyrow * const SHAPE_CELL: 20SHAPE_CELL;
function drawShapeInCell(ctx: OffscreenCanvasRenderingContext2D, cellX: number, cellY: number, kind: number): voidDraws one filled shape centered inside a square cell.drawShapeInCell(const ctx: anyctx, const cellX: numbercellX, const cellY: numbercellY, let i: numberi);
const rects: {}rects.push(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(const cellX: numbercellX, const cellY: numbercellY, const SHAPE_CELL: 20SHAPE_CELL, const SHAPE_CELL: 20SHAPE_CELL));
}
// Flatten the smooth, anti-aliased edges into flat colors so every pixel matches a
// palette entry exactly (see quantizeCanvasToShapeColors() for why this is required).
function quantizeCanvasToShapeColors(ctx: OffscreenCanvasRenderingContext2D, w: number, h: number): voidRewrites every pixel of the canvas so it is either fully transparent or one of the exact
design colors (see FILL_COLOR/STROKE_COLOR above). sheet.indexize() later requires each
pixel to match a palette entry exactly, so the smooth, blended edges anti-aliasing draws
must be snapped to flat colors here - otherwise indexize() would reject every blended
edge pixel as "not in the palette".quantizeCanvasToShapeColors(const ctx: anyctx, const sheetW: numbersheetW, const sheetH: numbersheetH);
return { canvas: OffscreenCanvascanvas, ctx: OffscreenCanvasRenderingContext2Dctx, rects: {}rects };
}
/**
* Demonstrates sprite sheets, source rectangles, palette offsets, and opacity pulsing.
*
* @implements {IBTDemo}
*/
class class DemoDemonstrates sprite sheets, source rectangles, palette offsets, and opacity pulsing.Demo {
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = null;
/** @type {SpriteSheet | null} */
Demo.sheet: SpriteSheet | nullsheet = null;
// Slot map for the shared UI kit theme, filled in init() by applyTheme().
// theme.bg, theme.text, and friends are palette indices for our own drawing.
Demo.theme: nulltheme = null;
// One Rect2i per shape cell in the programmatic sheet.
Demo.shapeRects: {}shapeRects = [];
// Star cell - reused for the palette-offset row below the shape grid.
/** @type {Rect2i | null} */
Demo.themeRect: Rect2i | nullthemeRect = null;
Demo.colorCount: numbercolorCount = 0;
Demo.baseColors: {}baseColors = [];
Demo.animTime: numberanimTime = 0;
/**
* @returns {Partial<HardwareSettings>}
*/
Demo.configure(): Partial<HardwareSettings>Optional hook to declare display size, optional output drawing-buffer size,
upscale filter, target fixed-update rate, rendering backend, and overlay.
When omitted, the engine uses
{@link
defaultConfig
}
(`320x240` logical,
`640x480` drawing buffer, `60` FPS, overlay enabled).
When present, you may return only the fields you want to change; the
engine merges them with
{@link
defaultConfig
}
via
{@link
mergeHardwareSettings
}
. Omit `displaySize` to inherit the full
default resolution and output buffer. Include `displaySize` when you
want a custom logical size; optional fields you omit then stay unset
(for example no `drawingBufferSize` means a 1:1 drawing buffer).configure() {
return {
isOverlayTimingChartEnabled: booleanisOverlayTimingChartEnabled: true,
overlayStyle: {
barPaletteIndex: number;
textPaletteIndex: number;
gapPaletteIndex: number;
}
overlayStyle: {
barPaletteIndex: numberbarPaletteIndex: const UI_BG: 240UI_BG,
textPaletteIndex: numbertextPaletteIndex: const UI_DIM: 245UI_DIM,
gapPaletteIndex: numbergapPaletteIndex: const UI_BAR: 246UI_BAR,
},
overlayTimingChartStyle: {
updateBarPaletteIndex: number;
renderBarPaletteIndex: number;
warningPaletteIndex: number;
errorPaletteIndex: number;
tagPaletteIndex: number;
}
overlayTimingChartStyle: {
updateBarPaletteIndex: numberupdateBarPaletteIndex: const UI_DIM: 245UI_DIM,
renderBarPaletteIndex: numberrenderBarPaletteIndex: const UI_INFO: 248UI_INFO,
warningPaletteIndex: numberwarningPaletteIndex: const UI_INFO: 248UI_INFO,
errorPaletteIndex: numbererrorPaletteIndex: const UI_TEXT: 244UI_TEXT,
tagPaletteIndex: numbertagPaletteIndex: const UI_DIM: 245UI_DIM,
},
};
}
/**
* Builds the shape sheet on a canvas, registers colors, and calls sheet.indexize().
*
* @returns {Promise<boolean>}
*/
async Demo.init(): Promise<boolean>Builds the shape sheet on a canvas, registers colors, and calls sheet.indexize().init() {
console.log('[SpriteDemo] Initializing...');
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);
// Install the shared UI theme: applyTheme() writes the twelve UI kit colors into
// high palette slots (240-251), far above this demo's sprite colors (slots 10-19),
// and returns a map of friendly names to those slots (this.theme.bg, .text, ...).
// Every caption and the code panel below draw with these shared colors.
this.Demo.theme: nulltheme = import applyThemeapplyTheme(this.Demo.palette: Palettepalette);
try {
const { const canvas: OffscreenCanvascanvas, const ctx: OffscreenCanvasRenderingContext2Dctx, const rects: {}rects } = function buildShapeSheet(): {
canvas: OffscreenCanvas;
ctx: OffscreenCanvasRenderingContext2D;
rects: Rect2i[];
}
Builds a 3x2 sprite sheet with six shapes on an offscreen canvas.buildShapeSheet();
this.Demo.shapeRects: {}shapeRects = const rects: {}rects;
this.Demo.themeRect: Rect2i | nullthemeRect = const rects: {}rects[3]; // Star - used for palette-offset demos.
this.Demo.baseColors: {}baseColors = import registerCanvasColorsregisterCanvasColors(this.Demo.palette: Palettepalette, const ctx: OffscreenCanvasRenderingContext2Dctx, const canvas: OffscreenCanvascanvas.width, const canvas: OffscreenCanvascanvas.height, const COLOR_BASE: 10COLOR_BASE);
this.Demo.colorCount: numbercolorCount = this.Demo.baseColors: {}baseColors.length;
const const colorCount: numbercolorCount = this.Demo.colorCount: numbercolorCount;
// Build theme blocks: Fire, Ice, Void, and Pulse are static once written here.
// palette.fillBlock(start, source, transform) writes transform(baseColor) into one
// slot per base color, starting at `start` - it replaces a hand-written for loop.
// Fire: push red up and pull blue down.
this.Demo.palette: Palettepalette.Palette.fillBlock(start: number, source: readonly Color32[], transform: (color: Color32, index: number) => Color32): numberWrites a transformed block of colors into contiguous palette slots.
Writes `transform(source[i], i)` into slot `start + i` for every `i` in `[0, source.length)`, delegating
each write to
{@link
set
}
so it inherits
{@link
set
}
's validation, including the rule that slot 0 must
stay transparent. Collapses the common pattern of looping
`palette.set(start + i, transform(baseColors[i]))` into one call.fillBlock(
const COLOR_BASE: 10COLOR_BASE + const colorCount: numbercolorCount,
this.Demo.baseColors: {}baseColors,
(base: Color32base) => new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(Math.min(255, base: Color32base.Color32.r: numberRed channel (0-255).r + 80), base: Color32base.Color32.g: numberGreen channel (0-255).g, Math.max(0, base: Color32base.Color32.b: numberBlue channel (0-255).b - 80)),
);
// Ice: pull red down and push blue up.
this.Demo.palette: Palettepalette.Palette.fillBlock(start: number, source: readonly Color32[], transform: (color: Color32, index: number) => Color32): numberWrites a transformed block of colors into contiguous palette slots.
Writes `transform(source[i], i)` into slot `start + i` for every `i` in `[0, source.length)`, delegating
each write to
{@link
set
}
so it inherits
{@link
set
}
's validation, including the rule that slot 0 must
stay transparent. Collapses the common pattern of looping
`palette.set(start + i, transform(baseColors[i]))` into one call.fillBlock(
const COLOR_BASE: 10COLOR_BASE + const colorCount: numbercolorCount * 2,
this.Demo.baseColors: {}baseColors,
(base: Color32base) => new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(Math.max(0, base: Color32base.Color32.r: numberRed channel (0-255).r - 60), base: Color32base.Color32.g: numberGreen channel (0-255).g, Math.min(255, base: Color32base.Color32.b: numberBlue channel (0-255).b + 80)),
);
// Void: darken every channel toward black.
this.Demo.palette: Palettepalette.Palette.fillBlock(start: number, source: readonly Color32[], transform: (color: Color32, index: number) => Color32): numberWrites a transformed block of colors into contiguous palette slots.
Writes `transform(source[i], i)` into slot `start + i` for every `i` in `[0, source.length)`, delegating
each write to
{@link
set
}
so it inherits
{@link
set
}
's validation, including the rule that slot 0 must
stay transparent. Collapses the common pattern of looping
`palette.set(start + i, transform(baseColors[i]))` into one call.fillBlock(
const COLOR_BASE: 10COLOR_BASE + const colorCount: numbercolorCount * 3,
this.Demo.baseColors: {}baseColors,
(base: Color32base) => new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(Math.floor(base: Color32base.Color32.r: numberRed channel (0-255).r * 0.25), Math.floor(base: Color32base.Color32.g: numberGreen channel (0-255).g * 0.25), Math.floor(base: Color32base.Color32.b: numberBlue channel (0-255).b * 0.25)),
);
// Pulse: same colors as the original, at full opacity.
this.Demo.palette: Palettepalette.Palette.fillBlock(start: number, source: readonly Color32[], transform: (color: Color32, index: number) => Color32): numberWrites a transformed block of colors into contiguous palette slots.
Writes `transform(source[i], i)` into slot `start + i` for every `i` in `[0, source.length)`, delegating
each write to
{@link
set
}
so it inherits
{@link
set
}
's validation, including the rule that slot 0 must
stay transparent. Collapses the common pattern of looping
`palette.set(start + i, transform(baseColors[i]))` into one call.fillBlock(
const COLOR_BASE: 10COLOR_BASE + const colorCount: numbercolorCount * 4,
this.Demo.baseColors: {}baseColors,
(base: Color32base) => new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(base: Color32base.Color32.r: numberRed channel (0-255).r, base: Color32base.Color32.g: numberGreen channel (0-255).g, base: Color32base.Color32.b: numberBlue channel (0-255).b, 255),
);
const const image: anyimage = await import canvasToImagecanvasToImage(const canvas: OffscreenCanvascanvas);
this.Demo.sheet: SpriteSheet | nullsheet = new new SpriteSheet(image: HTMLImageElement | null, size?: Vector2i): SpriteSheetCreates a sprite sheet from a loaded image.
Use the static load() method for easier loading from URL.SpriteSheet(const image: anyimage);
this.Demo.sheet: SpriteSheetsheet.SpriteSheet.indexize(palette: Palette): voidConverts the sprite sheet's RGBA pixels to palette indices.
Each non-transparent pixel is looked up in the provided palette via exact
color matching. Index 0 is always transparent. The resulting indices are
stored internally; an `r8uint` GPU texture is created lazily on the next
`getTexture()` call.
The original RGBA data is retained so `reindexize()` can re-convert after a
palette swap without reloading the image.indexize(this.Demo.palette: Palettepalette);
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);
console.log(
`[SpriteDemo] Built shape sheet: ${const canvas: OffscreenCanvascanvas.width}x${const canvas: OffscreenCanvascanvas.height}px, ${const colorCount: numbercolorCount} unique colors`,
);
} catch (function (local var) error: unknownerror) {
console.error('[SpriteDemo] Failed to build shape sheet:', function (local var) error: unknownerror);
return false;
}
console.log('[SpriteDemo] Initialization complete!');
return true;
}
Demo.update(): voidCalled zero or more times per frame at the fixed timestep declared by
`targetFPS`. The accumulator pattern ensures the target rate is met on
average, but a single frame may invoke this multiple times (catch-up) or
not at all. Update simulation, timers, and input-driven state here.
This is a hot path. Minimize allocations, reuse objects, and prefer
in-place vector operations where possible.
Avoid rendering work here; draw in `render()` instead.update() {
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;
if (!this.Demo.colorCount: numbercolorCount) {
return;
}
}
Demo.render(): voidCalled once per `requestAnimationFrame` tick (browser refresh rate).
Issue all draw calls for the current frame here.
When
{@link
HardwareSettings.isOverlayEnabled
}
is `true` (default), the engine
draws a screen-space overlay HUD after this method returns (present FPS, target FPS, draw calls,
frame/update()/render() timings, backend, demo title). Optional
{@link
overlayRows
}
adds stacked bars above
the footer.
Demos do not need to duplicate engine overlay text. Reserve about ~42 px at the top and space for the bottom palette
grid (or ~13 px when
{@link
HardwareSettings.isOverlayPaletteEnabled
}
is `false`) at the bottom (plus ~14 px per
custom overlay row) for overlay bars, or disable the overlay in `configure()` when using custom full-screen HUD
layouts.
This is a hot path. Batch draws by texture to reduce GPU state changes
and reuse Color32/Vector2i instances instead of allocating per frame.
Avoid mutating the simulation state here unless it is strictly visual.render() {
// Clear the whole screen with the shared UI theme's background color.
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(this.Demo.theme: nulltheme.bg);
// Row 1: six shapes - each draw call uses a different source Rect2i.
const const shapeY: 14shapeY = 14;
const const shapeSpacing: 50shapeSpacing = 50;
for (let let i: numberi = 0; let i: numberi < this.Demo.shapeRects: {}shapeRects.length; let i: numberi++) {
const const destX: numberdestX = 6 + let i: numberi * const shapeSpacing: 50shapeSpacing;
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.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => voidDraws a sprite region from an indexed sprite sheet.
Sprite draws are batched internally. Grouping draws from the same
{@link
SpriteSheet
}
minimizes batch flushes and reduces GPU state changes.
The sprite sheet must have been converted to palette indices via
`spriteSheet.indexize(palette)` before the first draw call. Prefer
`SpriteSheet.loadIndexed(...)` for one-call setup.
**Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1.
Index 0 is always transparent and is discarded by the fragment shader. The final palette
lookup is `storedIndex + paletteOffset`, so:
- `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`.
`palette[0]` is never reachable because stored indices start at 1.
- `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored
at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`,
and so on. Use this for palette-swap effects such as team colors or damage flashes.
**Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to
the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's
robust buffer access returns 0 for every component; because the fragment shader forces alpha
to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative
JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also
produces out-of-bounds black pixels.drawSprite(this.Demo.sheet: SpriteSheet | nullsheet, this.Demo.shapeRects: {}shapeRects[let i: numberi], new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const destX: numberdestX, const shapeY: 14shapeY), 0);
import uiui.caption(const destX: numberdestX, const shapeY: 14shapeY + 22, const SHAPE_NAMES: {}SHAPE_NAMES[let i: numberi], { color: stringcolor: 'dim' });
}
import uiui.caption(6, 58, 'Source rects - one region per shape', { color: stringcolor: 'dim' });
// Row 2: palette offsets on the star shape (offset shifts every pixel index).
const const n: numbern = this.Demo.colorCount: numbercolorCount;
const const themeY: 78themeY = 78;
const const themeSpacing: 72themeSpacing = 72;
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.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => voidDraws a sprite region from an indexed sprite sheet.
Sprite draws are batched internally. Grouping draws from the same
{@link
SpriteSheet
}
minimizes batch flushes and reduces GPU state changes.
The sprite sheet must have been converted to palette indices via
`spriteSheet.indexize(palette)` before the first draw call. Prefer
`SpriteSheet.loadIndexed(...)` for one-call setup.
**Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1.
Index 0 is always transparent and is discarded by the fragment shader. The final palette
lookup is `storedIndex + paletteOffset`, so:
- `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`.
`palette[0]` is never reachable because stored indices start at 1.
- `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored
at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`,
and so on. Use this for palette-swap effects such as team colors or damage flashes.
**Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to
the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's
robust buffer access returns 0 for every component; because the fragment shader forces alpha
to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative
JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also
produces out-of-bounds black pixels.drawSprite(this.Demo.sheet: SpriteSheet | nullsheet, this.Demo.themeRect: Rect2i | nullthemeRect, new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(8, const themeY: 78themeY), 0);
import uiui.caption(6, const themeY: 78themeY + 22, 'Original', { color: stringcolor: 'dim' });
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.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => voidDraws a sprite region from an indexed sprite sheet.
Sprite draws are batched internally. Grouping draws from the same
{@link
SpriteSheet
}
minimizes batch flushes and reduces GPU state changes.
The sprite sheet must have been converted to palette indices via
`spriteSheet.indexize(palette)` before the first draw call. Prefer
`SpriteSheet.loadIndexed(...)` for one-call setup.
**Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1.
Index 0 is always transparent and is discarded by the fragment shader. The final palette
lookup is `storedIndex + paletteOffset`, so:
- `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`.
`palette[0]` is never reachable because stored indices start at 1.
- `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored
at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`,
and so on. Use this for palette-swap effects such as team colors or damage flashes.
**Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to
the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's
robust buffer access returns 0 for every component; because the fragment shader forces alpha
to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative
JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also
produces out-of-bounds black pixels.drawSprite(this.Demo.sheet: SpriteSheet | nullsheet, this.Demo.themeRect: Rect2i | nullthemeRect, new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(8 + const themeSpacing: 72themeSpacing, const themeY: 78themeY), const n: numbern);
import uiui.caption(6 + const themeSpacing: 72themeSpacing, const themeY: 78themeY + 22, 'Fire', { color: stringcolor: 'dim' });
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.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => voidDraws a sprite region from an indexed sprite sheet.
Sprite draws are batched internally. Grouping draws from the same
{@link
SpriteSheet
}
minimizes batch flushes and reduces GPU state changes.
The sprite sheet must have been converted to palette indices via
`spriteSheet.indexize(palette)` before the first draw call. Prefer
`SpriteSheet.loadIndexed(...)` for one-call setup.
**Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1.
Index 0 is always transparent and is discarded by the fragment shader. The final palette
lookup is `storedIndex + paletteOffset`, so:
- `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`.
`palette[0]` is never reachable because stored indices start at 1.
- `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored
at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`,
and so on. Use this for palette-swap effects such as team colors or damage flashes.
**Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to
the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's
robust buffer access returns 0 for every component; because the fragment shader forces alpha
to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative
JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also
produces out-of-bounds black pixels.drawSprite(this.Demo.sheet: SpriteSheet | nullsheet, this.Demo.themeRect: Rect2i | nullthemeRect, new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(8 + const themeSpacing: 72themeSpacing * 2, const themeY: 78themeY), const n: numbern * 2);
import uiui.caption(6 + const themeSpacing: 72themeSpacing * 2, const themeY: 78themeY + 22, 'Ice', { color: stringcolor: 'dim' });
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.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => voidDraws a sprite region from an indexed sprite sheet.
Sprite draws are batched internally. Grouping draws from the same
{@link
SpriteSheet
}
minimizes batch flushes and reduces GPU state changes.
The sprite sheet must have been converted to palette indices via
`spriteSheet.indexize(palette)` before the first draw call. Prefer
`SpriteSheet.loadIndexed(...)` for one-call setup.
**Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1.
Index 0 is always transparent and is discarded by the fragment shader. The final palette
lookup is `storedIndex + paletteOffset`, so:
- `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`.
`palette[0]` is never reachable because stored indices start at 1.
- `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored
at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`,
and so on. Use this for palette-swap effects such as team colors or damage flashes.
**Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to
the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's
robust buffer access returns 0 for every component; because the fragment shader forces alpha
to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative
JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also
produces out-of-bounds black pixels.drawSprite(this.Demo.sheet: SpriteSheet | nullsheet, this.Demo.themeRect: Rect2i | nullthemeRect, new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(8 + const themeSpacing: 72themeSpacing * 3, const themeY: 78themeY), const n: numbern * 3);
import uiui.caption(6 + const themeSpacing: 72themeSpacing * 3, const themeY: 78themeY + 22, 'Void', { color: stringcolor: 'dim' });
this.Demo.renderCodeSnippet(): voidThe "how you would load a real PNG" cheat sheet, as a bordered kit panel anchored
to the bottom-left corner of the screen.renderCodeSnippet();
}
/**
* The "how you would load a real PNG" cheat sheet, as a bordered kit panel anchored
* to the bottom-left corner of the screen.
*/
Demo.renderCodeSnippet(): voidThe "how you would load a real PNG" cheat sheet, as a bordered kit panel anchored
to the bottom-left corner of the screen.renderCodeSnippet() {
import uiui.begin(import UI_ANCHORSUI_ANCHORS.BOTTOM_LEFT, { y: numbery: 178 });
import uiui.panel('Production PNG load:');
import uiui.label('const indexed = await SpriteSheet', { color: stringcolor: 'info' });
import uiui.label(" .loadIndexed('/sprites/test.png', palette, 10);", { color: stringcolor: 'info' });
import uiui.end();
}
}
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 DemoDemonstrates sprite sheets, source rectangles, palette offsets, and opacity pulsing.Demo);