/*
Animation: tick-based timing for walk cycles, state machines, cooldowns, and particles.
@description Tick-based animation: cycle walk frames, drive a small state machine, and spawn particles that fade.
Part of the BLIT386 demo series.
Prerequisites:
- Basics: https://demos.blit386.dev/basics
- Primitives: https://demos.blit386.dev/primitives
- Sprites: https://demos.blit386.dev/sprites
Live version: https://demos.blit386.dev/animation
Guides:
- Game Loop: https://blit386.dev/docs/guides/game-loop#why-motion-can-look-stale-or-jittery
- Palette: https://blit386.dev/docs/guides/palette#layout-swap-vs-value-swap
- Overlay: https://blit386.dev/docs/guides/overlay#custom-rows
- Random: https://blit386.dev/docs/guides/random#deterministic-by-default
Reference:
- Game Loop: https://blit386.dev/docs/api/game-loop#timer
- Easing: https://blit386.dev/docs/api/easing#applyeasing
- Core Types: https://blit386.dev/docs/api/core-types#color32
- Overlay: https://blit386.dev/docs/api/overlay#custom-rows-optional
---
WHAT YOU WILL SEE
-----------------
A small gray walker - an 8x8 body with one eye on two short legs, built on an offscreen
canvas and drawn at twice its size - stands on a dark green ground strip along the bottom
of a 320x240 screen, with a soft shadow under its feet. Every 2 seconds it changes state,
in a loop that repeats every 6 seconds:
1. Idle - it stands still.
2. Walking - it slides sideways 1 pixel per tick, legs cycling through three poses,
turning around (and facing the other way) whenever it reaches x = 64 or x = 220.
3. Jumping - it rises along a smooth arc up to 40 pixels high and lands again after one
second, twice in a row, while its shadow stays on the ground and shrinks as it rises.
Two UI kit panels sit along the top edge. Timers, on the left, shows an ability cooldown
(a countdown and a meter), the ticks until the next particle spawn, and how many
particles are alive. State, on the right, lists the three states with a lit pip next to
the current one. On the ground strip, a short list names the concepts on show.
Every 45 ticks (three quarters of a second) a burst of 32 tiny sparks shoots up from the
walker's feet like a fountain, arcs over under gravity, and falls back to the ground. Each
burst has its own hue, tied to the moment it spawned. A spark glows for most of its
1.5-second life and then winks out quickly. The engine overlay gets a custom row with the
state and the tick count, and the timing chart is on.
WHAT YOU WILL LEARN
-------------------
- BT.ticks goes up once per update() call at a fixed rate (targetFPS), not once per screen
refresh. All timing here compares BT.ticks with a stored start tick; at 60 ticks per
second, one tick is 1/60 of a second.
- A state machine: AnimState names the three states, and update() switches between them
on a 360-tick cycle with `tick % 360`.
- A cooldown timer: a tick count that counts down to zero, shown as seconds and as a
ui.meter().
- Periodic events with the Timer class: new Timer(45), timer.fireIfElapsed(tick), and
timer.remainingTicks(tick).
- Simple particle physics: each spark has a velocity, and gravity adds a little to its
downward speed every tick, which bends its path into an arc.
- A jump arc from Math.sin(): progress from 0 to 1 becomes a smooth hill.
- Palette animation for particles: each particle owns a palette slot, update() writes its
current color there with palette.set(), and render() just passes the slot number.
- applyEasing(t, 'ease-in') and Color32.fromHSL(hue, saturation, lightness) for the fade
and the hue.
- Building a four-frame walk strip on an OffscreenCanvas and cutting frames out of it with
sheet.tileRect().
- Drawing one sprite at twice its size, mirrored to face the way it walks, with the
params form of BT.drawSprite(): { scale: 2, flags: BT.FLIP_H }.
HOW PARTICLE COLORS WORK
------------------------
Each particle gets its own palette slot (50-177, a rotating pool of 128) the moment it is
spawned. In update(), the color is computed - hue picked at spawn time, alpha from age -
and written into that slot with palette.set(). In render(), the particle is drawn with
the slot number alone; no Color32 objects are needed there. The Palette Animation demo
explores this idea in depth: https://demos.blit386.dev/palette-animation
TRY THIS
--------
- Press the backquote key (`) to open the engine overlay. The custom row shows the state
on the left and the tick count on the right - watch the state flip every 120 ticks.
- Watch the cooldown: it starts at 2 seconds when a jump begins, turns from orange to
green as it reaches zero, and the meter empties with it.
*/
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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT, class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32, class Rect2iInteger rectangle for pixel-perfect bounds and regions.
Used throughout the engine for sprite regions, display-space bounds, and
zero-allocation geometry helpers. Both convenience getters and allocation-free
`*To()` helpers are provided so callers can choose between readability and
hot-path efficiency.Rect2i, 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').Palette} Palette */
/** @typedef {import('blit386').SpriteSheet} SpriteSheet */
// AnimState names the three states the walker can be in. Object.freeze prevents these
// values from being changed by accident.
const const AnimState: anyAnimState = Object.freeze({
type Idle: stringIdle: 'Idle', // standing still
type Walking: stringWalking: 'Walking', // sliding across the screen
type Jumping: stringJumping: 'Jumping', // following a jump arc
});
// How many particles can be alive at once. Each gets its own palette slot. A burst lives
// for two spawn intervals, so at most about 64 are alive - 128 slots leave plenty of
// room.
const const MAX_PARTICLES: 128MAX_PARTICLES = 128;
// Every SPAWN_INTERVAL ticks, BURST_SIZE particles shoot up from the walker's feet.
const const SPAWN_INTERVAL: 45SPAWN_INTERVAL = 45;
const const BURST_SIZE: 32BURST_SIZE = 32;
// How many ticks one particle lives (90 ticks = 1.5 seconds at 60 FPS).
const const PARTICLE_LIFETIME: 90PARTICLE_LIFETIME = 90;
// How much faster a particle falls each tick, in pixels per tick. Small numbers make
// floaty sparks, big numbers make them drop like stones.
const const GRAVITY: 0.06GRAVITY = 0.06;
// Where in the palette particle colors are stored (slots 50-177).
const const PARTICLE_SLOT_START: 50PARTICLE_SLOT_START = 50;
// The states in the order the State panel lists them (and the order they play in).
const const STATE_ORDER: {}STATE_ORDER = [const AnimState: anyAnimState.Idle, const AnimState: anyAnimState.Walking, const AnimState: anyAnimState.Jumping];
// Where the walker's colors start (slot 20 and up; the canvas has three colors).
const const SPRITE_BASE: 20SPRITE_BASE = 20;
// Walk animation: four 18x18 frames 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;
// The walker is drawn at twice its size, so each canvas pixel becomes a 2x2 block.
const const WALKER_SCALE: 2WALKER_SCALE = 2;
// The y coordinate of the ground's top edge. The walker's feet rest on this line.
const const GROUND_Y: 172GROUND_Y = 172;
// The walker turns around when its left edge reaches one of these x positions. The
// range is centered: 64 pixels of room on the left, 64 on the right of the 36-pixel-wide
// walker.
const const WALK_MIN_X: 64WALK_MIN_X = 64;
const const WALK_MAX_X: 220WALK_MAX_X = 220;
// 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 walker
const const C_GROUND_EDGE: 3C_GROUND_EDGE = 3; // (72, 104, 60) lighter green line along the ground's top edge
// 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; // deep navy screen background
const const UI_HEADER: 246UI_HEADER = 246; // warm amber (render bars, chart tags)
const const UI_ACCENT: 247UI_ACCENT = 247; // phosphor green (update bars)
const const UI_WARM: 248UI_WARM = 248; // orange (chart warning and error frames)
const const UI_INFO: 249UI_INFO = 249; // light blue (overlay row text)
// Draws one frame of the walker onto the canvas: a body square with one eye, plus two
// legs whose positions change with `frameIndex` (0 idle, 1 left foot forward, 2 mid
// stride, 3 right foot forward). The eye sits on the right, so the walker faces right;
// renderCharacter() mirrors it to face left.
function function drawWalkFrame(ctx: any, frameIndex: any): voiddrawWalkFrame(ctx: anyctx, frameIndex: anyframeIndex) {
const const ox: numberox = frameIndex: anyframeIndex * const WALK_FRAME_W: 18WALK_FRAME_W;
const const bodyColor: "#b0b0c8"bodyColor = '#b0b0c8';
const const legColor: "#8080a0"legColor = '#8080a0';
const const eyeColor: "#1a1c2c"eyeColor = '#1a1c2c';
ctx: anyctx.fillStyle = const bodyColor: "#b0b0c8"bodyColor;
ctx: anyctx.fillRect(const ox: numberox + 5, 4, 8, 8);
ctx: anyctx.fillStyle = const eyeColor: "#1a1c2c"eyeColor;
ctx: anyctx.fillRect(const ox: numberox + 10, 6, 2, 2);
ctx: anyctx.fillStyle = const legColor: "#8080a0"legColor;
if (frameIndex: anyframeIndex === 0) {
// Idle: feet apart under the body, lined up with the body edges.
ctx: anyctx.fillRect(const ox: numberox + 5, 12, 3, 4);
ctx: anyctx.fillRect(const ox: numberox + 10, 12, 3, 4);
} else if (frameIndex: anyframeIndex === 1) {
// Left foot forward.
ctx: anyctx.fillRect(const ox: numberox + 4, 12, 3, 4);
ctx: anyctx.fillRect(const ox: numberox + 10, 13, 3, 3);
} else if (frameIndex: anyframeIndex === 2) {
// Mid stride: feet under the hips.
ctx: anyctx.fillRect(const ox: numberox + 6, 12, 3, 4);
ctx: anyctx.fillRect(const ox: numberox + 9, 12, 3, 4);
} else {
// Right foot forward.
ctx: anyctx.fillRect(const ox: numberox + 5, 13, 3, 3);
ctx: anyctx.fillRect(const ox: numberox + 11, 12, 3, 4);
}
}
// Builds the walk strip: an offscreen canvas with all four frames side by side. Returns
// the canvas and its 2D drawing context. Throws if the browser cannot give us a 2D
// context.
function function buildWalkSheet(): {
canvas: any;
ctx: any;
}
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);
for (let let f: numberf = 0; let f: numberf < const WALK_FRAME_COUNT: 4WALK_FRAME_COUNT; let f: numberf++) {
function drawWalkFrame(ctx: any, frameIndex: any): voiddrawWalkFrame(const ctx: anyctx, let f: numberf);
}
return { canvas: anycanvas, ctx: anyctx };
}
// Tick-based animation timing and state management: a state machine, a cooldown, periodic
// particle spawns, and a jump arc, all driven by BT.ticks.
class class DemoDemo {
// The palette holds all colors used in this demo.
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = null;
// The walk strip built in init() on an offscreen canvas.
/** @type {SpriteSheet | null} */
Demo.spriteSheet: SpriteSheet | nullspriteSheet = null;
// One Rect2i per walk-strip frame (idle + three walk poses).
Demo.walkFrames: {}walkFrames = [];
// Which state the walker is in right now.
Demo.animState: anyanimState = const AnimState: anyAnimState.Idle;
// The walker's position on screen (top-left corner of its frame). Its feet end 16
// canvas rows below the frame's top, so at 2x the frame's top sits 32 pixels above
// the ground line.
Demo.charPos: Vector2icharPos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(80, const GROUND_Y: 172GROUND_Y - 16 * const WALKER_SCALE: 2WALKER_SCALE);
// 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 45 ticks (0.75 seconds at 60 FPS) to spawn the next burst.
Demo.spawnTimer: TimerspawnTimer = new new Timer(intervalTicks: number): TimerCreates a timer that fires once per fixed-tick interval.Timer(const SPAWN_INTERVAL: 45SPAWN_INTERVAL);
// The active particles: each has a position (x, y), a velocity (vx, vy), a
// spawnTick, a hue, and a paletteSlot. Position and velocity are fractional numbers
// so slow sparks still move smoothly; they are rounded to whole pixels only when
// drawn.
Demo.particles: {}particles = [];
// Counts spawned particles so the palette slots are handed out in a circle.
Demo.nextParticleSlot: numbernextParticleSlot = 0;
// When the current jump started (used to calculate the arc height).
Demo.jumpStartTick: numberjumpStartTick = 0;
// How many ticks one hop takes from launch to landing (1 second = 60 ticks). The
// Jumping state lasts 2 seconds, so the walker hops twice.
Demo.jumpDuration: numberjumpDuration = 60;
// The walker's horizontal direction (+1 = right, -1 = left).
Demo.walkDir: numberwalkDir = 1;
// Where the current walk started; the leg pose is picked from the distance walked.
Demo.walkStartX: numberwalkStartX = 80;
// Reused every frame for BT.drawSprite(): the scale stays at 2, and flags flips to
// BT.FLIP_H (a mirror image) while the walker faces left.
Demo.spriteParams: {
scale: number;
flags: number;
}
spriteParams = { scale: numberscale: const WALKER_SCALE: 2WALKER_SCALE, flags: numberflags: 0 };
// 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 the overlay bars and the timing
// chart.
Demo.configure(): {
overlayStyle: {
barPaletteIndex: number;
textPaletteIndex: number;
gapPaletteIndex: number;
};
isOverlayTimingChartEnabled: boolean;
overlayTimingChartStyle: {
updateBarPaletteIndex: number;
renderBarPaletteIndex: number;
warningPaletteIndex: number;
errorPaletteIndex: number;
tagPaletteIndex: number;
};
}
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 for update() bars, matching the "ready" cooldown color.
updateBarPaletteIndex: numberupdateBarPaletteIndex: const UI_ACCENT: 247UI_ACCENT,
// Theme amber for render() bars.
renderBarPaletteIndex: numberrenderBarPaletteIndex: const UI_HEADER: 246UI_HEADER,
// Theme orange flags frames that are close to the frame budget, and the
// same orange marks frames that went over it.
warningPaletteIndex: numberwarningPaletteIndex: const UI_WARM: 248UI_WARM,
errorPaletteIndex: numbererrorPaletteIndex: const UI_WARM: 248UI_WARM,
// Theme amber for milestone labels such as BT.assignTag() calls.
tagPaletteIndex: numbertagPaletteIndex: const UI_HEADER: 246UI_HEADER,
},
};
}
// Sets up the palette and builds the walk strip.
async Demo.init(): unknowninit() {
console.log('[AnimationDemo] Initializing...');
// Create the palette and fill the scene colors (ground, its edge, shadow).
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;
... 124 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_GROUND_EDGE: 3C_GROUND_EDGE, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(72, 104, 60));
// 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-177), 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-177) 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: 128MAX_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: anycanvas, const ctx: anyctx } = function buildWalkSheet(): {
canvas: any;
ctx: any;
}
buildWalkSheet();
// Read the canvas colors into palette slots starting at SPRITE_BASE.
import registerCanvasColorsregisterCanvasColors(this.Demo.palette: Palettepalette, const ctx: anyctx, const canvas: anycanvas.width, const canvas: anycanvas.height, const SPRITE_BASE: 20SPRITE_BASE);
// Turn the canvas into an image the engine can use, then convert its pixels
// to palette slot numbers.
const const image: anyimage = await import canvasToImagecanvasToImage(const canvas: anycanvas);
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);
// The strip is WALK_FRAME_COUNT square cells; tileRect(f) cuts frame f out of
// it.
this.Demo.spriteSheet: SpriteSheetspriteSheet.SpriteSheet.tileSize: number | Vector2i | nullGets the size of one tile in this sheet's grid, or `null` when the sheet
has no grid.
The grid is what lets `BT.drawTile(sheet, index, destPos)`,
`BT.drawTile(sheet, col, row, destPos)`, and
{@link
tileRect
}
find a
tile without you computing a `Rect2i`. Tiles are counted in whole cells
from the top-left; a partial cell at the right or bottom edge is not a
tile. The returned vector is frozen - assign a new size instead of
mutating it. The grid survives a hot-reload image swap; bounds are
checked against the sheet's current size on every lookup.
Sets the size of one tile in this sheet's grid.tileSize = const WALK_FRAME_W: 18WALK_FRAME_W;
this.Demo.walkFrames: {}walkFrames = Array.from({ length: numberlength: const WALK_FRAME_COUNT: 4WALK_FRAME_COUNT }, (_: any_, f: anyf) => this.Demo.spriteSheet: SpriteSheet | nullspriteSheet.SpriteSheet.tileRect(index: number): Rect2i (+1 overload)Returns the source rectangle of one tile in this sheet's grid.
Allocates a new `Rect2i` per call - build frame lists with it in
`init()`. For per-frame drawing, `BT.drawTile` does the same lookup
without allocating.tileRect(f: anyf));
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;
... 124 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: anycanvas.width}x${const canvas: anycanvas.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). Advances the state machine, counts down
// the cooldown, spawns and ages particles, and writes each particle's current color
// into its palette slot.
Demo.update(): voidupdate() {
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;
... 124 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 the states every 2 seconds (120 ticks at 60 FPS).
this.Demo.autoCycleStates(tick: any): voidautoCycleStates(const tick: numbertick);
// Count down the cooldown. It never goes below zero.
if (this.Demo.abilityCooldownTicks: numberabilityCooldownTicks > 0) {
this.Demo.abilityCooldownTicks: numberabilityCooldownTicks--;
}
// Spawn a burst of particles every SPAWN_INTERVAL 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.spawnBurst(): voidspawnBurst();
}
// Move every particle: add its velocity to its position, then let gravity pull a
// little harder on it. Adding to vy each tick is what bends a straight line into
// an arc - the spark slows as it rises, stops at the top, and falls faster and
// faster on the way down.
for (const const p: anyp of this.Demo.particles: {}particles) {
const p: anyp.x += const p: anyp.vx;
const p: anyp.y += const p: anyp.vy;
const p: anyp.vy += const GRAVITY: 0.06GRAVITY;
}
// Remove dead particles: too old, or fallen back down to the ground's edge (a 2x2
// spark at y = GROUND_Y - 1 would already cover the edge line). 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 < const PARTICLE_LIFETIME: 90PARTICLE_LIFETIME && p: anyp.y < const GROUND_Y: 172GROUND_Y - 1);
// Update each particle's palette slot with its current color. The hue is fixed at
// spawn time; only the 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;
// 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 PARTICLE_LIFETIME: 90PARTICLE_LIFETIME;
// 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')));
// 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 p: anyp.hue, 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));
}
// Move the walker sideways while in the Walking state.
this.Demo.updateRockPosition(): voidupdateRockPosition();
}
// Runs once per screen refresh to draw the walker, particles, and UI. Notice: no
// Color32 objects appear in draw calls - only palette slot numbers.
Demo.render(): voidrender() {
// 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;
... 124 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 walker stands on, from GROUND_Y down to the bottom of
// the screen, with a 2-pixel lighter line on top so the edge reads clearly.
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;
... 124 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, const GROUND_Y: 172GROUND_Y, 320, 240 - const GROUND_Y: 172GROUND_Y), const C_GROUND: 1C_GROUND);
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;
... 124 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, const GROUND_Y: 172GROUND_Y, 320, 2), const C_GROUND_EDGE: 3C_GROUND_EDGE);
// Draw the walker with a shadow below it.
this.Demo.renderCharacter(): voidrenderCharacter();
// The State panel (the overlay row also shows state + ticks).
this.Demo.renderStateIndicator(): voidrenderStateIndicator();
// Draw any active particles.
this.Demo.renderParticles(): voidrenderParticles();
// Draw the timer readouts and the concept summary (shared UI kit groups).
this.Demo.renderUI(): voidrenderUI();
}
// Status row in the engine overlay: animation state (left) and tick count (right).
Demo.overlayRows(): {}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;
... 124 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, 2 seconds each. The full
// cycle is 6 seconds (360 ticks at 60 FPS).
Demo.autoCycleStates(tick: any): voidautoCycleStates(tick: anytick) {
// cyclePos goes 0-359, repeating: `%` gives the remainder after dividing by 360.
const const cyclePos: numbercyclePos = tick: anytick % 360;
if (const cyclePos: numbercyclePos < 120) {
// First 2 seconds: Idle. The walker stands 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. The walker 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. The walker 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: anytick;
// 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 walker based on the current state. Only Walking moves it sideways; the
// jump arc is a vertical offset computed in renderCharacter().
Demo.updateRockPosition(): voidupdateRockPosition() {
if (this.Demo.animState: anyanimState === const AnimState: anyAnimState.Walking) {
// Move 1 pixel per tick; turn around near the 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 >= const WALK_MAX_X: 220WALK_MAX_X) {
this.Demo.walkDir: numberwalkDir = -1;
}
if (this.Demo.charPos: Vector2icharPos.Vector2i.x: numberHorizontal component (defaults to 0).x <= const WALK_MIN_X: 64WALK_MIN_X) {
this.Demo.walkDir: numberwalkDir = 1;
}
}
}
// Draws the walker at the correct position, with a shadow below. While Walking, the
// frame cycles through the three walk poses based on the distance from walkStartX.
// While Jumping, the sprite moves up in an arc while the shadow stays on the ground.
Demo.renderCharacter(): voidrenderCharacter() {
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 3 pixels, advance to the next of
// the three walk poses (frames 1, 2, 3).
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) {
// jumpProgress runs from 0 at launch to 1 at landing (and on to 2 for the
// second hop). Math.sin(progress * PI) traces a hill from 0 up to 1 and back
// to 0; Math.abs keeps the second hop pointing up too. Negative y is up on
// screen.
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;
... 124 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) * 40);
}
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));
// Face the way we are walking: the frames face right, so mirror them with
// BT.FLIP_H while walkDir is -1. The walker keeps its last facing while it idles
// and jumps.
this.Demo.spriteParams: {
scale: number;
flags: number;
}
spriteParams.flags: numberflags = this.Demo.walkDir: numberwalkDir < 0 ? 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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.type FLIP_H: numberHorizontal flip flag for sprite rendering. Pass in
{@link
SpriteDrawParams.flags
}
; combine with the other `FLIP_*` / `ROT_*` bits using `|`.FLIP_H : 0;
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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => void (+1 overload)drawSprite(this.Demo.spriteSheet: SpriteSheet | nullspriteSheet, let srcRect: anysrcRect, const drawPos: Vector2idrawPos, this.Demo.spriteParams: {
scale: number;
flags: number;
}
spriteParams);
// The shadow is a 1-pixel line on the ground's top edge, right under the feet, no
// matter how high the jump is. It gets 2 pixels narrower on each side for every
// 10 pixels of height, like a real shadow shrinking as the thing casting it
// moves away from the ground.
const const shrink: numbershrink = 2 * Math.floor(-let yOffset: numberyOffset / 10);
const const shadowW: numbershadowW = 24 - 2 * const shrink: numbershrink;
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;
... 124 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 + 6 + const shrink: numbershrink, const GROUND_Y: 172GROUND_Y, const shadowW: numbershadowW, 1), const C_SHADOW: 2C_SHADOW);
}
// Draws the state machine as a kit panel in the top-right corner: all three states in
// the order they play, with a lit pip (a small filled square) next to the current
// one. Watching the lit pip step down the list makes the Idle -> Walking -> Jumping
// loop obvious at a glance.
Demo.renderStateIndicator(): voidrenderStateIndicator() {
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_RIGHT);
import uiui.panel('State');
for (const const state: anystate of const STATE_ORDER: {}STATE_ORDER) {
import uiui.pip(const state: anystate, const state: anystate === this.Demo.animState: anyanimState);
}
import uiui.end();
}
// Spawns a burst of BURST_SIZE particles at the walker's feet, all flying upward at
// slightly different speeds and angles, like a little fountain. Each particle gets
// its own palette slot from the rotating pool.
Demo.spawnBurst(): voidspawnBurst() {
// The whole burst shares a base hue picked from the spawn tick, so each burst has
// its own color. The % keeps the hue within 0-359 degrees of the color wheel.
const const baseHue: numberbaseHue = (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;
... 124 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 * 3) % 360;
// The fountain starts at the middle of the walker's feet (it is 36 pixels wide,
// so the middle is 18 pixels in from its left edge), just above the ground line.
const const originX: numberoriginX = this.Demo.charPos: Vector2icharPos.Vector2i.x: numberHorizontal component (defaults to 0).x + 18;
const const originY: numberoriginY = const GROUND_Y: 172GROUND_Y - 2;
for (let let i: numberi = 0; let i: numberi < const BURST_SIZE: 32BURST_SIZE; let i: numberi++) {
// Rotate through the 128 slots in a circle: `%` wraps the count back to 0
// after 127, so the oldest particle's slot gets reused.
const const slot: numberslot = const PARTICLE_SLOT_START: 50PARTICLE_SLOT_START + (this.Demo.nextParticleSlot: numbernextParticleSlot % const MAX_PARTICLES: 128MAX_PARTICLES);
this.Demo.nextParticleSlot: numbernextParticleSlot++;
// BT.random is the engine's shared random number generator. float(a, b)
// returns a fractional number from a up to (but not including) b. vx spreads
// the sparks left and right; vy is always negative, which on screen means
// upward, so every spark starts by rising. int(0, 40) nudges each spark's hue
// a little away from the burst's base hue, so the burst shimmers.
this.Demo.particles: {}particles.push({
x: numberx: const originX: numberoriginX + 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;
... 124 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, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.float(min: number, max: number): numberReturns the next pseudo-random float in [min, max).float(-4, 4),
y: numbery: const originY: numberoriginY,
vx: numbervx: 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;
... 124 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, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.float(min: number, max: number): numberReturns the next pseudo-random float in [min, max).float(-1.2, 1.2),
vy: numbervy: 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;
... 124 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, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.float(min: number, max: number): numberReturns the next pseudo-random float in [min, max).float(-3, -1.2),
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;
... 124 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,
hue: numberhue: (const baseHue: numberbaseHue + 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;
... 124 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, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
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(0, 40)) % 360,
paletteSlot: numberpaletteSlot: const slot: numberslot, // this particle "owns" this palette slot
});
}
}
// Draws all active particles as tiny colored squares. The color for each particle
// was already written in update() via palette.set(); here we just use the slot
// number.
Demo.renderParticles(): voidrenderParticles() {
for (const const p: anyp of this.Demo.particles: {}particles) {
// Draw a 2x2 square. Math.floor turns the fractional position into a whole
// pixel, and 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;
... 124 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(Math.floor(const p: anyp.x), Math.floor(const p: anyp.y), 2, 2), 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(): voidrenderUI() {
// 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;
... 124 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 kit panel in the top-left corner, level with the State panel
// on the right. kvCols: 10 makes the key column of the ui.kv() rows wide enough
// for "Particles" plus a space.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { kvCols: numberkvCols: 10 });
import uiui.panel('Timers');
// 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 shows the same fraction as a bar.
import uiui.meter(null, const cooldownPercent: anycooldownPercent, { color: stringcolor: 'warm', width: numberwidth: 100 });
// Key-value rows: the key on the left in a dim color, the value lined up in a
// column on the right. The next burst is under a second away, so it is counted
// in ticks rather than seconds. padStart(2) adds a space in front of one-digit
// numbers, so the text keeps the same width and the panel does not twitch.
import uiui.kv('Spawn in', `${String(const ticksUntilSpawn: numberticksUntilSpawn).padStart(2)} ticks`);
import uiui.kv('Particles', this.Demo.particles: {}particles.length);
import uiui.end();
// Concept summary: a borderless kit group on the ground strip, a little below its
// top edge so the walker's feet never touch the text.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { y: numbery: const GROUND_Y: 172GROUND_Y + 10 });
import uiui.label('Tick-based timing', { color: stringcolor: 'header' });
import uiui.label('- Fixed-rate ticks (BT.ticks)', { color: stringcolor: 'dim' });
import uiui.label('- Cooldowns and Timer events', { 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 DemoDemo);