/*
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.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 } 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 | nullpalette = 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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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 | 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 live in low slots so
// configure() could name them before the UI theme existed.
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_TAG: 1C_OVERLAY_TAG, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(255, 255, 255));
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));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_RENDER: 3C_OVERLAY_RENDER, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(20, 24, 36));
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));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_WARN: 5C_OVERLAY_WARN, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(120, 130, 160));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_ERR: 6C_OVERLAY_ERR, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.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: Palettepalette,
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): Color32Creates a clamped 8-bit RGBA color.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: Palettepalette,
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): Color32Creates a clamped 8-bit RGBA color.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: Palettepalette,
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): Color32Creates a clamped 8-bit RGBA color.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: Palettepalette);
// 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) => 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);
// 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) => voidStarts 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.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) => voidStarts 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.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) => voidStarts 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.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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.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) => voidInstantly exchanges two palette entries.
This is an immediate operation, not an animated effect. The visual change
takes effect on the next frame.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) => 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);
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): 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);
}
// 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.bootstrap(class DemoDemo);