// Animation and timing: how to animate sprites using tick-based timing.
// @description Tick-based animation: cycle walk frames, drive a small state machine, and spawn particles that fade out.
//
// Prerequisites: Basics (https://demos.blit386.dev/basics),
// Sprites (https://demos.blit386.dev/sprites).
// Guide: https://blit386.dev/docs/api/game-loop
//
// In BLIT386, the tick counter goes up once per update() call at a fixed rate (targetFPS),
// not once per screen refresh. render() can run more often than update() on a high refresh
// monitor, so there can be more drawn frames than ticks. This demo shows the most common
// patterns for making things happen over time:
//
//   1. State machines: an object can be in one of several states
//      (Idle, Walking, Jumping) and behavior changes accordingly.
//
//   2. Cooldown timers: track how many ticks must pass before an ability
//      can be used again (like a spell cooldown in an RPG).
//
//   3. Periodic events: spawn a new particle every N ticks.
//
//   4. Jump arc: a smooth sine-curve arc using Math.sin (not gravity-style parabola).
//
// All timing is done by comparing BT.ticks to a stored "start tick".
// Each tick is one update() call; at targetFPS = 60, one tick is 1/60 of a second.
//
// HOW PARTICLE COLORS WORK:
//
// Each particle gets its own palette slot at the moment it is spawned.
// In update(), we compute the color (hue from spawn time, alpha from age)
// and write it into that slot with palette.set(). In render(), we just
// use the particle's slot number - no Color32 objects needed there.
//
// Palette Animation demo explores this palette-animation idea in depth:
// https://demos.blit386.dev/palette-animation
//
// The cooldown readout, spawn timer, concept summary, and state badge are drawn with the
// shared UI kit (src/shared/ui.js), which installs its twelve UI colors high in the
// palette (slots 240-251) via applyTheme().

import { function applyEasing(t: number, easing: EasingFunction): number
Applies an easing curve to a normalized time value.
@since1.0.3@changed1.5.0 Added sine, cubic, quartic, quintic, expo, circ, back, elastic, and bounce families.@paramt - Normalized time in [0, 1]. Values outside this range are not clamped.@parameasing - Easing curve to apply.@returnsEased value. Guaranteed to return 0 for `t = 0` and 1 for `t = 1`.
applyEasing
, 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
,
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.
@since0.1.0
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.
@since0.1.0
SpriteSheet
, class Timer
Fixed-tick interval helper for {@link IBTDemo.update } loops. Counts engine ticks ( {@link BT.ticks } ), which advance once per fixed update at {@link HardwareSettings.targetFPS } , not once per {@link IBTDemo.render } frame. Convert ticks to seconds with `intervalTicks / BT.targetFPS`. Tracks a "last fired" tick and reports when a configured interval has elapsed. Useful for periodic events such as particle spawning, score ticks, or palette swaps.
@since1.0.3
Timer
, 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 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 */ // AnimState defines the three states the moving rock can be in. // Object.freeze prevents these values from being changed by accident. const const AnimState: anyAnimState = Object.freeze({ type Idle: stringIdle: 'Idle', // The rock is sitting still. type Walking: stringWalking: 'Walking', // The rock is sliding across the screen. type Jumping: stringJumping: 'Jumping', // The rock is following a jump arc. }); // How many particles can be alive at once. Each gets its own palette slot. const const MAX_PARTICLES: 20MAX_PARTICLES = 20; // Where in the palette particle colors are stored (slots 50..69). const const PARTICLE_SLOT_START: 50PARTICLE_SLOT_START = 50; // Where the sprite's colors start (slots 20..20+N-1, where N is extracted at runtime). const const SPRITE_BASE: 20SPRITE_BASE = 20; // Walk animation: four source rects in one horizontal strip (idle + three walk poses). const const WALK_FRAME_W: 18WALK_FRAME_W = 18; const const WALK_FRAME_COUNT: 4WALK_FRAME_COUNT = 4; // Scene color slots (low palette slots - text and panels use the shared UI theme instead). const const C_GROUND: 1C_GROUND = 1; // (40, 60, 40) dark green ground strip. const const C_SHADOW: 2C_SHADOW = 2; // (0, 0, 0, 100) semi-transparent shadow under the rock. const const C_STATE_IDLE: 3C_STATE_IDLE = 3; // (150, 150, 150) calm gray - the Idle state color. const const C_STATE_WALK: 4C_STATE_WALK = 4; // (100, 255, 100) "go" green - the Walking state color. const const C_STATE_JUMP: 5C_STATE_JUMP = 5; // (255, 100, 100) alert red - the Jumping state color. // 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_HEADER: 246UI_HEADER = 246; // 'ui_header' - warm amber (render bars, chart tags). const const UI_ACCENT: 247UI_ACCENT = 247; // 'ui_accent' - phosphor green (update bars). const const UI_WARM: 248UI_WARM = 248; // 'ui_accent_warm' - orange (chart warning and error frames). const const UI_INFO: 249UI_INFO = 249; // 'ui_info' - light blue (overlay row text). /** * Draws one character pose into a walk-strip cell. * Frame 0 = idle; frames 1-3 = walk cycle with alternating leg positions. * * @param {OffscreenCanvasRenderingContext2D} ctx * @param {number} frameIndex */ function function drawWalkFrame(ctx: OffscreenCanvasRenderingContext2D, frameIndex: number): void
Draws one character pose into a walk-strip cell. Frame 0 = idle; frames 1-3 = walk cycle with alternating leg positions.
@paramctx@paramframeIndex
drawWalkFrame
(ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
, frameIndex: number
@paramframeIndex
frameIndex
) {
const const ox: numberox = frameIndex: number
@paramframeIndex
frameIndex
* const WALK_FRAME_W: 18WALK_FRAME_W;
const const bodyColor: "#b0b0c8"bodyColor = '#b0b0c8'; const const legColor: "#8080a0"legColor = '#8080a0'; ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.fillStyle = const bodyColor: "#b0b0c8"bodyColor;
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.fillRect(const ox: numberox + 5, 4, 8, 8);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.fillStyle = const legColor: "#8080a0"legColor;
if (frameIndex: number
@paramframeIndex
frameIndex
=== 0) {
// Idle: feet together under the body. ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.fillRect(const ox: numberox + 6, 12, 3, 4);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.fillRect(const ox: numberox + 9, 12, 3, 4);
} else if (frameIndex: number
@paramframeIndex
frameIndex
=== 1) {
// Left foot forward. ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.fillRect(const ox: numberox + 4, 12, 3, 4);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.fillRect(const ox: numberox + 10, 13, 3, 3);
} else if (frameIndex: number
@paramframeIndex
frameIndex
=== 2) {
// Mid stride: feet under hips. ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.fillRect(const ox: numberox + 6, 12, 3, 4);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.fillRect(const ox: numberox + 9, 12, 3, 4);
} else { // Right foot forward. ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.fillRect(const ox: numberox + 5, 13, 3, 3);
ctx: OffscreenCanvasRenderingContext2D
@paramctx
ctx
.fillRect(const ox: numberox + 11, 12, 3, 4);
} } /** * Builds a horizontal strip with idle + three walk frames. * * @returns {{ canvas: OffscreenCanvas, ctx: OffscreenCanvasRenderingContext2D, frames: Rect2i[] }} */ function
function buildWalkSheet(): {
    canvas: OffscreenCanvas;
    ctx: OffscreenCanvasRenderingContext2D;
    frames: Rect2i[];
}
Builds a horizontal strip with idle + three walk frames.
@returns
buildWalkSheet
() {
const const sheetW: numbersheetW = const WALK_FRAME_W: 18WALK_FRAME_W * const WALK_FRAME_COUNT: 4WALK_FRAME_COUNT; const const sheetH: 18sheetH = const WALK_FRAME_W: 18WALK_FRAME_W; const const canvas: anycanvas = new OffscreenCanvas(const sheetW: numbersheetW, const sheetH: 18sheetH); const const ctx: anyctx = const canvas: anycanvas.getContext('2d'); if (!const ctx: anyctx) { throw new Error('Could not create 2D context for walk sheet'); } const ctx: anyctx.clearRect(0, 0, const sheetW: numbersheetW, const sheetH: 18sheetH); const const frames: {}frames = []; for (let let f: numberf = 0; let f: numberf < const WALK_FRAME_COUNT: 4WALK_FRAME_COUNT; let f: numberf++) { function drawWalkFrame(ctx: OffscreenCanvasRenderingContext2D, frameIndex: number): void
Draws one character pose into a walk-strip cell. Frame 0 = idle; frames 1-3 = walk cycle with alternating leg positions.
@paramctx@paramframeIndex
drawWalkFrame
(const ctx: anyctx, let f: numberf);
const frames: {}frames.push(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
(let f: numberf * const WALK_FRAME_W: 18WALK_FRAME_W, 0, const WALK_FRAME_W: 18WALK_FRAME_W, const WALK_FRAME_W: 18WALK_FRAME_W));
} return { canvas: OffscreenCanvascanvas, ctx: OffscreenCanvasRenderingContext2Dctx, frames: {}frames }; } /** * Demonstrates tick-based animation timing and state management. * Shows state machines, cooldowns, periodic particle events, and jump arcs. * The "character" is the rock sprite from test.png. * * @implements {IBTDemo} */ class class Demo
Demonstrates tick-based animation timing and state management. Shows state machines, cooldowns, periodic particle events, and jump arcs. The "character" is the rock sprite from test.png.
@implementsIBTDemo
Demo
{
// The palette holds all colors used in this demo. /** @type {Palette | null} */ Demo.palette: Palette | null
@type{Palette | null}
palette
= null;
// The sprite sheet loaded from /sprites/test.png. /** @type {SpriteSheet | null} */ Demo.spriteSheet: SpriteSheet | null
@type{SpriteSheet | null}
spriteSheet
= null;
// One Rect2i per walk-strip frame (idle + three walk poses). Demo.walkFrames: {}walkFrames = []; // Animation state tracks what the rock is currently doing. Demo.animState: anyanimState = const AnimState: anyAnimState.Idle; // The rock's position on screen (top-left corner). Demo.charPos: Vector2icharPos = 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
(80, 100);
// abilityCooldownTicks counts down how many ticks the ability is still unavailable. Demo.abilityCooldownTicks: numberabilityCooldownTicks = 0; // Total duration of the cooldown (2 seconds at 60 FPS = 120 ticks). Demo.abilityCooldownDuration: numberabilityCooldownDuration = 120; // Fires every 180 ticks (3 seconds at 60 FPS) to spawn the next particle batch. Demo.spawnTimer: TimerspawnTimer = new new Timer(intervalTicks: number): Timer
Creates a timer that fires once per fixed-tick interval.
@paramintervalTicks - Number of ticks required between firings; must be a positive integer.
Timer
(180);
// particles is an array of active particle objects. // Each particle: { pos: Vector2i, spawnTick: number, paletteSlot: number } Demo.particles: {}particles = []; // Tracks which particle slots are currently in use (a rotating pool). Demo.nextParticleSlot: numbernextParticleSlot = 0; // When the current jump started (used to calculate the arc height). Demo.jumpStartTick: numberjumpStartTick = 0; // How many ticks a jump takes from launch to landing (1 second = 60 ticks). Demo.jumpDuration: numberjumpDuration = 60; // The rock's horizontal "walk" direction (+1 = right, -1 = left). Demo.walkDir: numberwalkDir = 1; // Starting X position for the walk state. Demo.walkStartX: numberwalkStartX = 80; // 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; // Reused every frame for the engine overlay status row (state + ticks). Demo.overlayRowData: {}overlayRowData = [{ leftText: stringleftText: 'State: Idle', rightText: stringrightText: 'Ticks: 0', textPaletteIndex: numbertextPaletteIndex: const UI_INFO: 249UI_INFO }]; /** * Tells the engine which palette slots to use for overlay bars and timing chart. * * @returns {Partial<HardwareSettings>} */ Demo.configure(): Partial<HardwareSettings>
Tells the engine which palette slots to use for overlay bars and timing chart.
@returns
configure
() {
return {
overlayStyle: {
    barPaletteIndex: number;
    textPaletteIndex: number;
    gapPaletteIndex: number;
}
overlayStyle
: {
barPaletteIndex: numberbarPaletteIndex: const UI_BG: 240UI_BG, textPaletteIndex: numbertextPaletteIndex: const UI_INFO: 249UI_INFO, gapPaletteIndex: numbergapPaletteIndex: const UI_BG: 240UI_BG, }, // Show the scrolling timing chart in the overlay so each frame's update/render cost is visible. isOverlayTimingChartEnabled: booleanisOverlayTimingChartEnabled: true,
overlayTimingChartStyle: {
    updateBarPaletteIndex: number;
    renderBarPaletteIndex: number;
    warningPaletteIndex: number;
    errorPaletteIndex: number;
    tagPaletteIndex: number;
}
overlayTimingChartStyle
: {
// Theme green - used for update() bars, matching the "ready" cooldown color. updateBarPaletteIndex: numberupdateBarPaletteIndex: const UI_ACCENT: 247UI_ACCENT, // Theme amber - used for render() bars. renderBarPaletteIndex: numberrenderBarPaletteIndex: const UI_HEADER: 246UI_HEADER, // Theme orange - flags frames that are close to the frame budget. warningPaletteIndex: numberwarningPaletteIndex: const UI_WARM: 248UI_WARM, // Theme orange again - the old palette also shared one red for warning and error. errorPaletteIndex: numbererrorPaletteIndex: const UI_WARM: 248UI_WARM, // Theme amber - used for milestone labels such as "Start" or BT.assignTag() calls. tagPaletteIndex: numbertagPaletteIndex: const UI_HEADER: 246UI_HEADER, }, }; } /** * Sets up the palette, loads the sprite and font. * * @returns {Promise<boolean>} Returns true when everything is ready. */ async Demo.init(): Promise<boolean>
Sets up the palette, loads the sprite and font.
@returnsReturns true when everything is ready.
init
() {
console.log('[AnimationDemo] Initializing...'); // Create the palette and fill the scene colors (ground, shadow, state indicator). 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
(256);
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_GROUND: 1C_GROUND, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(40, 60, 40));
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_SHADOW: 2C_SHADOW, 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, 0, 100));
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_STATE_IDLE: 3C_STATE_IDLE, 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
(150, 150, 150));
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_STATE_WALK: 4C_STATE_WALK, 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
(100, 255, 100));
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_STATE_JUMP: 5C_STATE_JUMP, 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, 100, 100));
// 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 20-21) // and particle slots (50-69), and returns a map of friendly names to those slots // (this.theme.bg, .text, ...). All on-screen text and the cooldown meter use them. this.Demo.theme: nulltheme = import applyThemeapplyTheme(this.Demo.palette: Palette
@type{Palette | null}
palette
);
// Particle slots (50..69) start fully transparent (alpha 0) so they stay invisible // until update() writes real colors into them when particles spawn. for (let let i: numberi = 0; let i: numberi < const MAX_PARTICLES: 20MAX_PARTICLES; let i: numberi++) { this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const PARTICLE_SLOT_START: 50PARTICLE_SLOT_START + let i: numberi, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(0, 0, 0, 0));
} // Build a four-frame walk strip on an offscreen canvas (idle + three walk poses). try { const { const canvas: OffscreenCanvascanvas, const ctx: OffscreenCanvasRenderingContext2Dctx, const frames: {}frames } =
function buildWalkSheet(): {
    canvas: OffscreenCanvas;
    ctx: OffscreenCanvasRenderingContext2D;
    frames: Rect2i[];
}
Builds a horizontal strip with idle + three walk frames.
@returns
buildWalkSheet
();
this.Demo.walkFrames: {}walkFrames = const frames: {}frames; import registerCanvasColorsregisterCanvasColors(this.Demo.palette: Palette
@type{Palette | null}
palette
, const ctx: OffscreenCanvasRenderingContext2Dctx, const canvas: OffscreenCanvascanvas.width, const canvas: OffscreenCanvascanvas.height, const SPRITE_BASE: 20SPRITE_BASE);
const const image: anyimage = await import canvasToImagecanvasToImage(const canvas: OffscreenCanvascanvas); this.Demo.spriteSheet: SpriteSheet | null
@type{SpriteSheet | null}
spriteSheet
= new new SpriteSheet(image: HTMLImageElement | null, size?: Vector2i): SpriteSheet
Creates a sprite sheet from a loaded image. Use the static load() method for easier loading from URL.
@paramimage - Pre-loaded HTMLImageElement, or null for raw indexed data sheets.@paramsize - Explicit dimensions (required when image is null).
SpriteSheet
(const image: anyimage);
this.Demo.spriteSheet: SpriteSheet
@type{SpriteSheet | null}
spriteSheet
.SpriteSheet.indexize(palette: Palette): void
Converts 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.
@parampalette - Active palette used for color-to-index mapping.@throwsIf any opaque pixel's color is not present in the palette.
indexize
(this.Demo.palette: Palette
@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
.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
);
console.log(`[AnimationDemo] Built walk sheet: ${const canvas: OffscreenCanvascanvas.width}x${const canvas: OffscreenCanvascanvas.height}px, 4 frames`); } catch (function (local var) error: unknownerror) { console.error('[AnimationDemo] Failed to build walk sheet:', function (local var) error: unknownerror); return false; } console.log('[AnimationDemo] Initialization complete!'); return true; } /** * Runs at a fixed rate (60 times per second) to: * 1. Advance the state machine (Idle -> Walking -> Jumping cycle). * 2. Count down the cooldown timer. * 3. Spawn new particles and age existing ones. * 4. Update each particle's palette slot with its current color. */ Demo.update(): void
Runs at a fixed rate (60 times per second) to: 1. Advance the state machine (Idle -> Walking -> Jumping cycle). 2. Count down the cooldown timer. 3. Spawn new particles and age existing ones. 4. Update each particle's palette slot with its current color.
update
() {
const const tick: numbertick =
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
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
;
// Auto-cycle through states every 2 seconds (120 ticks at 60 FPS). this.Demo.autoCycleStates(tick: number): void
Automatically cycles through Idle -> Walking -> Jumping every 2 seconds each. The full cycle is 6 seconds (360 ticks at 60 FPS).
@paramtick - Current tick count.
autoCycleStates
(const tick: numbertick);
// Count down the cooldown. It never goes below zero. if (this.Demo.abilityCooldownTicks: numberabilityCooldownTicks > 0) { this.Demo.abilityCooldownTicks: numberabilityCooldownTicks--; } // Spawn a particle batch every 180 ticks. if (this.Demo.spawnTimer: TimerspawnTimer.Timer.fireIfElapsed(currentTick?: number): boolean
Returns true once per interval and advances the internal last-fired tick.
@paramcurrentTick - Tick to evaluate against; defaults to engine tick counter.@returnsTrue when at least `intervalTicks` have elapsed since the last fire/reset.
fireIfElapsed
(const tick: numbertick)) {
this.Demo.spawnParticle(): void
Spawns a new particle near the rock's position. Each particle gets its own reserved palette slot from the rotating pool.
spawnParticle
();
} // Remove dead particles (older than 3 seconds = 180 ticks). // Array.filter returns a new array with only the entries that pass the test. this.Demo.particles: {}particles = this.Demo.particles: {}particles.filter((p: anyp) => const tick: numbertick - p: anyp.spawnTick < 180); // Update each particle's palette slot with its current color. // The hue is fixed at spawn time; only alpha changes as the particle ages. for (const const p: anyp of this.Demo.particles: {}particles) { const const age: numberage = const tick: numbertick - const p: anyp.spawnTick; const const lifetime: 180lifetime = 180; // t goes from 0 (just born) to 1 (fully aged, about to disappear). // Think of it like a candle burning down: 0 is a fresh candle, 1 is gone. const const t: numbert = const age: numberage / const lifetime: 180lifetime; // applyEasing(t, 'ease-in') starts slow and accelerates toward the end. // Subtracting from 1 flips it: alpha stays high for most of the particle's life, // then drops quickly right before it disappears - like a real spark that glows // brightly, then winks out all at once instead of fading evenly. const const alpha: anyalpha = Math.floor(255 * (1 - function applyEasing(t: number, easing: EasingFunction): number
Applies an easing curve to a normalized time value.
@since1.0.3@changed1.5.0 Added sine, cubic, quartic, quintic, expo, circ, back, elastic, and bounce families.@paramt - Normalized time in [0, 1]. Values outside this range are not clamped.@parameasing - Easing curve to apply.@returnsEased value. Guaranteed to return 0 for `t = 0` and 1 for `t = 1`.
applyEasing
(const t: numbert, 'ease-in')));
// Hue is based on when the particle was spawned - no two batches look the same. const const hue: numberhue = (const p: anyp.spawnTick * 3) % 360; // Compute the color and write it into this particle's reserved palette slot. const const color: Color32color = class Color32
Mutable 32-bit RGBA color value with 8-bit channels.
@since0.1.0
Color32
.Color32.fromHSL(h: number, s: number, l: number, a?: number): Color32
Creates a color from HSL values.
@paramh - Hue in degrees (0-360).@params - Saturation as percentage (0-100).@paraml - Lightness as percentage (0-100).@parama - Alpha channel (0-255, defaults to 255).@returnsNew color converted from HSL values.
fromHSL
(const hue: numberhue, 100, 60);
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 p: anyp.paletteSlot, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(const color: Color32color.Color32.r: number
Red channel (0-255).
r
, const color: Color32color.Color32.g: number
Green channel (0-255).
g
, const color: Color32color.Color32.b: number
Blue channel (0-255).
b
, const alpha: anyalpha));
} // Update rock position in the Walking and Jumping states. this.Demo.updateRockPosition(): void
Moves the rock based on the current state. Walk: slide left/right; Idle/Jump: handled via jump arc in render.
updateRockPosition
();
} /** * Runs once per screen refresh to draw the rock, particles, and UI. * Notice: NO Color32 objects appear in draw calls - only palette indices and offsets. */ Demo.render(): void
Runs once per screen refresh to draw the rock, particles, and UI. Notice: NO Color32 objects appear in draw calls - only palette indices and offsets.
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) => 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
(this.Demo.theme: nulltheme.bg);
// Green ground strip the rock stands on.
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.drawRectFill: (rect: Rect2i, paletteIndex: number) => void
Draws a filled rectangle.
@since0.1.0@paramrect - Rectangle bounds in display coordinates.@parampaletteIndex - Palette color index.
drawRectFill
(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(0, 150, 320, 90), const C_GROUND: 1C_GROUND);
// Draw the character with a shadow below it. this.Demo.renderCharacter(): void
Draws the character sprite at the correct position, with a shadow below. During Walking, srcRect cycles through walk frames based on distance from walkStartX. During Jumping, the sprite moves up in an arc while the shadow stays on the ground.
renderCharacter
();
// Large on-screen state indicator (overlay row also shows state + ticks). this.Demo.renderStateIndicator(): void
Draws the state readout so the Idle / Walking / Jumping cycle is obvious on screen: a small kit panel in the top-right corner plus a color strip on the ground edge.
renderStateIndicator
();
// Draw any active particles. this.Demo.renderParticles(): void
Draws all active particles as small colored squares. The color for each particle was already updated in update() via palette.set(). Here we just use the slot number - no Color32 needed.
renderParticles
();
// Draw the timer readouts and the concept summary (shared UI kit groups). this.Demo.renderUI(): void
Draws the timer readouts and the concept summary with the shared UI kit. State and tick count are shown in overlayRows() above the bottom FPS bar.
renderUI
();
} /** * Status row in the engine overlay: animation state (left) and tick count (right). * * @returns {readonly { leftText: string, rightText?: string }[]} */
Demo.overlayRows(): readonly {
    leftText: string;
    rightText?: string;
}[]
Status row in the engine overlay: animation state (left) and tick count (right).
@returns
overlayRows
() {
const const row: anyrow = this.Demo.overlayRowData: {}overlayRowData[0]; const row: anyrow.leftText = `State: ${this.Demo.animState: anyanimState}`; const row: anyrow.rightText = `Ticks: ${
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
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
}`;
return this.Demo.overlayRowData: {}overlayRowData; } /** * Automatically cycles through Idle -> Walking -> Jumping every 2 seconds each. * The full cycle is 6 seconds (360 ticks at 60 FPS). * * @param {number} tick - Current tick count. */ Demo.autoCycleStates(tick: number): void
Automatically cycles through Idle -> Walking -> Jumping every 2 seconds each. The full cycle is 6 seconds (360 ticks at 60 FPS).
@paramtick - Current tick count.
autoCycleStates
(tick: number
- Current tick count.
@paramtick - Current tick count.
tick
) {
// cyclePos goes 0..359, repeating. const const cyclePos: numbercyclePos = tick: number
- Current tick count.
@paramtick - Current tick count.
tick
% 360;
if (const cyclePos: numbercyclePos < 120) { // First 2 seconds: Idle. Rock sits still. if (this.Demo.animState: anyanimState !== const AnimState: anyAnimState.Idle) { this.Demo.animState: anyanimState = const AnimState: anyAnimState.Idle; } } else if (const cyclePos: numbercyclePos < 240) { // Second 2 seconds: Walking. Rock slides sideways. if (this.Demo.animState: anyanimState !== const AnimState: anyAnimState.Walking) { this.Demo.animState: anyanimState = const AnimState: anyAnimState.Walking; this.Demo.walkStartX: numberwalkStartX = this.Demo.charPos: Vector2icharPos.Vector2i.x: number
Horizontal component (defaults to 0).
x
;
this.Demo.walkDir: numberwalkDir = 1; } } else { // Last 2 seconds: Jumping. Rock follows a sine arc. if (this.Demo.animState: anyanimState !== const AnimState: anyAnimState.Jumping) { this.Demo.animState: anyanimState = const AnimState: anyAnimState.Jumping; this.Demo.jumpStartTick: numberjumpStartTick = tick: number
- Current tick count.
@paramtick - Current tick count.
tick
;
// Trigger the ability cooldown at the start of a jump. if (this.Demo.abilityCooldownTicks: numberabilityCooldownTicks === 0) { this.Demo.abilityCooldownTicks: numberabilityCooldownTicks = this.Demo.abilityCooldownDuration: numberabilityCooldownDuration; } } } } /** * Moves the rock based on the current state. * Walk: slide left/right; Idle/Jump: handled via jump arc in render. */ Demo.updateRockPosition(): void
Moves the rock based on the current state. Walk: slide left/right; Idle/Jump: handled via jump arc in render.
updateRockPosition
() {
if (this.Demo.animState: anyanimState === const AnimState: anyAnimState.Walking) { // Move 1 pixel per tick; bounce off screen edges. this.Demo.charPos: Vector2icharPos.Vector2i.x: number
Horizontal component (defaults to 0).
x
+= this.Demo.walkDir: numberwalkDir;
if (this.Demo.charPos: Vector2icharPos.Vector2i.x: number
Horizontal component (defaults to 0).
x
> 220) {
this.Demo.walkDir: numberwalkDir = -1; } if (this.Demo.charPos: Vector2icharPos.Vector2i.x: number
Horizontal component (defaults to 0).
x
< 60) {
this.Demo.walkDir: numberwalkDir = 1; } } } /** * Draws the character sprite at the correct position, with a shadow below. * During Walking, srcRect cycles through walk frames based on distance from walkStartX. * During Jumping, the sprite moves up in an arc while the shadow stays on the ground. */ Demo.renderCharacter(): void
Draws the character sprite at the correct position, with a shadow below. During Walking, srcRect cycles through walk frames based on distance from walkStartX. During Jumping, the sprite moves up in an arc while the shadow stays on the ground.
renderCharacter
() {
let let srcRect: anysrcRect = this.Demo.walkFrames: {}walkFrames[0]; if (this.Demo.animState: anyanimState === const AnimState: anyAnimState.Walking) { // walkStartX is where the walk began; every few pixels, advance to the next frame. const const steps: anysteps = Math.abs(this.Demo.charPos: Vector2icharPos.Vector2i.x: number
Horizontal component (defaults to 0).
x
- this.Demo.walkStartX: numberwalkStartX);
const const walkFrame: numberwalkFrame = 1 + (Math.floor(const steps: anysteps / 3) % 3); let srcRect: anysrcRect = this.Demo.walkFrames: {}walkFrames[const walkFrame: numberwalkFrame]; } // Calculate the vertical offset for the jump arc. let let yOffset: numberyOffset = 0; if (this.Demo.animState: anyanimState === const AnimState: anyAnimState.Jumping) { const const jumpProgress: numberjumpProgress = (
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
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
- this.Demo.jumpStartTick: numberjumpStartTick) / this.Demo.jumpDuration: numberjumpDuration;
let yOffset: numberyOffset = -Math.abs(Math.sin(const jumpProgress: numberjumpProgress * Math.PI) * 35); } const const drawPos: Vector2idrawPos = 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
(this.Demo.charPos: Vector2icharPos.Vector2i.x: number
Horizontal component (defaults to 0).
x
, this.Demo.charPos: Vector2icharPos.Vector2i.y: number
Vertical component (defaults to 0).
y
+ Math.floor(let yOffset: numberyOffset));
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) => void
Draws 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.
@since0.1.0@paramspriteSheet - Indexed sprite sheet.@paramsrcRect - Source rectangle within the sprite sheet, in pixels.@paramdestPos - Destination top-left position in display coordinates.@parampaletteOffset - Shift added to every stored pixel index before palette lookup (default 0).@exampleBT.drawSprite(sheet, new Rect2i(0, 0, 16, 16), new Vector2i(10, 10)); BT.drawSprite(sheet, new Rect2i(0, 0, 16, 16), new Vector2i(10, 10), 16); // blue team
drawSprite
(this.Demo.spriteSheet: SpriteSheet | null
@type{SpriteSheet | null}
spriteSheet
, let srcRect: anysrcRect, const drawPos: Vector2idrawPos, 0);
const const shadowY: numbershadowY = this.Demo.charPos: Vector2icharPos.Vector2i.y: number
Vertical component (defaults to 0).
y
+ let srcRect: anysrcRect.height - 4;
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
(this.Demo.charPos: Vector2icharPos.Vector2i.x: number
Horizontal component (defaults to 0).
x
+ 3, const shadowY: numbershadowY, let srcRect: anysrcRect.width - 6, 4), const C_SHADOW: 2C_SHADOW);
} /** * Draws the state readout so the Idle / Walking / Jumping cycle is obvious on screen: * a small kit panel in the top-right corner plus a color strip on the ground edge. */ Demo.renderStateIndicator(): void
Draws the state readout so the Idle / Walking / Jumping cycle is obvious on screen: a small kit panel in the top-right corner plus a color strip on the ground edge.
renderStateIndicator
() {
// Pick the scene slot for the strip and the kit text role for the panel label. // Idle is calm gray/dim, Walking is "go" green, Jumping is alert red/orange. let let stripColor: numberstripColor = const C_STATE_IDLE: 3C_STATE_IDLE; let let stateRole: stringstateRole = 'dim'; if (this.Demo.animState: anyanimState === const AnimState: anyAnimState.Walking) { let stripColor: numberstripColor = const C_STATE_WALK: 4C_STATE_WALK; let stateRole: stringstateRole = 'accent'; } else if (this.Demo.animState: anyanimState === const AnimState: anyAnimState.Jumping) { let stripColor: numberstripColor = const C_STATE_JUMP: 5C_STATE_JUMP; let stateRole: stringstateRole = 'warm'; } // A small bordered kit panel in the top-right corner names the current state. import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_RIGHT); import uiui.panel('State'); import uiui.label(this.Demo.animState: anyanimState, { color: stringcolor: let stateRole: stringstateRole }); import uiui.end(); // A matching color strip painted right into the scene, on the ground edge, so // you can see the state change even without reading the panel text.
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
(218, 152, 96, 6), let stripColor: numberstripColor);
import uiui.caption(218, 161, 'State machine', { color: stringcolor: 'dim' }); } /** * Spawns a new particle near the rock's position. * Each particle gets its own reserved palette slot from the rotating pool. */ Demo.spawnParticle(): void
Spawns a new particle near the rock's position. Each particle gets its own reserved palette slot from the rotating pool.
spawnParticle
() {
// Rotate through 20 slots in a circle. // When the pool wraps around, old particles' slots get reused. const const slot: numberslot = const PARTICLE_SLOT_START: 50PARTICLE_SLOT_START + (this.Demo.nextParticleSlot: numbernextParticleSlot % const MAX_PARTICLES: 20MAX_PARTICLES); this.Demo.nextParticleSlot: numbernextParticleSlot++; // Scatter the particle around the rock. BT.random is the engine's shared random number generator, and int() // returns a whole number starting at the first value and stopping just before the second - so int(-5, 25) // gives anything from -5 to 24, and 25 itself never comes up. // Both ranges are deliberately lopsided. Most of the x offsets are positive, so dust trails off to the right, // and most of the y offsets are negative, which on screen means upward - the dust rises off the rock. const const x: numberx = this.Demo.charPos: Vector2icharPos.Vector2i.x: number
Horizontal component (defaults to 0).
x
+
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
.random: Random
Default engine PRNG (live reference - not a copy). Time-seeded when the engine singleton is created. Call {@link BT.randomSeed } for a reproducible run. Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.
@since1.5.0@returnsThe shared {@link Random} instance.@exampleBT.randomSeed(42); BT.random.int(150, 420); BT.random.pick(['a', 'b', 'c']);
random
.Random.int(minOrMaxExclusive: number, maxExclusive?: number): number
Returns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).
@paramminOrMaxExclusive - When alone, exclusive upper bound from 0; otherwise inclusive min.@parammaxExclusive - Exclusive upper bound when two arguments are passed.@returnsWhole number in the half-open range.@since1.5.0
int
(-5, 25);
const const y: numbery = this.Demo.charPos: Vector2icharPos.Vector2i.y: number
Vertical component (defaults to 0).
y
+
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
.random: Random
Default engine PRNG (live reference - not a copy). Time-seeded when the engine singleton is created. Call {@link BT.randomSeed } for a reproducible run. Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.
@since1.5.0@returnsThe shared {@link Random} instance.@exampleBT.randomSeed(42); BT.random.int(150, 420); BT.random.pick(['a', 'b', 'c']);
random
.Random.int(minOrMaxExclusive: number, maxExclusive?: number): number
Returns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).
@paramminOrMaxExclusive - When alone, exclusive upper bound from 0; otherwise inclusive min.@parammaxExclusive - Exclusive upper bound when two arguments are passed.@returnsWhole number in the half-open range.@since1.5.0
int
(-15, 5);
this.Demo.particles: {}particles.push({ pos: Vector2ipos: 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 x: numberx, const y: numbery),
spawnTick: numberspawnTick:
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
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
,
paletteSlot: numberpaletteSlot: const slot: numberslot, // This particle "owns" this palette slot. }); } /** * Draws all active particles as small colored squares. * The color for each particle was already updated in update() via palette.set(). * Here we just use the slot number - no Color32 needed. */ Demo.renderParticles(): void
Draws all active particles as small colored squares. The color for each particle was already updated in update() via palette.set(). Here we just use the slot number - no Color32 needed.
renderParticles
() {
for (const const p: anyp of this.Demo.particles: {}particles) { // Draw a 4x4 square. The color is whatever update() put in p.paletteSlot.
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 p: anyp.pos.x - 2, const p: anyp.pos.y - 2, 4, 4), const p: anyp.paletteSlot);
} } /** * Draws the timer readouts and the concept summary with the shared UI kit. * State and tick count are shown in overlayRows() above the bottom FPS bar. */ Demo.renderUI(): void
Draws the timer readouts and the concept summary with the shared UI kit. State and tick count are shown in overlayRows() above the bottom FPS bar.
renderUI
() {
// How much of the cooldown is still left, as a fraction from 0 (ready) to 1 (full). const const cooldownPercent: anycooldownPercent = Math.max(0, this.Demo.abilityCooldownTicks: numberabilityCooldownTicks / this.Demo.abilityCooldownDuration: numberabilityCooldownDuration); // Ask the timer how many ticks are left until the next spawn event. const const ticksUntilSpawn: numberticksUntilSpawn = this.Demo.spawnTimer: TimerspawnTimer.Timer.remainingTicks(currentTick?: number): number
Returns ticks remaining until the timer will fire.
@paramcurrentTick - Tick to compare against; defaults to engine tick counter.@returnsRemaining ticks in `[0, intervalTicks]`.
remainingTicks
(
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
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
);
// Timer readouts: a borderless kit group pinned near the top-left corner. // { x, y } pin the group's top-left corner; the kit stacks the rows below it. import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { x: numberx: 4, y: numbery: 22 }); // Orange while counting down, green once the ability is ready again. // Math.ceil rounds up, so "1s" shows until the very last tick of the cooldown. const const cooldownSecs: anycooldownSecs = Math.ceil(this.Demo.abilityCooldownTicks: numberabilityCooldownTicks / 60); import uiui.label(`Cooldown: ${const cooldownSecs: anycooldownSecs}s`, { color: stringcolor: const cooldownPercent: anycooldownPercent > 0 ? 'warm' : 'accent' }); // A read-only kit meter replaces the old hand-drawn cooldown bar rectangles. import uiui.meter(null, const cooldownPercent: anycooldownPercent, { color: stringcolor: 'warm', width: numberwidth: 100 }); import uiui.label(`Next spawn: ${Math.ceil(const ticksUntilSpawn: numberticksUntilSpawn / 60)}s`, { color: stringcolor: 'header' }); import uiui.label(`Particles: ${this.Demo.particles: {}particles.length}`, { color: stringcolor: 'dim' }); import uiui.end(); // Concept summary: another borderless kit group, pinned over the ground strip. import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { x: numberx: 4, y: numbery: 162 }); import uiui.label('Tick-based timing:', { color: stringcolor: 'header' }); import uiui.label('- Tick-based timing (update rate)', { color: stringcolor: 'dim' }); import uiui.label('- Cooldown & event scheduling', { color: stringcolor: 'dim' }); import uiui.label('- State machine transitions', { color: stringcolor: 'dim' }); import uiui.end(); } } // Hand the Demo class to BLIT386 to start the demo loop. function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
(class Demo
Demonstrates tick-based animation timing and state management. Shows state machines, cooldowns, periodic particle events, and jump arcs. The "character" is the rock sprite from test.png.
@implementsIBTDemo
Demo
);