/*

Palette Cycling: classic retro color rotation using BT.paletteCycle().
@description Classic retro color rotation with BT.paletteCycle: rotate palette slots to make a still image flow.

Part of the BLIT386 demo series.

Prerequisites:
  - Basics: https://demos.blit386.dev/basics
  - Primitives: https://demos.blit386.dev/primitives
  - Palette Animation: https://demos.blit386.dev/palette-animation

Live version: https://demos.blit386.dev/palette-cycling

Guides:
  - Palette: https://blit386.dev/docs/guides/palette#runtime-palette-effects
  - Palette patterns: https://blit386.dev/docs/guides/palette#practical-palette-first-patterns
  - Overlay: https://blit386.dev/docs/guides/overlay#colors-and-hud-palette-slots

Reference:
  - Palette: https://blit386.dev/docs/api/palette#palette-effects

---

WHAT YOU WILL SEE
-----------------

Three kit panels stacked down the screen, each titled with the exact BT.paletteCycle()
call that drives it and filled with rectangles that never move but whose colors flow:

  1. Sky (top) - ten full-width purple-to-pink stripes, one per slot (10-19), cycling
     very slowly at 0.5 steps per second.
  2. Fire (middle) - 24 narrow columns of 8 blocks from 6 slots (30-35), dark red to
     bright yellow. Each column starts a little further along the gradient than its
     neighbor, following a sine wave, so the bands bend into flame tongues. They cycle at
     6 steps per second with a negative speed, so the other way round.
  3. Water (bottom) - a grid of 36 by 8 small blocks from 8 slots (50-57), dark blue to
     cyan, laid out diagonally and cycling at 4 steps per second.

Every 3 seconds the demo also swaps two fire slots with BT.paletteSwap() and names them
for about a second in orange, on the right of the fire panel's title row.

WHAT YOU WILL LEARN
-------------------

- BT.paletteCycle(start, end, speed) rotates the colors in slots start to end (both
  inclusive) at `speed` steps per second, forever. A positive speed moves every color one
  slot down (slot 11's color lands in slot 10, and slot 10's wraps to the end); a negative
  speed moves them the other way.
- BT.paletteSwap(indexA, indexB) exchanges two slots instantly - not an effect, a one-off.
- Palette effects run inside the engine once per tick, so init() starts them and the demo
  never touches those colors again.
- Filling a run of slots with a gradient from a tiny helper and a callback.
- Keeping the cycled ranges clear of the slots the UI kit and the overlay own.

WHAT IS PALETTE CYCLING?
------------------------

On old hardware, colors were stored in numbered "slots" called a palette. Every pixel on
screen was just a number pointing to a slot. If you ROTATE the colors - move each color
one slot over and wrap the one that falls off the end back to the start - everything on
screen that uses those slots appears to ripple or flow. The picture data never changes;
only the paint-bucket labels shift around.

This trick powered water, lava, plasma, and aurora effects in classic games like Sonic,
Secret of Mana, and Chrono Trigger - all without redrawing a single pixel.

BLIT386 gives you BT.paletteCycle(start, end, speed) to do exactly that. Call it once in
init(), and the engine rotates the colors automatically every tick.

UPDATE VS RENDER
----------------

  - init() fills the gradient slots and starts three BT.paletteCycle() effects. The engine
    rotates those slot ranges on its own every tick until BT.paletteClearEffects() is
    called - which this demo never does.
  - update() only runs the periodic BT.paletteSwap() and hides its caption.
  - render() draws fixed rectangles using palette index numbers; the bands appear to flow
    because the engine shifted the slot colors, not because render() recomputes Color32
    values.

The band headings are drawn with the shared UI kit (src/shared/ui.js); its theme colors
live in slots 240-251, far away from every cycling range.

TRY THIS
--------

- Press the backquote key (`) to open the engine overlay. The timing chart shows that
  update() does almost nothing - the rotation happens inside the engine.
- Watch the fire panel's title row: every 3 seconds it names the two slots that just
  swapped. The two colors trade places in the rotating ring for good, so the fire
  gradient slowly gets scrambled.

*/

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.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
,
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
, class Color32
Mutable 32-bit RGBA color value with 8-bit channels.
@since0.1.0
Color32
, class Rect2i
Integer rectangle for pixel-perfect bounds and regions. Used throughout the engine for sprite regions, display-space bounds, and zero-allocation geometry helpers. Both convenience getters and allocation-free `*To()` helpers are provided so callers can choose between readability and hot-path efficiency.
@since0.1.0
Rect2i
} from 'blit386';
import { import applyThemeapplyTheme, import uiui, import UI_ANCHORSUI_ANCHORS } from './shared/ui.js'; /** @typedef {import('blit386').Palette} Palette */ // Water: 8 blue slots cycling at 4 steps per second. const const WATER_SLOTS: 8WATER_SLOTS = 8; const const WATER_SPEED: 4WATER_SPEED = 4; // Fire: 6 orange-yellow slots cycling the other way at 6 steps per second. const const FIRE_SLOTS: 6FIRE_SLOTS = 6; const const FIRE_SPEED: -6FIRE_SPEED = -6; // Sky: 10 purple-pink slots cycling very slowly at 0.5 steps per second. const const SKY_SLOTS: 10SKY_SLOTS = 10; const const SKY_SPEED: 0.5SKY_SPEED = 0.5; // Palette swap demo: every SWAP_INTERVAL ticks, swap two fire slots to show // BT.paletteSwap(). A tick is one fixed update; 180 ticks is 3 seconds at 60 ticks per // second. const const SWAP_INTERVAL: 180SWAP_INTERVAL = 180; // Layout of the three kit panels on the 320x240 screen, 4 pixels from the edges and 8 // apart, stacked top to bottom. The bottom 13 pixels stay clear for the overlay's toggle // hint. const const EDGE: 4EDGE = 4; const const PANEL_GAP: 8PANEL_GAP = 8; const const PANEL_W: numberPANEL_W = 320 - 2 * const EDGE: 4EDGE; // 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. 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; // How tall the artwork band inside each panel is, and where each panel starts. const const SKY_ART_H: 32SKY_ART_H = 32; const const FIRE_ART_H: 40FIRE_ART_H = 40; const const WATER_ART_H: 40WATER_ART_H = 40; const const SKY_PANEL_Y: 4SKY_PANEL_Y = const EDGE: 4EDGE; const const FIRE_PANEL_Y: numberFIRE_PANEL_Y = const SKY_PANEL_Y: 4SKY_PANEL_Y + const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP + const SKY_ART_H: 32SKY_ART_H + const PANEL_BOTTOM: 11PANEL_BOTTOM + const PANEL_GAP: 8PANEL_GAP; const const WATER_PANEL_Y: numberWATER_PANEL_Y = const FIRE_PANEL_Y: numberFIRE_PANEL_Y + const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP + const FIRE_ART_H: 40FIRE_ART_H + const PANEL_BOTTOM: 11PANEL_BOTTOM + const PANEL_GAP: 8PANEL_GAP; // The fire and water grids: 24 fire columns 12 pixels apart and 36 water columns 8 pixels // apart, both 288 pixels wide, with rows of 5-pixel-tall blocks. const const FIRE_COLS: 24FIRE_COLS = 24; const const FIRE_COL_STEP: 12FIRE_COL_STEP = 12; const const WATER_COLS: 36WATER_COLS = 36; const const WATER_COL_STEP: 8WATER_COL_STEP = 8; const const BLOCK_H: 5BLOCK_H = 5; const const GRID_W: 288GRID_W = 288; // 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 six // low slots, which init() fills by hand with fixed colors. const const C_OVERLAY_TAG: 1C_OVERLAY_TAG = 1; // chart milestone tags (white) const const C_OVERLAY_BAR: 2C_OVERLAY_BAR = 2; // overlay bar background (very dark navy) const const C_OVERLAY_RENDER: 3C_OVERLAY_RENDER = 3; // timing chart render bars (dark blue) const const C_OVERLAY_TEXT: 4C_OVERLAY_TEXT = 4; // overlay text and chart update bars (golden yellow) const const C_OVERLAY_WARN: 5C_OVERLAY_WARN = 5; // timing chart warnings (cool gray-blue) const const C_OVERLAY_ERR: 6C_OVERLAY_ERR = 6; // timing chart error bars (dark gray-violet) // Sky gradient: slots 10-19 (10 slots). const const C_SKY_BASE: 10C_SKY_BASE = 10; // Fire gradient: slots 30-35 (6 slots). const const C_FIRE_BASE: 30C_FIRE_BASE = 30; // Water gradient: slots 50-57 (8 slots). const const C_WATER_BASE: 50C_WATER_BASE = 50; // Fills a run of palette slots with a smooth gradient. For each slot we compute // t = i / (count - 1): think of t as "how far along the gradient are we?" - the first // slot gets t = 0 (the start), the last slot gets t = 1 (the end), and the slots between // get evenly spaced fractions like 0.25 or 0.5 (exactly halfway). The `colorAt` callback // turns that fraction into the actual Color32 for the slot, so each gradient only has to // describe its own start and end colors. `baseSlot` is the first slot of the run. function function fillGradient(palette: any, baseSlot: any, count: any, colorAt: any): voidfillGradient(palette: anypalette, baseSlot: anybaseSlot, count: anycount, colorAt: anycolorAt) { for (let let i: numberi = 0; let i: numberi < count: anycount; let i: numberi++) { palette: anypalette.set(baseSlot: anybaseSlot + let i: numberi, colorAt: anycolorAt(let i: numberi / (count: anycount - 1))); } } // Demonstrates BT.paletteCycle() for automatic palette rotation and BT.paletteSwap() for // an instant exchange of two slots. class class DemoDemo { // The active palette, built in init(). /** @type {Palette | null} */ Demo.palette: Palette | null
@type{Palette | null}
palette
= null;
// Palette slot map for the shared UI kit theme, filled in init() by applyTheme(). // theme.bg, theme.dim, and friends are palette indices ready for BT draw calls. Demo.theme: nulltheme = null; // Tick of the last swap, so we know when the next one is due. Demo.lastSwapTick: numberlastSwapTick = 0; // Which two slots were last swapped, for the caption. Demo.swappedA: numberswappedA = 0; Demo.swappedB: numberswappedB = 0; // Whether the swap caption is currently shown. Demo.showSwapLabel: booleanshowSwapLabel = false; // 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): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(0, 0, 0, 0);
// Palette cycling runs in the engine every tick; the timing chart shows update vs // render time. The overlay style colors come from the dedicated low slots set in // init().
Demo.configure(): {
    isOverlayPaletteEnabled: boolean;
    isOverlayVisibleAtStart: boolean;
    overlayPaletteRowsVisible: number;
    overlayStyle: {
        barPaletteIndex: number;
        textPaletteIndex: number;
        gapPaletteIndex: number;
    };
}
configure
() {
return { isOverlayPaletteEnabled: booleanisOverlayPaletteEnabled: true, isOverlayVisibleAtStart: booleanisOverlayVisibleAtStart: true, overlayPaletteRowsVisible: numberoverlayPaletteRowsVisible: 2,
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, gapPaletteIndex: numbergapPaletteIndex: const C_OVERLAY_BAR: 2C_OVERLAY_BAR, }, }; } // Builds the palette with gradient colors and starts the cycling effects. async Demo.init(): unknowninit() { console.log('[PaletteCyclingDemo] Initializing...'); this.Demo.palette: Palette | null
@type{Palette | null}
palette
=
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.paletteCreate: (size?: number) => Palette
Creates a standalone palette instance.
@since1.0.3@paramsize - Palette size. Defaults to 256 colors.@returnsNew mutable palette.
paletteCreate
(256);
// Colors for the engine overlay (the stats HUD). They live in low slots so // configure() could name them before the UI theme existed. this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_OVERLAY_TAG: 1C_OVERLAY_TAG, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(255, 255, 255));
this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_OVERLAY_BAR: 2C_OVERLAY_BAR, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(10, 12, 20));
this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_OVERLAY_RENDER: 3C_OVERLAY_RENDER, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(20, 24, 36));
this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_OVERLAY_TEXT: 4C_OVERLAY_TEXT, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(255, 210, 80));
this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_OVERLAY_WARN: 5C_OVERLAY_WARN, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(120, 130, 160));
this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_OVERLAY_ERR: 6C_OVERLAY_ERR, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(70, 70, 90));
// The three gradients below all use the fillGradient() helper defined above the // class; each one only supplies its own start and end channel values. // Sky gradient: deep purple to soft pink (red 40-160, green 20-60, blue 80-180). function fillGradient(palette: any, baseSlot: any, count: any, colorAt: any): voidfillGradient( this.Demo.palette: Palette
@type{Palette | null}
palette
,
const C_SKY_BASE: 10C_SKY_BASE, const SKY_SLOTS: 10SKY_SLOTS, (t: anyt) => new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(Math.floor(40 + t: anyt * 120), Math.floor(20 + t: anyt * 40), Math.floor(80 + t: anyt * 100)),
); // Fire gradient: dark red to bright yellow (red 80-255, green 0-200, blue 0-40). function fillGradient(palette: any, baseSlot: any, count: any, colorAt: any): voidfillGradient( this.Demo.palette: Palette
@type{Palette | null}
palette
,
const C_FIRE_BASE: 30C_FIRE_BASE, const FIRE_SLOTS: 6FIRE_SLOTS, (t: anyt) => new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(Math.floor(80 + t: anyt * 175), Math.floor(t: anyt * 200), Math.floor(t: anyt * 40)),
); // Water gradient: dark blue to bright cyan (red 0-60, green 40-200, blue // 100-255). function fillGradient(palette: any, baseSlot: any, count: any, colorAt: any): voidfillGradient( this.Demo.palette: Palette
@type{Palette | null}
palette
,
const C_WATER_BASE: 50C_WATER_BASE, const WATER_SLOTS: 8WATER_SLOTS, (t: anyt) => new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(Math.floor(t: anyt * 60), Math.floor(40 + t: anyt * 160), Math.floor(100 + t: anyt * 155)),
); // Install the shared UI kit colors (panel fills, borders, headings, dim text). // applyTheme() writes 12 colors into slots 240-251 - far above every range the // engine cycles here (sky 10-19, fire 30-35, water 50-57), so the rotation can // never touch the UI theme. this.Demo.theme: nulltheme = import applyThemeapplyTheme(this.Demo.palette: Palette
@type{Palette | null}
palette
);
// Make this palette the one the engine draws with.
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) => void
Stores the active engine palette. Use this to swap the **entire palette** (e.g. switch between a day and night theme). After this call the renderer uploads the new palette uniform on the next frame. **Palette-value swap (change what a slot looks like):** mutate the live {@link BT.palette } in place with `palette.set(slot, newColor)`. The renderer uploads dirty slots on the next frame; no `paletteSet()` or {@link BT.spritesRefresh } needed. **Palette-layout swap (same colors, different slot positions):** build a new palette with the same colors at new indices, call `paletteSet()`, then call {@link BT.spritesRefresh } so every sprite sheet re-maps its original RGBA pixels against the new slot layout.
@since1.0.3@parampalette - Palette to make active.
paletteSet
(this.Demo.palette: Palette
@type{Palette | null}
palette
);
// Start the cycling effects. The engine runs them once per tick until // BT.paletteClearEffects() is called - this demo never calls it, so they run // forever. Each call names the first and last slot of a range (both inclusive) // and a speed in steps per second.
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
.paletteCycle: (start: number, end: number, speed: number) => void
Starts rotating a range of palette entries at a constant speed. Classic water/fire/plasma animation technique. Runs indefinitely until canceled via {@link BT.paletteClearEffects } . Uses a fractional accumulator, so slow speeds still step exactly on time.
@since1.0.3@changed1.8.0 Advances once per fixed update on the tick clock instead of once per rendered frame on wall time.@paramstart - First palette index in the cycling range (inclusive).@paramend - Last palette index in the cycling range (inclusive).@paramspeed - Steps per second. Positive = forward, negative = backward.
paletteCycle
(const C_SKY_BASE: 10C_SKY_BASE, const C_SKY_BASE: 10C_SKY_BASE + const SKY_SLOTS: 10SKY_SLOTS - 1, const SKY_SPEED: 0.5SKY_SPEED);
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
.paletteCycle: (start: number, end: number, speed: number) => void
Starts rotating a range of palette entries at a constant speed. Classic water/fire/plasma animation technique. Runs indefinitely until canceled via {@link BT.paletteClearEffects } . Uses a fractional accumulator, so slow speeds still step exactly on time.
@since1.0.3@changed1.8.0 Advances once per fixed update on the tick clock instead of once per rendered frame on wall time.@paramstart - First palette index in the cycling range (inclusive).@paramend - Last palette index in the cycling range (inclusive).@paramspeed - Steps per second. Positive = forward, negative = backward.
paletteCycle
(const C_FIRE_BASE: 30C_FIRE_BASE, const C_FIRE_BASE: 30C_FIRE_BASE + const FIRE_SLOTS: 6FIRE_SLOTS - 1, const FIRE_SPEED: -6FIRE_SPEED);
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
.paletteCycle: (start: number, end: number, speed: number) => void
Starts rotating a range of palette entries at a constant speed. Classic water/fire/plasma animation technique. Runs indefinitely until canceled via {@link BT.paletteClearEffects } . Uses a fractional accumulator, so slow speeds still step exactly on time.
@since1.0.3@changed1.8.0 Advances once per fixed update on the tick clock instead of once per rendered frame on wall time.@paramstart - First palette index in the cycling range (inclusive).@paramend - Last palette index in the cycling range (inclusive).@paramspeed - Steps per second. Positive = forward, negative = backward.
paletteCycle
(const C_WATER_BASE: 50C_WATER_BASE, const C_WATER_BASE: 50C_WATER_BASE + const WATER_SLOTS: 8WATER_SLOTS - 1, const WATER_SPEED: 4WATER_SPEED);
console.log('[PaletteCyclingDemo] Initialized'); return true; } // Swaps two fire slots every SWAP_INTERVAL ticks to demonstrate BT.paletteSwap(), and // hides the caption a second later. 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: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
;
// The pair is not random - it is computed from the tick counter, so each swap // picks a predictable pair: tick % FIRE_SLOTS and (tick + 3) % FIRE_SLOTS. `%` is // the remainder after division, so both land inside the 6 fire slots. if (const tick: numbertick - this.Demo.lastSwapTick: numberlastSwapTick >= const SWAP_INTERVAL: 180SWAP_INTERVAL) { // Two slots within the fire range, always 3 apart. this.Demo.swappedA: numberswappedA = const C_FIRE_BASE: 30C_FIRE_BASE + (const tick: numbertick % const FIRE_SLOTS: 6FIRE_SLOTS); this.Demo.swappedB: numberswappedB = const C_FIRE_BASE: 30C_FIRE_BASE + ((const tick: numbertick + 3) % const FIRE_SLOTS: 6FIRE_SLOTS); // The two slots can never be equal here, so this check is just a safety net. if (this.Demo.swappedA: numberswappedA !== this.Demo.swappedB: numberswappedB) {
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
.paletteSwap: (indexA: number, indexB: number) => void
Instantly exchanges two palette entries. This is an immediate operation, not an animated effect. The visual change takes effect on the next frame.
@since1.0.3@paramindexA - First palette index.@paramindexB - Second palette index.
paletteSwap
(this.Demo.swappedA: numberswappedA, this.Demo.swappedB: numberswappedB);
this.Demo.showSwapLabel: booleanshowSwapLabel = true; } this.Demo.lastSwapTick: numberlastSwapTick = const tick: numbertick; } // Hide the swap caption after 60 ticks (1 second). if (this.Demo.showSwapLabel: booleanshowSwapLabel && const tick: numbertick - this.Demo.lastSwapTick: numberlastSwapTick > 60) { this.Demo.showSwapLabel: booleanshowSwapLabel = false; } } // Draws the three animated bands and their kit-panel headings. No Color32 objects // here - only palette indices. Demo.render(): voidrender() {
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) => void
Sets the frame clear color using a palette index. The renderer uses this color when clearing the full display at the start of the next frame.
@since0.1.0@parampaletteIndex - Palette index for the full-screen clear pass.
clear
(this.Demo.theme: nulltheme.bg);
this.Demo.renderSkyPanel(): voidrenderSkyPanel(); this.Demo.renderFirePanel(): voidrenderFirePanel(); this.Demo.renderWaterPanel(): voidrenderWaterPanel(); } // Opens and closes a titled kit panel with an empty body `artH` pixels tall. The // artwork is drawn after it with ui.drawRectFill(), not BT.drawRectFill(): 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 call order. Demo.renderPanel(y: any, artH: any, title: any): voidrenderPanel(y: anyy, artH: anyartH, title: anytitle) { import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { x: numberx: const EDGE: 4EDGE, y: anyy, width: numberwidth: const PANEL_W: numberPANEL_W }); import uiui.panel(title: anytitle); import uiui.spacer(artH: anyartH); import uiui.end(); } // Fills one block of artwork on top of a panel. Demo.fillBlock(x: any, y: any, width: any, height: any, slot: any): voidfillBlock(x: anyx, y: anyy, width: anywidth, height: anyheight, slot: anyslot) { this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2i
Sets all components of this rectangle. Modifies this rectangle directly for maximum performance. WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.
@paramx - New x position.@paramy - New y position.@paramwidth - New width.@paramheight - New height.@returnsThis rectangle for chaining.
set
(x: anyx, y: anyy, width: anywidth, height: anyheight);
import uiui.drawRectFill(this.Demo.tempRect: Rect2itempRect, slot: anyslot); } // Sky: ten full-width stripes, one per sky slot, 3 pixels tall. The slow cycling // makes the twilight colors creep up the band. The panel title is the exact call // from init() that drives it. Demo.renderSkyPanel(): voidrenderSkyPanel() { this.Demo.renderPanel(y: any, artH: any, title: any): voidrenderPanel(const SKY_PANEL_Y: 4SKY_PANEL_Y, const SKY_ART_H: 32SKY_ART_H, 'BT.paletteCycle(10, 19, 0.5)'); const const x: numberx = const EDGE: 4EDGE + const PANEL_TEXT_INSET: 10PANEL_TEXT_INSET; const const top: numbertop = const SKY_PANEL_Y: 4SKY_PANEL_Y + const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP + 1; for (let let i: numberi = 0; let i: numberi < const SKY_SLOTS: 10SKY_SLOTS; let i: numberi++) { this.Demo.fillBlock(x: any, y: any, width: any, height: any, slot: any): voidfillBlock(const x: numberx, const top: numbertop + let i: numberi * 3, const PANEL_W: numberPANEL_W - 2 * const PANEL_TEXT_INSET: 10PANEL_TEXT_INSET, 3, const C_SKY_BASE: 10C_SKY_BASE + let i: numberi); } } // Fire: 24 narrow columns of 8 blocks. Down each column the slot goes up by one per // row, so every column is a copy of the gradient, but each column starts a little // further along it than its neighbor. The start point follows a sine wave, so it // rises and falls smoothly across the panel and the color bands bend into flame // tongues instead of lying flat. Demo.renderFirePanel(): voidrenderFirePanel() { this.Demo.renderPanel(y: any, artH: any, title: any): voidrenderPanel(const FIRE_PANEL_Y: numberFIRE_PANEL_Y, const FIRE_ART_H: 40FIRE_ART_H, 'BT.paletteCycle(30, 35, -6)'); const const x0: numberx0 = const EDGE: 4EDGE + (const PANEL_W: numberPANEL_W - const GRID_W: 288GRID_W) / 2; const const top: numbertop = const FIRE_PANEL_Y: numberFIRE_PANEL_Y + const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP; for (let let col: numbercol = 0; let col: numbercol < const FIRE_COLS: 24FIRE_COLS; let col: numbercol++) { // Math.sin() swings between -1 and 1; + 1 makes that 0 to 2, and * 1.5 makes // it 0 to 3. Math.floor() turns it into a whole step: 0, 1, 2, or 3. const const phase: anyphase = Math.floor((Math.sin(let col: numbercol * 0.8) + 1) * 1.5); for (let let row: numberrow = 0; let row: numberrow < const FIRE_ART_H: 40FIRE_ART_H / const BLOCK_H: 5BLOCK_H; let row: numberrow++) { const const slot: numberslot = const C_FIRE_BASE: 30C_FIRE_BASE + ((let row: numberrow + const phase: anyphase) % const FIRE_SLOTS: 6FIRE_SLOTS); this.Demo.fillBlock(x: any, y: any, width: any, height: any, slot: any): voidfillBlock(const x0: numberx0 + let col: numbercol * const FIRE_COL_STEP: 12FIRE_COL_STEP, const top: numbertop + let row: numberrow * const BLOCK_H: 5BLOCK_H, const FIRE_COL_STEP: 12FIRE_COL_STEP - 1, const BLOCK_H: 5BLOCK_H, const slot: numberslot); } } // The swap caption, while it is active: right-aligned in the panel's title row, // clear of the artwork. Every character of the system font is 6 pixels wide, so // the text is label.length * 6 pixels wide. if (this.Demo.showSwapLabel: booleanshowSwapLabel) { const const label: stringlabel = `swapped ${this.Demo.swappedA: numberswappedA} <-> ${this.Demo.swappedB: numberswappedB}`; const const labelX: numberlabelX = const EDGE: 4EDGE + const PANEL_W: numberPANEL_W - const PANEL_TEXT_INSET: 10PANEL_TEXT_INSET - const label: stringlabel.length * 6; import uiui.caption(const labelX: numberlabelX, const FIRE_PANEL_Y: numberFIRE_PANEL_Y + 11, const label: stringlabel, { color: stringcolor: 'warm' }); } } // Water: a grid of 36 columns by 8 rows. The slot goes up by one per column and one // per row, so the gradient runs diagonally, and the forward cycling makes the // diagonal waves roll across the grid. Demo.renderWaterPanel(): voidrenderWaterPanel() { this.Demo.renderPanel(y: any, artH: any, title: any): voidrenderPanel(const WATER_PANEL_Y: numberWATER_PANEL_Y, const WATER_ART_H: 40WATER_ART_H, 'BT.paletteCycle(50, 57, 4)'); const const x0: numberx0 = const EDGE: 4EDGE + (const PANEL_W: numberPANEL_W - const GRID_W: 288GRID_W) / 2; const const top: numbertop = const WATER_PANEL_Y: numberWATER_PANEL_Y + const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP; for (let let row: numberrow = 0; let row: numberrow < const WATER_ART_H: 40WATER_ART_H / const BLOCK_H: 5BLOCK_H; let row: numberrow++) { for (let let col: numbercol = 0; let col: numbercol < const WATER_COLS: 36WATER_COLS; let col: numbercol++) { const const slot: numberslot = const C_WATER_BASE: 50C_WATER_BASE + ((let col: numbercol + let row: numberrow) % const WATER_SLOTS: 8WATER_SLOTS); this.Demo.fillBlock(x: any, y: any, width: any, height: any, slot: any): voidfillBlock(const x0: numberx0 + let col: numbercol * const WATER_COL_STEP: 8WATER_COL_STEP, const top: numbertop + let row: numberrow * const BLOCK_H: 5BLOCK_H, const WATER_COL_STEP: 8WATER_COL_STEP - 1, const BLOCK_H: 5BLOCK_H, const slot: numberslot); } } } } function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
(class DemoDemo);