// 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): numberApplies an easing curve to a normalized time value.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.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 TimerFixed-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.Timer, 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 */
// 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): voidDraws one character pose into a walk-strip cell.
Frame 0 = idle; frames 1-3 = walk cycle with alternating leg positions.drawWalkFrame(ctx: OffscreenCanvasRenderingContext2Dctx, frameIndex: numberframeIndex) {
const const ox: numberox = frameIndex: numberframeIndex * const WALK_FRAME_W: 18WALK_FRAME_W;
const const bodyColor: "#b0b0c8"bodyColor = '#b0b0c8';
const const legColor: "#8080a0"legColor = '#8080a0';
ctx: OffscreenCanvasRenderingContext2Dctx.fillStyle = const bodyColor: "#b0b0c8"bodyColor;
ctx: OffscreenCanvasRenderingContext2Dctx.fillRect(const ox: numberox + 5, 4, 8, 8);
ctx: OffscreenCanvasRenderingContext2Dctx.fillStyle = const legColor: "#8080a0"legColor;
if (frameIndex: numberframeIndex === 0) {
// Idle: feet together under the body.
ctx: OffscreenCanvasRenderingContext2Dctx.fillRect(const ox: numberox + 6, 12, 3, 4);
ctx: OffscreenCanvasRenderingContext2Dctx.fillRect(const ox: numberox + 9, 12, 3, 4);
} else if (frameIndex: numberframeIndex === 1) {
// Left foot forward.
ctx: OffscreenCanvasRenderingContext2Dctx.fillRect(const ox: numberox + 4, 12, 3, 4);
ctx: OffscreenCanvasRenderingContext2Dctx.fillRect(const ox: numberox + 10, 13, 3, 3);
} else if (frameIndex: numberframeIndex === 2) {
// Mid stride: feet under hips.
ctx: OffscreenCanvasRenderingContext2Dctx.fillRect(const ox: numberox + 6, 12, 3, 4);
ctx: OffscreenCanvasRenderingContext2Dctx.fillRect(const ox: numberox + 9, 12, 3, 4);
} else {
// Right foot forward.
ctx: OffscreenCanvasRenderingContext2Dctx.fillRect(const ox: numberox + 5, 13, 3, 3);
ctx: OffscreenCanvasRenderingContext2Dctx.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.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): voidDraws one character pose into a walk-strip cell.
Frame 0 = idle; frames 1-3 = walk cycle with alternating leg positions.drawWalkFrame(const ctx: anyctx, let f: numberf);
const frames: {}frames.push(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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 DemoDemonstrates 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.Demo {
// The palette holds all colors used in this demo.
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = null;
// The sprite sheet loaded from /sprites/test.png.
/** @type {SpriteSheet | null} */
Demo.spriteSheet: SpriteSheet | nullspriteSheet = 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): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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): TimerCreates a timer that fires once per fixed-tick interval.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.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.init() {
console.log('[AnimationDemo] Initializing...');
// Create the palette and fill the scene colors (ground, shadow, state indicator).
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);
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_GROUND: 1C_GROUND, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(40, 60, 40));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_SHADOW: 2C_SHADOW, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(0, 0, 0, 100));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_STATE_IDLE: 3C_STATE_IDLE, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(150, 150, 150));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_STATE_WALK: 4C_STATE_WALK, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(100, 255, 100));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_STATE_JUMP: 5C_STATE_JUMP, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.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: Palettepalette);
// 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: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const PARTICLE_SLOT_START: 50PARTICLE_SLOT_START + let i: numberi, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.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.buildWalkSheet();
this.Demo.walkFrames: {}walkFrames = const frames: {}frames;
import registerCanvasColorsregisterCanvasColors(this.Demo.palette: Palettepalette, 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 | nullspriteSheet = 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.spriteSheet: SpriteSheetspriteSheet.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(`[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(): voidRuns 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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks;
// Auto-cycle through states every 2 seconds (120 ticks at 60 FPS).
this.Demo.autoCycleStates(tick: number): voidAutomatically cycles through Idle -> Walking -> Jumping every 2 seconds each.
The full cycle is 6 seconds (360 ticks at 60 FPS).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): booleanReturns true once per interval and advances the internal last-fired tick.fireIfElapsed(const tick: numbertick)) {
this.Demo.spawnParticle(): voidSpawns 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): numberApplies an easing curve to a normalized time value.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 Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32.Color32.fromHSL(h: number, s: number, l: number, a?: number): Color32Creates a color from HSL values.fromHSL(const hue: numberhue, 100, 60);
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const p: anyp.paletteSlot, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(const color: Color32color.Color32.r: numberRed channel (0-255).r, const color: Color32color.Color32.g: numberGreen channel (0-255).g, const color: Color32color.Color32.b: numberBlue channel (0-255).b, const alpha: anyalpha));
}
// Update rock position in the Walking and Jumping states.
this.Demo.updateRockPosition(): voidMoves 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(): voidRuns 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) => 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);
// 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) => voidDraws a filled rectangle.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(0, 150, 320, 90), const C_GROUND: 1C_GROUND);
// Draw the character with a shadow below it.
this.Demo.renderCharacter(): voidDraws 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(): voidDraws 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(): voidDraws 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(): voidDraws 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).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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.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): voidAutomatically cycles through Idle -> Walking -> Jumping every 2 seconds each.
The full cycle is 6 seconds (360 ticks at 60 FPS).autoCycleStates(tick: number- Current tick count.tick) {
// cyclePos goes 0..359, repeating.
const const cyclePos: numbercyclePos = tick: number- 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: numberHorizontal 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.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(): voidMoves 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: numberHorizontal component (defaults to 0).x += this.Demo.walkDir: numberwalkDir;
if (this.Demo.charPos: Vector2icharPos.Vector2i.x: numberHorizontal component (defaults to 0).x > 220) {
this.Demo.walkDir: numberwalkDir = -1;
}
if (this.Demo.charPos: Vector2icharPos.Vector2i.x: numberHorizontal 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(): voidDraws 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: numberHorizontal 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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.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): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(this.Demo.charPos: Vector2icharPos.Vector2i.x: numberHorizontal component (defaults to 0).x, this.Demo.charPos: Vector2icharPos.Vector2i.y: numberVertical 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) => 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.spriteSheet: SpriteSheet | nullspriteSheet, let srcRect: anysrcRect, const drawPos: Vector2idrawPos, 0);
const const shadowY: numbershadowY = this.Demo.charPos: Vector2icharPos.Vector2i.y: numberVertical 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) => voidDraws a filled rectangle.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(this.Demo.charPos: Vector2icharPos.Vector2i.x: numberHorizontal 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(): voidDraws 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) => voidDraws a filled rectangle.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(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(): voidSpawns 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: numberHorizontal 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: RandomDefault 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.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(-5, 25);
const const y: numbery = this.Demo.charPos: Vector2icharPos.Vector2i.y: numberVertical 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: RandomDefault 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.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(-15, 5);
this.Demo.particles: {}particles.push({
pos: Vector2ipos: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.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(): voidDraws 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) => voidDraws a filled rectangle.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(const 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(): voidDraws 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): numberReturns ticks remaining until the timer will fire.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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.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.bootstrap(class DemoDemonstrates 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.Demo);