/*
Palette Animation: change palette entries every tick for instant visual effects.
@description Change palette entries every tick to animate a whole scene at once, without redrawing any pixels.
Part of the BLIT386 demo series.
Prerequisites:
- Basics: https://demos.blit386.dev/basics
- Primitives: https://demos.blit386.dev/primitives
- Colors: https://demos.blit386.dev/colors
Live version: https://demos.blit386.dev/palette-animation
Guides:
- Palette: https://blit386.dev/docs/guides/palette#draw-using-absolute-palette-indices
- Palette patterns: https://blit386.dev/docs/guides/palette#practical-palette-first-patterns
- Overlay: https://blit386.dev/docs/guides/overlay#optional-bands
Reference:
- Active palette: https://blit386.dev/docs/api/palette#active-palette
- Overlay: https://blit386.dev/docs/api/overlay#palette-grid-sizing
---
WHAT YOU WILL SEE
-----------------
A 520 by 390 pixel screen with four kit panels: the gradient across the full width on
top, and below it the tall fire panel on the left beside the health bar and water strip
stacked on the right. Each one is animated by changing palette slot colors in update()
while render() draws the same rectangles every frame:
1. Scrolling Gradient - 32 swatches, one per slot (10-41), holding a rainbow whose base
hue turns 60 degrees per second, so the colors slide along the bar.
2. Fire - an 80 by 100 pixel column of 20 bands (slots 50-69), black at the bottom
through red and orange to pale yellow at the top, with a gentle sine-wave flicker.
Notes beside it name the top and bottom slots.
3. Flashing Health Bar - a 240 pixel bar filled with one slot (80). Simulated health
drains from 100 toward 0 over 6 seconds and then starts over; at 30 or below the slot
alternates between red and white every 8 ticks.
4. Cycling Water Strip - 18 tiles using three slots (90-92). Every 8 ticks the bright
highlight moves to the next slot, so a ripple seems to travel along the strip.
WHAT YOU WILL LEARN
-------------------
- The palette-animation rule: render() writes palette indices only, update() computes
Color32 values and stores them with palette.set(slot, color).
- The engine notices a changed palette on its own - no second BT.paletteSet() is needed.
- Color32.fromHSL(hue, saturation, lightness) for walking around the color wheel.
- Math.sin() for a flicker, `%` (remainder) for wrapping counters, BT.deltaSeconds and
BT.ticks for timing.
- Keeping animated slots clear of the slots other code owns (the UI kit theme in 240-251,
the overlay colors in 2-5).
WHAT IS PALETTE ANIMATION?
--------------------------
Old game hardware (Super Nintendo, Sega Genesis, Commodore 64) had strict rules: each
pixel only stored a small number - a "palette index" pointing to one color slot. To
animate colors, programmers changed what color was IN the slot, not what was on screen.
Imagine 16 numbered buckets of paint. A painting only records the bucket number for every
spot, not the actual color. To change the sky from blue to red, you repaint bucket 5, and
every sky spot changes at once - without touching the painting.
That trick is called "palette animation". Modern engines do not need it, but it is a
beautiful technique to understand, and BLIT386 lets you do it the same way.
UPDATE VS RENDER
----------------
- init() builds one palette: fixed colors for the engine overlay and the shared UI kit
theme, placeholders for every animated slot, then BT.paletteSet(). It calls update()
once so the first frame already has real colors.
- update() computes new Color32 values and writes them into slots 10-41, 50-69, 80, and
90-92 every tick.
- render() draws the four panels with palette indices only. The picture moves because
the colors behind the indices changed, not because render() passes new colors.
The panels are drawn with the shared UI kit (src/shared/ui.js); its theme colors live in
slots 240-251, far away from every slot this demo animates.
TRY THIS
--------
- Press the backquote key (`) to open the engine overlay. Its palette grid is turned on
with 64 columns: watch slots 10-41 rotate, 50-69 flicker, 80 flash, and 90-92 ripple
while slots 240-251 stay put.
- The overlay's gap color is slot 80, the health-bar slot, so the thin dividers in the
overlay flash along with the bar when health runs low.
*/
import { function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>One-liner bootstrap function for BLIT386 demos.
Handles canvas retrieval and engine initialization. Backend selection
(WebGPU or software fallback) is managed internally by BTAPI.
This function provides a streamlined way to start a demo with sensible defaults
while allowing customization through options.bootstrap, const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 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, class Vector2iInteger 2D vector for pixel-perfect positioning.
Used for points, sizes, directions, and camera offsets throughout the engine.
The API includes both allocation-free `*To()` / `*InPlace()` variants and
convenience methods that return new vectors.Vector2i } from 'blit386';
import { import applyThemeapplyTheme, import uiui, import UI_ANCHORSUI_ANCHORS } from './shared/ui.js';
/** @typedef {import('blit386').Palette} Palette */
// Gradient bar: how many color slots the scrolling gradient uses. More slots make a
// smoother rainbow; fewer make it chunkier.
const const GRAD_SLOTS: 32GRAD_SLOTS = 32;
// Size of each gradient swatch in pixels: 32 swatches x 15 pixels = 480, which fits the
// 484-pixel content width of the full-width gradient panel.
const const GRAD_SWATCH_W: 15GRAD_SWATCH_W = 15;
const const GRAD_SWATCH_H: 22GRAD_SWATCH_H = 22;
// Fire column: how many color slots make up the fire gradient. The bottom of the fire is
// black; the top is pale yellow.
const const FIRE_SLOTS: 20FIRE_SLOTS = 20;
// Height of each fire band in pixels: 20 bands x 5 pixels make a 100-pixel column.
const const FIRE_BAND_H: 5FIRE_BAND_H = 5;
// Width of the fire column in pixels.
const const FIRE_COL_W: 80FIRE_COL_W = 80;
// Water strip: three slots take turns being the bright highlight, which reads as a
// ripple.
const const WATER_SLOTS: 3WATER_SLOTS = 3;
// Health bar: the slot flips between red and white every FLASH_PERIOD ticks. A tick is
// one fixed update; 8 ticks is about 0.13 seconds at 60 ticks per second.
const const FLASH_PERIOD: 8FLASH_PERIOD = 8;
// How low the simulated health must fall before the bar starts flashing. Health counts
// down from HEALTH_MAX toward 0, then starts over.
const const HEALTH_LOW: 30HEALTH_LOW = 30;
const const HEALTH_MAX: 100HEALTH_MAX = 100;
// How many ticks one full health drain takes.
const const HEALTH_DRAIN_TICKS: 360HEALTH_DRAIN_TICKS = 360; // 6 seconds at 60 ticks per second
// Layout of the four kit panels on the 520x390 screen, with an 8-pixel margin all round:
// the gradient across the full width on top, and below it the tall fire panel on the
// left, beside the health and water panels stacked on the right. The right stack is
// exactly as tall as the fire panel, so the bottom edges line up.
const const PANEL_X: 8PANEL_X = 8;
const const PANEL_W: 504PANEL_W = 504;
const const PANEL_GAP: 8PANEL_GAP = 8;
// A kit panel with a title starts its content 22 pixels below its top edge and ends 11
// pixels below the last row (the rim and the title strip). Text inside starts 10 pixels
// in from the panel edge, and a label row is 10 pixels tall.
const const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP = 22;
const const PANEL_BOTTOM: 11PANEL_BOTTOM = 11;
const const PANEL_TEXT_INSET: 10PANEL_TEXT_INSET = 10;
const const LABEL_ROW_H: 10LABEL_ROW_H = 10;
// Empty bands that ui.spacer() reserves for the artwork drawn inside each panel. Artwork
// under a dim label row starts ART_GAP pixels below it, so letters that hang below the
// line (g, p, y) never touch it; the fire column gets the same gap under the title.
const const ART_GAP: 5ART_GAP = 5;
// Height of one water tile, needed here to size the water panel's spacer.
const const WATER_TILE_H: 17WATER_TILE_H = 17;
const const GRAD_SPACER: numberGRAD_SPACER = const ART_GAP: 5ART_GAP + const GRAD_SWATCH_H: 22GRAD_SWATCH_H + 3;
const const FIRE_SPACER: numberFIRE_SPACER = 3 + const FIRE_SLOTS: 20FIRE_SLOTS * const FIRE_BAND_H: 5FIRE_BAND_H + 3;
const const HEALTH_SPACER: 21HEALTH_SPACER = 21;
const const WATER_SPACER: numberWATER_SPACER = const ART_GAP: 5ART_GAP + const WATER_TILE_H: 17WATER_TILE_H + 2;
// Panel heights, from the rows each one stacks: title, an optional dim label row, the
// artwork band, and the bottom rim.
const const GRAD_PANEL_H: numberGRAD_PANEL_H = const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP + const LABEL_ROW_H: 10LABEL_ROW_H + const GRAD_SPACER: numberGRAD_SPACER + const PANEL_BOTTOM: 11PANEL_BOTTOM;
const const FIRE_PANEL_H: numberFIRE_PANEL_H = const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP + const FIRE_SPACER: numberFIRE_SPACER + const PANEL_BOTTOM: 11PANEL_BOTTOM;
const const HEALTH_PANEL_H: numberHEALTH_PANEL_H = const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP + const HEALTH_SPACER: 21HEALTH_SPACER + const LABEL_ROW_H: 10LABEL_ROW_H + const PANEL_BOTTOM: 11PANEL_BOTTOM;
// Vertical positions: the whole block (gradient + gap + fire panel) is centered on the
// 390-pixel screen.
const const BLOCK_H: numberBLOCK_H = const GRAD_PANEL_H: numberGRAD_PANEL_H + const PANEL_GAP: 8PANEL_GAP + const FIRE_PANEL_H: numberFIRE_PANEL_H;
const const GRAD_PANEL_Y: anyGRAD_PANEL_Y = Math.floor((390 - const BLOCK_H: numberBLOCK_H) / 2);
const const LOWER_PANELS_Y: anyLOWER_PANELS_Y = const GRAD_PANEL_Y: anyGRAD_PANEL_Y + const GRAD_PANEL_H: numberGRAD_PANEL_H + const PANEL_GAP: 8PANEL_GAP;
const const WATER_PANEL_Y: anyWATER_PANEL_Y = const LOWER_PANELS_Y: anyLOWER_PANELS_Y + const HEALTH_PANEL_H: numberHEALTH_PANEL_H + const PANEL_GAP: 8PANEL_GAP;
// The fire panel is 168 pixels wide; the health and water panels fill the rest of the
// row.
const const FIRE_PANEL_W: 168FIRE_PANEL_W = 168;
const const RIGHT_PANEL_X: numberRIGHT_PANEL_X = const PANEL_X: 8PANEL_X + const FIRE_PANEL_W: 168FIRE_PANEL_W + const PANEL_GAP: 8PANEL_GAP;
const const RIGHT_PANEL_W: numberRIGHT_PANEL_W = const PANEL_X: 8PANEL_X + const PANEL_W: 504PANEL_W - const RIGHT_PANEL_X: numberRIGHT_PANEL_X;
// Health bar size, and the water strip: 18 tiles (six repeats of the 3-slot cycle), each
// 16 pixels wide with a 1-pixel gap and WATER_TILE_H tall.
const const HEALTH_BAR_W: 240HEALTH_BAR_W = 240;
const const HEALTH_BAR_H: 12HEALTH_BAR_H = 12;
const const WATER_TILES: 18WATER_TILES = 18;
const const WATER_TILE_STEP: 17WATER_TILE_STEP = 17;
// Palette slot constants. We group the palette like compartments in a paint box: each
// animated panel owns a range of slots that it refills in update() every tick. The shared
// UI kit adds its own 12 theme colors in slots 240-251 (see init()), safely above
// everything listed here. Slot 0 is always transparent - reserved by the engine.
// Engine overlay style slots. configure() runs BEFORE init() installs the shared UI
// theme, so the overlay style cannot use theme slots - instead it points at these four
// low slots, which init() fills by hand with fixed colors.
const const C_OVERLAY_BAR: 2C_OVERLAY_BAR = 2; // overlay bar background (very dark navy)
const const C_OVERLAY_ERR: 3C_OVERLAY_ERR = 3; // timing chart error bars (dark blue)
const const C_OVERLAY_TEXT: 4C_OVERLAY_TEXT = 4; // overlay text and chart update bars (golden yellow)
const const C_OVERLAY_DIM: 5C_OVERLAY_DIM = 5; // chart render and warning bars (cool gray-blue)
// Gradient section: 32 slots, one per swatch column. update() rewrites all 32 every tick
// to scroll the hue.
const const C_GRAD_BASE: 10C_GRAD_BASE = 10; // slots 10-41
// Fire section: 20 slots, one per horizontal band. The gradient ends at slot 41, so the
// fire starts at a round 50 to leave a gap.
const const C_FIRE_BASE: 50C_FIRE_BASE = 50; // slots 50-69
// Health bar: one slot that flips between red and white.
const const C_HEALTH_BAR: 80C_HEALTH_BAR = 80; // slot 80
// Water strip: three slots that take turns being bright.
const const C_WATER_BASE: 90C_WATER_BASE = 90; // slots 90-92
// Demonstrates palette animation: changing palette entries every tick to make a scrolling
// gradient, a fire column, a flashing health bar, and rippling water, all without
// touching the rectangles drawn in render().
class class DemoDemo {
// The single palette used for all drawing.
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = null;
// Palette slot map for the shared UI kit theme, filled in init() by applyTheme().
// theme.bg, theme.border, theme.header, and friends are palette indices ready for BT
// draw calls.
Demo.theme: nulltheme = null;
// Seconds since the demo started, advanced by BT.deltaSeconds every tick. Drives the
// continuous animation in update().
Demo.animTime: numberanimTime = 0;
// Simulated health value (0-100), drained over time.
Demo.health: numberhealth = const HEALTH_MAX: 100HEALTH_MAX;
// Which water slot is currently the bright one (0, 1, or 2).
Demo.waterPhase: numberwaterPhase = 0;
// Tick when the water ripple last advanced one step.
Demo.waterTick: numberwaterTick = 0;
// Reused for every rectangle drawn in render(), so drawing allocates no new Rect2i.
Demo.tempRect: Rect2itempRect = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(0, 0, 0, 0);
// A wider canvas, the overlay's palette grid (64 columns), and overlay colors from
// the dedicated low slots - plus the animated health slot as a playful gap color.
Demo.configure(): {
displaySize: Vector2i;
maxCanvasSize: Vector2i;
isOverlayPaletteEnabled: boolean;
overlayPaletteColumns: number;
overlayStyle: {
barPaletteIndex: number;
textPaletteIndex: number;
gapPaletteIndex: number;
};
isOverlayTimingChartEnabled: boolean;
overlayTimingChartStyle: {
updateBarPaletteIndex: number;
renderBarPaletteIndex: number;
warningPaletteIndex: number;
errorPaletteIndex: number;
tagPaletteIndex: number;
};
}
configure() {
return {
displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(520, 390),
maxCanvasSize: Vector2imaxCanvasSize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(520 * 2, 390 * 2),
isOverlayPaletteEnabled: booleanisOverlayPaletteEnabled: true,
overlayPaletteColumns: numberoverlayPaletteColumns: 64,
overlayStyle: {
barPaletteIndex: number;
textPaletteIndex: number;
gapPaletteIndex: number;
}
overlayStyle: {
barPaletteIndex: numberbarPaletteIndex: const C_OVERLAY_BAR: 2C_OVERLAY_BAR,
textPaletteIndex: numbertextPaletteIndex: const C_OVERLAY_TEXT: 4C_OVERLAY_TEXT,
// The gap reuses the animated health slot, so it flashes with the demo.
gapPaletteIndex: numbergapPaletteIndex: const C_HEALTH_BAR: 80C_HEALTH_BAR,
},
isOverlayTimingChartEnabled: booleanisOverlayTimingChartEnabled: true,
overlayTimingChartStyle: {
updateBarPaletteIndex: number;
renderBarPaletteIndex: number;
warningPaletteIndex: number;
errorPaletteIndex: number;
tagPaletteIndex: number;
}
overlayTimingChartStyle: {
updateBarPaletteIndex: numberupdateBarPaletteIndex: const C_OVERLAY_TEXT: 4C_OVERLAY_TEXT,
renderBarPaletteIndex: numberrenderBarPaletteIndex: const C_OVERLAY_DIM: 5C_OVERLAY_DIM,
warningPaletteIndex: numberwarningPaletteIndex: const C_OVERLAY_DIM: 5C_OVERLAY_DIM,
errorPaletteIndex: numbererrorPaletteIndex: const C_OVERLAY_ERR: 3C_OVERLAY_ERR,
tagPaletteIndex: numbertagPaletteIndex: const C_OVERLAY_TEXT: 4C_OVERLAY_TEXT,
},
};
}
// Builds the palette (overlay slots, placeholders for the animated slots, the shared
// UI theme), activates it, then runs update() once so every animated slot has real
// colors before the first render().
async Demo.init(): unknowninit() {
console.log('[PaletteAnimationDemo] Initializing...');
// Build the main palette. Think of this as setting up the paint box before you
// start painting. Static entries (overlay colors, UI theme) go in now and never
// change. Dynamic entries (gradient, fire, health, water) start as placeholders
// and get overwritten in update() every tick.
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);
// Colors for the engine overlay (the stats HUD). They match the shared UI theme's
// look but live in low slots so configure() could name them before the theme
// existed.
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_BAR: 2C_OVERLAY_BAR, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(10, 12, 20)); // very dark navy
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_ERR: 3C_OVERLAY_ERR, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(20, 24, 36)); // dark blue
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_TEXT: 4C_OVERLAY_TEXT, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(255, 210, 80)); // golden yellow
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_DIM: 5C_OVERLAY_DIM, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(120, 130, 160)); // cool gray-blue
// Placeholders for the animated slots: black for the gradient and fire, red for
// the health bar, dark blue for the water. The first update() call below replaces
// them all.
for (let let i: numberi = 0; let i: numberi < const GRAD_SLOTS: 32GRAD_SLOTS; let i: numberi++) {
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_GRAD_BASE: 10C_GRAD_BASE + 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));
}
for (let let i: numberi = 0; let i: numberi < const FIRE_SLOTS: 20FIRE_SLOTS; let i: numberi++) {
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_FIRE_BASE: 50C_FIRE_BASE + 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));
}
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_HEALTH_BAR: 80C_HEALTH_BAR, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(220, 40, 40));
for (let let i: numberi = 0; let i: numberi < const WATER_SLOTS: 3WATER_SLOTS; let i: numberi++) {
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_WATER_BASE: 90C_WATER_BASE + let i: numberi, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(0, 80, 160));
}
// Install the shared UI kit colors (panel fills, borders, headings, dim text).
// applyTheme() writes 12 colors into slots 240-251 - far above every range this
// demo animates (gradient 10-41, fire 50-69, health 80, water 90-92), so the
// animation can never overwrite the UI theme. The returned map remembers where
// each color landed (this.theme.bg, this.theme.header, ...).
this.Demo.theme: nulltheme = import applyThemeapplyTheme(this.Demo.palette: Palettepalette);
// Tell the engine to draw with this palette from now 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;
... 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);
// Run one update so every animated slot has real colors before the first render.
// update() takes no arguments; this priming call also advances animTime by one
// tick, the same as every regular call from the engine's game loop.
this.Demo.update(): voidupdate();
console.log('[PaletteAnimationDemo] Initialized');
return true;
}
// Runs once per tick, 60 times per second. Computes new Color32 values for every
// animated slot. render() never sees a Color32 - it only uses the slot numbers.
Demo.update(): voidupdate() {
// Advance the clock. BT.deltaSeconds is the length of one tick in seconds (1/60).
this.Demo.animTime: numberanimTime += const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.deltaSeconds: numberFixed-step seconds per update tick.
Equivalent to `1 / BT.targetFPS` when `BT.targetFPS` is finite and positive.
Falls back to `1 / 60` when target FPS is non-finite or non-positive.deltaSeconds;
// Simulated health: drains from HEALTH_MAX toward 0 over HEALTH_DRAIN_TICKS
// ticks, then jumps back to full so the demo loops forever. `%` is the remainder
// after division, so tick % HEALTH_DRAIN_TICKS counts 0-359 and wraps.
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;
this.Demo.health: numberhealth = const HEALTH_MAX: 100HEALTH_MAX - Math.floor((const tick: numbertick % const HEALTH_DRAIN_TICKS: 360HEALTH_DRAIN_TICKS) * (const HEALTH_MAX: 100HEALTH_MAX / const HEALTH_DRAIN_TICKS: 360HEALTH_DRAIN_TICKS));
// Panel 1: the scrolling gradient. The base hue turns a little every tick so the
// rainbow appears to slide along the bar.
this.Demo.updateGradient(): voidupdateGradient();
// Panel 2: the fire column. Each slot is one band of the flame; lower slots are
// closer to the bottom and darker.
this.Demo.updateFire(): voidupdateFire();
// Panel 3: the flashing health bar. One slot flips between red and white based on
// the tick count.
this.Demo.updateHealthBar(tick: any): voidupdateHealthBar(const tick: numbertick);
// Panel 4: the water strip. Three slots take turns being the bright highlight.
this.Demo.updateWater(tick: any): voidupdateWater(const tick: numbertick);
}
// Draws all four panels. Only palette indices appear here - no Color32 objects.
Demo.render(): voidrender() {
// Clear the screen with the UI theme's dark background.
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);
// Draw each of the four panels.
this.Demo.renderGradientPanel(): voidrenderGradientPanel();
this.Demo.renderFirePanel(): voidrenderFirePanel();
this.Demo.renderHealthPanel(): voidrenderHealthPanel();
this.Demo.renderWaterPanel(): voidrenderWaterPanel();
}
// Scrolling gradient: rewrites the 32 hue slots so the rainbow appears to slide. The
// base hue advances every tick, and each slot gets a hue a little ahead of the one
// before it.
Demo.updateGradient(): voidupdateGradient() {
// The base hue grows over time: 60 degrees per second, so one full turn of the
// color wheel takes 6 seconds. Math.floor() rounds down to whole degrees, and
// % 360 keeps the hue in the 0-359 range.
const const baseHue: numberbaseHue = Math.floor(this.Demo.animTime: numberanimTime * 60) % 360;
for (let let i: numberi = 0; let i: numberi < const GRAD_SLOTS: 32GRAD_SLOTS; let i: numberi++) {
// Spread the slots evenly around the color wheel: (i / GRAD_SLOTS) * 360
// spaces 32 hues over 360 degrees.
const const hue: numberhue = (const baseHue: numberbaseHue + (let i: numberi / const GRAD_SLOTS: 32GRAD_SLOTS) * 360) % 360;
// Color32.fromHSL(hue, saturation, lightness): hue 0-360 is the position on
// the color wheel (0 red, 120 green, 240 blue), saturation 0-100 is how vivid
// the color is (0 gray, 100 pure), lightness 0-100 is how bright (0 black,
// 50 pure color, 100 white).
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_GRAD_BASE: 10C_GRAD_BASE + let i: numberi, 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, 90, 55));
}
}
// Fire column: each slot is one horizontal band of flame. The bottom is black and
// dark red; moving up it turns orange and then pale yellow. A sine wave nudges each
// band's position a little over time so the flame flickers.
Demo.updateFire(): voidupdateFire() {
for (let let i: numberi = 0; let i: numberi < const FIRE_SLOTS: 20FIRE_SLOTS; let i: numberi++) {
// t is 0 at the bottom slot and 1 at the top slot.
const const t: numbert = let i: numberi / (const FIRE_SLOTS: 20FIRE_SLOTS - 1);
// Math.sin() returns a value between -1 and 1 that swings back and forth over
// time. Adding 1 and scaling by 0.075 turns that into a 0-0.15 wobble.
const const flicker: numberflicker = (Math.sin(this.Demo.animTime: numberanimTime * 7 + let i: numberi * 0.8) + 1) * 0.075;
// Add the wobble to t and clamp the result between 0 and 1.
const const ft: anyft = Math.min(1, Math.max(0, const t: numbert + const flicker: numberflicker));
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_FIRE_BASE: 50C_FIRE_BASE + let i: numberi, this.Demo.fireColor(ft: any): Color32fireColor(const ft: anyft));
}
}
// Maps a position along the flame (`ft`, 0 at the bottom, 1 at the top) to a fire
// color by blending through four stops: black at 0, dark red at 0.3, bright orange at
// 0.6, pale yellow at 1.
Demo.fireColor(ft: any): Color32fireColor(ft: anyft) {
if (ft: anyft < 0.3) {
// Black to dark red.
const const s: numbers = ft: anyft / 0.3; // rescales 0-0.3 to 0-1
return new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(Math.floor(const s: numbers * 160), 0, 0);
}
if (ft: anyft < 0.6) {
// Dark red to bright orange.
const const s: numbers = (ft: anyft - 0.3) / 0.3;
return new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(160 + Math.floor(const s: numbers * 95), Math.floor(const s: numbers * 100), 0);
}
// Orange to pale yellow.
const const s: numbers = (ft: anyft - 0.6) / 0.4;
return new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(255, 100 + Math.floor(const s: numbers * 155), Math.floor(const s: numbers * 120));
}
// Health bar: flips one slot between red and white every FLASH_PERIOD ticks, but only
// while health is critically low. Otherwise the slot holds a steady red.
Demo.updateHealthBar(tick: any): voidupdateHealthBar(tick: anytick) {
if (this.Demo.health: numberhealth <= const HEALTH_LOW: 30HEALTH_LOW) {
// Math.floor(tick / FLASH_PERIOD) counts up by one every FLASH_PERIOD ticks,
// and % 2 turns that count into 0, 1, 0, 1, ... - so the color flips every 8
// ticks.
const const flashOn: booleanflashOn = Math.floor(tick: anytick / const FLASH_PERIOD: 8FLASH_PERIOD) % 2 === 0;
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_HEALTH_BAR: 80C_HEALTH_BAR, const flashOn: booleanflashOn ? new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(255, 50, 50) : new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(255, 255, 255));
} else {
// Healthy: steady red.
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_HEALTH_BAR: 80C_HEALTH_BAR, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(200, 40, 40));
}
}
// Water strip: three slots take turns being the brightest. Each slot cycles bright,
// medium, dark, bright, ... and the three are offset by one step, so the bright spot
// appears to travel along the strip.
Demo.updateWater(tick: any): voidupdateWater(tick: anytick) {
// Advance the ripple every 8 ticks - 7.5 steps per second.
if (tick: anytick - this.Demo.waterTick: numberwaterTick >= 8) {
this.Demo.waterPhase: numberwaterPhase = (this.Demo.waterPhase: numberwaterPhase + 1) % const WATER_SLOTS: 3WATER_SLOTS;
this.Demo.waterTick: numberwaterTick = tick: anytick;
}
for (let let i: numberi = 0; let i: numberi < const WATER_SLOTS: 3WATER_SLOTS; let i: numberi++) {
// How far slot i is from the current bright spot: 0, 1, or 2 steps. Adding
// WATER_SLOTS before the % keeps the result from going negative.
const const dist: numberdist = (let i: numberi - this.Demo.waterPhase: numberwaterPhase + const WATER_SLOTS: 3WATER_SLOTS) % const WATER_SLOTS: 3WATER_SLOTS;
// dist 0 is the brightest, 1 medium, 2 the darkest.
let let color: anycolor;
if (const dist: numberdist === 0) {
// Bright highlight - the wave crest.
let color: anycolor = new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(80, 200, 255);
} else if (const dist: numberdist === 1) {
// Medium shade - just before or after the crest.
let color: anycolor = new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(30, 100, 200);
} else {
// Dark trough - between ripples.
let color: anycolor = new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(10, 40, 120);
}
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_WATER_BASE: 90C_WATER_BASE + let i: numberi, let color: Color32color);
}
}
// Opens a titled kit panel at a fixed spot and width. The caller then adds its rows
// (labels, spacers) and closes the group with ui.end().
Demo.beginPanel(x: any, y: any, width: any, title: any): voidbeginPanel(x: anyx, y: anyy, width: anywidth, title: anytitle) {
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { x: anyx, y: anyy, width: anywidth });
import uiui.panel(title: anytitle);
}
// Fills a rectangle on top of a kit panel. Kit panels are sprites, and the engine
// draws every BT.drawRectFill() before any sprite - so a plain one would hide under
// the panel. ui.drawRectFill() takes the same rectangle and palette slot but draws a
// sprite, which lands on top in the order we call it.
Demo.fillOnPanel(x: any, y: any, width: any, height: any, slot: any): voidfillOnPanel(x: anyx, y: anyy, width: anywidth, height: anyheight, slot: anyslot) {
this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(x: anyx, y: anyy, width: anywidth, height: anyheight);
import uiui.drawRectFill(this.Demo.tempRect: Rect2itempRect, slot: anyslot);
}
// Panel 1: the scrolling gradient bar, across the full width. 32 rectangles sit
// inside it, each using a different palette slot. Because update() rotates the hues
// in those slots, the bar appears to scroll.
Demo.renderGradientPanel(): voidrenderGradientPanel() {
// ui.spacer() reserves an empty band under the dim label for the swatches, which
// are drawn after ui.end() so they land on top of the panel.
this.Demo.beginPanel(x: any, y: any, width: any, title: any): voidbeginPanel(const PANEL_X: 8PANEL_X, const GRAD_PANEL_Y: anyGRAD_PANEL_Y, const PANEL_W: 504PANEL_W, 'Scrolling Gradient - slots 10-41');
import uiui.label('update() turns every hue a little each tick', { color: stringcolor: 'dim' });
import uiui.spacer(const GRAD_SPACER: numberGRAD_SPACER);
import uiui.end();
// The swatches are centered in the panel, ART_GAP pixels below the label row.
const const stripW: numberstripW = const GRAD_SLOTS: 32GRAD_SLOTS * const GRAD_SWATCH_W: 15GRAD_SWATCH_W;
const const x0: numberx0 = const PANEL_X: 8PANEL_X + (const PANEL_W: 504PANEL_W - const stripW: numberstripW) / 2;
const const y: anyy = const GRAD_PANEL_Y: anyGRAD_PANEL_Y + const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP + const LABEL_ROW_H: 10LABEL_ROW_H + const ART_GAP: 5ART_GAP;
for (let let i: numberi = 0; let i: numberi < const GRAD_SLOTS: 32GRAD_SLOTS; let i: numberi++) {
this.Demo.fillOnPanel(x: any, y: any, width: any, height: any, slot: any): voidfillOnPanel(const x0: numberx0 + let i: numberi * const GRAD_SWATCH_W: 15GRAD_SWATCH_W, const y: anyy, const GRAD_SWATCH_W: 15GRAD_SWATCH_W, const GRAD_SWATCH_H: 22GRAD_SWATCH_H, const C_GRAD_BASE: 10C_GRAD_BASE + let i: numberi);
}
}
// Panel 2: the fire column, on the left. FIRE_SLOTS horizontal bands stacked
// vertically; their colors change in update() to flicker.
Demo.renderFirePanel(): voidrenderFirePanel() {
this.Demo.beginPanel(x: any, y: any, width: any, title: any): voidbeginPanel(const PANEL_X: 8PANEL_X, const LOWER_PANELS_Y: anyLOWER_PANELS_Y, const FIRE_PANEL_W: 168FIRE_PANEL_W, 'Fire - slots 50-69');
import uiui.spacer(const FIRE_SPACER: numberFIRE_SPACER);
import uiui.end();
// Fire bands from the bottom (band 0, darkest) to the top (band FIRE_SLOTS - 1,
// brightest). We draw them bottom-up so band 0 sits at the base of the column.
const const colX: numbercolX = const PANEL_X: 8PANEL_X + const PANEL_TEXT_INSET: 10PANEL_TEXT_INSET;
const const colTop: anycolTop = const LOWER_PANELS_Y: anyLOWER_PANELS_Y + const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP + 3;
const const colBottom: anycolBottom = const colTop: anycolTop + const FIRE_SLOTS: 20FIRE_SLOTS * const FIRE_BAND_H: 5FIRE_BAND_H;
for (let let i: numberi = 0; let i: numberi < const FIRE_SLOTS: 20FIRE_SLOTS; let i: numberi++) {
const const y: numbery = const colBottom: anycolBottom - (let i: numberi + 1) * const FIRE_BAND_H: 5FIRE_BAND_H;
this.Demo.fillOnPanel(x: any, y: any, width: any, height: any, slot: any): voidfillOnPanel(const colX: numbercolX, const y: numbery, const FIRE_COL_W: 80FIRE_COL_W, const FIRE_BAND_H: 5FIRE_BAND_H, const C_FIRE_BASE: 50C_FIRE_BASE + let i: numberi);
}
// Notes beside the column: the slot at the very top, the sin() flicker in the
// middle, and the slot at the very bottom. A caption's capital letters cover 7
// rows starting 1 row above the y it is given, so labelY = top + 1 lines their
// top up with the column top, and labelY = bottom - 6 lines their bottom up with
// it.
const const noteX: numbernoteX = const colX: numbercolX + const FIRE_COL_W: 80FIRE_COL_W + 8;
import uiui.caption(const noteX: numbernoteX, const colTop: anycolTop + 1, 'slot 69', { color: stringcolor: 'dim' });
import uiui.caption(const noteX: numbernoteX, (const colTop: anycolTop + const colBottom: anycolBottom) / 2 - 2, 'flicker', { color: stringcolor: 'dim' });
import uiui.caption(const noteX: numbernoteX, const colBottom: anycolBottom - 6, 'slot 50', { color: stringcolor: 'dim' });
}
// Panel 3: the flashing health bar, top right. One palette slot flips between red and
// white when health is critically low.
Demo.renderHealthPanel(): voidrenderHealthPanel() {
// Heading, a spacer for the bar artwork, then the live status line. The status
// text turns warm orange when health is critical, dim gray otherwise.
const const isCritical: booleanisCritical = this.Demo.health: numberhealth <= const HEALTH_LOW: 30HEALTH_LOW;
this.Demo.beginPanel(x: any, y: any, width: any, title: any): voidbeginPanel(const RIGHT_PANEL_X: numberRIGHT_PANEL_X, const LOWER_PANELS_Y: anyLOWER_PANELS_Y, const RIGHT_PANEL_W: numberRIGHT_PANEL_W, 'Flashing Health Bar - slot 80');
import uiui.spacer(const HEALTH_SPACER: 21HEALTH_SPACER);
import uiui.label(const isCritical: booleanisCritical ? 'CRITICAL! slot 80 flashes red/white' : 'Healthy: slot 80 = steady red', {
color: stringcolor: const isCritical: booleanisCritical ? 'warm' : 'dim',
});
import uiui.end();
// The bar sits in the middle of the band the spacer reserved.
const const barX: numberbarX = const RIGHT_PANEL_X: numberRIGHT_PANEL_X + const PANEL_TEXT_INSET: 10PANEL_TEXT_INSET;
const const barY: anybarY = const LOWER_PANELS_Y: anyLOWER_PANELS_Y + const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP + (const HEALTH_SPACER: 21HEALTH_SPACER - const HEALTH_BAR_H: 12HEALTH_BAR_H) / 2;
// Width of the filled part from the current health: health / HEALTH_MAX is a
// fraction from 0 to 1, multiplied by the full bar width. Math.max keeps at least
// 1 pixel visible.
const const barW: anybarW = Math.max(1, Math.floor((this.Demo.health: numberhealth / const HEALTH_MAX: 100HEALTH_MAX) * const HEALTH_BAR_W: 240HEALTH_BAR_W));
// Background trough (dark, always full width) with a theme-colored outline.
// ui.drawRect() is the sprite version of BT.drawRect(), for the same reason as
// fillOnPanel(): it has to show on top of the kit panel.
this.Demo.fillOnPanel(x: any, y: any, width: any, height: any, slot: any): voidfillOnPanel(const barX: numberbarX, const barY: anybarY, const HEALTH_BAR_W: 240HEALTH_BAR_W, const HEALTH_BAR_H: 12HEALTH_BAR_H, this.Demo.theme: nulltheme.bg);
this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(const barX: numberbarX, const barY: anybarY, const HEALTH_BAR_W: 240HEALTH_BAR_W, const HEALTH_BAR_H: 12HEALTH_BAR_H);
import uiui.drawRect(this.Demo.tempRect: Rect2itempRect, this.Demo.theme: nulltheme.border);
// The filled part uses C_HEALTH_BAR, which update() flashes when health is low.
this.Demo.fillOnPanel(x: any, y: any, width: any, height: any, slot: any): voidfillOnPanel(const barX: numberbarX, const barY: anybarY, const barW: anybarW, const HEALTH_BAR_H: 12HEALTH_BAR_H, const C_HEALTH_BAR: 80C_HEALTH_BAR);
// The health value as text right of the bar, in the theme's amber heading color.
// labelY = barY + 3 puts the 7-pixel-tall digits on rows barY + 2 to barY + 8,
// the middle of the 12-pixel bar.
import uiui.caption(const barX: numberbarX + const HEALTH_BAR_W: 240HEALTH_BAR_W + 8, const barY: anybarY + 3, `HP ${this.Demo.health: numberhealth}`, { color: stringcolor: 'header' });
}
// Panel 4: the water strip, bottom right. The tiles each use one of the three water
// slots, so the brightness cycles across them like a ripple.
Demo.renderWaterPanel(): voidrenderWaterPanel() {
this.Demo.beginPanel(x: any, y: any, width: any, title: any): voidbeginPanel(const RIGHT_PANEL_X: numberRIGHT_PANEL_X, const WATER_PANEL_Y: anyWATER_PANEL_Y, const RIGHT_PANEL_W: numberRIGHT_PANEL_W, 'Cycling Water Strip - slots 90-92');
import uiui.label('Three slots take turns being the bright crest', { color: stringcolor: 'dim' });
import uiui.spacer(const WATER_SPACER: numberWATER_SPACER);
import uiui.end();
const const x0: numberx0 = const RIGHT_PANEL_X: numberRIGHT_PANEL_X + const PANEL_TEXT_INSET: 10PANEL_TEXT_INSET;
const const y: anyy = const WATER_PANEL_Y: anyWATER_PANEL_Y + const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP + const LABEL_ROW_H: 10LABEL_ROW_H + const ART_GAP: 5ART_GAP;
for (let let i: numberi = 0; let i: numberi < const WATER_TILES: 18WATER_TILES; let i: numberi++) {
// i % WATER_SLOTS maps the tile index to one of the three water slots.
const const slot: numberslot = const C_WATER_BASE: 90C_WATER_BASE + (let i: numberi % const WATER_SLOTS: 3WATER_SLOTS);
this.Demo.fillOnPanel(x: any, y: any, width: any, height: any, slot: any): voidfillOnPanel(const x0: numberx0 + let i: numberi * const WATER_TILE_STEP: 17WATER_TILE_STEP, const y: anyy, const WATER_TILE_STEP: 17WATER_TILE_STEP - 1, const WATER_TILE_H: 17WATER_TILE_H, const slot: numberslot);
}
}
}
// 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);