// 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 series.
//
// Prerequisites:
// Basics https://demos.blit386.dev/basics
// Primitives https://demos.blit386.dev/primitives
// Palette Presets https://demos.blit386.dev/palette-presets
// Palette Animation https://demos.blit386.dev/palette-animation
// (guides: https://blit386.dev/docs/guides/palette-presets,
// https://blit386.dev/docs/guides/palette#runtime-palette-effects)
//
// Live version: https://demos.blit386.dev/palette-cycling
// Guide: https://blit386.dev/docs/guides/palette#runtime-palette-effects
//
// 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 forward, wrap the last
// one to the beginning - 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
// each frame. Positive speed = forward, negative = backward.
//
// UPDATE VS RENDER (palette work split):
// init() registers gradient colors and starts BT.paletteCycle() - the engine
// rotates those slot ranges automatically every frame.
// update() only runs the periodic BT.paletteSwap() demo and hides its label.
// render() draws fixed rectangles using palette index numbers; the bands
// appear to flow because the engine shifted 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 high palette slots (240 and up), far away from every cycling range.
//
// WHAT YOU WILL SEE (three horizontal bands):
// 1. Sky (top) - purple-pink slots cycling very slowly = twilight drift
// 2. Fire (middle) - orange-yellow slots cycling backward = rising flames
// 3. Water (bottom) - blue gradient slots cycling forward = flowing water
//
// Plus a palette swap demonstration every few seconds.
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT, class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32, 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').IBTDemo} IBTDemo */
/** @typedef {import('blit386').HardwareSettings} HardwareSettings */
/** @typedef {import('blit386').Palette} Palette */
// Water
// 8 blue-gradient 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 backward 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().
const const SWAP_INTERVAL: 180SWAP_INTERVAL = 180; // ~3 seconds at 60 FPS
// Slot 0: 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.
*
* @param {Palette} palette - The palette to write into.
* @param {number} baseSlot - The first slot of the gradient run.
* @param {number} count - How many slots to fill.
* @param {(t: number) => Color32} colorAt - Returns the color for progress t (0..1).
*/
function function fillGradient(palette: Palette, baseSlot: number, count: number, colorAt: (t: number) => Color32): voidFills 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.fillGradient(palette: Palette- The palette to write into.palette, baseSlot: number- The first slot of the gradient run.baseSlot, count: number- How many slots to fill.count, colorAt: (t: number) => Color32- Returns the color for progress t (0..1).colorAt) {
for (let let i: numberi = 0; let i: numberi < count: number- How many slots to fill.count; let i: numberi++) {
palette: Palette- The palette to write into.palette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(baseSlot: number- The first slot of the gradient run.baseSlot + let i: numberi, colorAt: (t: number) => Color32- Returns the color for progress t (0..1).colorAt(let i: numberi / (count: number- How many slots to fill.count - 1)));
}
}
/**
* Demonstrates BT.paletteCycle() for automatic palette rotation, plus
* BT.paletteSwap() for instant entry exchange and BT.paletteClearEffects()
* for stopping all running effects.
*
* @implements {IBTDemo}
*/
class class DemoDemonstrates BT.paletteCycle() for automatic palette rotation, plus
BT.paletteSwap() for instant entry exchange and BT.paletteClearEffects()
for stopping all running effects.Demo {
/** @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, ... are palette indices ready for BT draw calls.
Demo.theme: nulltheme = null;
// Track the last swap tick so we know when to do the next swap demo.
Demo.lastSwapTick: numberlastSwapTick = 0;
// Which two slots were last swapped (for the UI label).
Demo.swappedA: numberswappedA = 0;
Demo.swappedB: numberswappedB = 0;
Demo.showSwapLabel: booleanshowSwapLabel = false;
/**
* Palette cycling runs in the engine each frame; the chart shows update vs render
* time. The overlay style colors come from the dedicated low slots set in init().
*
* @returns {Partial<HardwareSettings>}
*/
Demo.configure(): Partial<HardwareSettings>Palette cycling runs in the engine each frame; the chart shows update vs render
time. The overlay style colors come from the dedicated low slots set in init().configure() {
return {
isOverlayTimingChartEnabled: booleanisOverlayTimingChartEnabled: true,
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,
},
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_RENDER: 3C_OVERLAY_RENDER,
warningPaletteIndex: numberwarningPaletteIndex: const C_OVERLAY_WARN: 5C_OVERLAY_WARN,
errorPaletteIndex: numbererrorPaletteIndex: const C_OVERLAY_ERR: 6C_OVERLAY_ERR,
tagPaletteIndex: numbertagPaletteIndex: const C_OVERLAY_TAG: 1C_OVERLAY_TAG,
},
};
}
/**
* Builds the palette with gradient colors and starts the cycling effects.
*
* @returns {Promise<boolean>}
*/
async Demo.init(): Promise<boolean>Builds the palette with gradient colors and starts the cycling effects.init() {
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;
... 106 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). These live in low slots so
// configure() could reference 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 same 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: Palette, baseSlot: number, count: number, colorAt: (t: number) => Color32): voidFills 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.fillGradient(
this.Demo.palette: Palettepalette,
const C_SKY_BASE: 10C_SKY_BASE,
const SKY_SLOTS: 10SKY_SLOTS,
(t: numbert) => new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(Math.floor(40 + t: numbert * 120), Math.floor(20 + t: numbert * 40), Math.floor(80 + t: numbert * 100)),
);
// Fire gradient: dark red to bright yellow (red 80..255, green 0..200, blue 0..40).
function fillGradient(palette: Palette, baseSlot: number, count: number, colorAt: (t: number) => Color32): voidFills 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.fillGradient(
this.Demo.palette: Palettepalette,
const C_FIRE_BASE: 30C_FIRE_BASE,
const FIRE_SLOTS: 6FIRE_SLOTS,
(t: numbert) => new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(Math.floor(80 + t: numbert * 175), Math.floor(t: numbert * 200), Math.floor(t: numbert * 40)),
);
// Water gradient: dark blue to bright cyan (red 0..60, green 40..200, blue 100..255).
function fillGradient(palette: Palette, baseSlot: number, count: number, colorAt: (t: number) => Color32): voidFills 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.fillGradient(
this.Demo.palette: Palettepalette,
const C_WATER_BASE: 50C_WATER_BASE,
const WATER_SLOTS: 8WATER_SLOTS,
(t: numbert) => new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(Math.floor(t: numbert * 60), Math.floor(40 + t: numbert * 160), Math.floor(100 + t: numbert * 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);
// Activate 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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteSet: (palette: Palette) => voidStores the active engine palette.
Use this to swap the **entire palette** (e.g. switch between a day and night
theme). After this call the renderer uploads the new palette uniform on the
next frame.
**Palette-value swap (change what a slot looks like):** mutate the live
{@link
BT.palette
}
in place with `palette.set(slot, newColor)`. The renderer
uploads dirty slots on the next frame; no `paletteSet()` or
{@link
BT.spritesRefresh
}
needed.
**Palette-layout swap (same colors, different slot positions):** build a new
palette with the same colors at new indices, call `paletteSet()`, then call
{@link
BT.spritesRefresh
}
so every sprite sheet re-maps its original RGBA
pixels against the new slot layout.paletteSet(this.Demo.palette: Palettepalette);
// Start cycling effects
// These run automatically each frame until we call BT.paletteClearEffects().
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.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
for sub-frame precision.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;
... 106 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
for sub-frame precision.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;
... 106 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
for sub-frame precision.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;
}
/**
* Periodically swaps two fire palette entries to demonstrate BT.paletteSwap().
*/
Demo.update(): voidPeriodically swaps two fire palette entries to demonstrate BT.paletteSwap().update() {
const const tick: numbertick = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.ticks: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks;
// Every SWAP_INTERVAL ticks, swap two fire slots to demonstrate BT.paletteSwap().
// 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.
if (const tick: numbertick - this.Demo.lastSwapTick: numberlastSwapTick >= const SWAP_INTERVAL: 180SWAP_INTERVAL) {
// Pick two different slots within the fire range.
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);
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;
... 106 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 label 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(): voidDraws the three animated bands and their kit-panel headings.
No Color32 objects here - only palette indices.render() {
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.clear: (paletteIndex: number) => voidSets the frame clear color using a palette index.
The renderer uses this color when clearing the full display at the start
of the next frame.clear(this.Demo.theme: nulltheme.bg);
this.Demo.renderSkyPanel(): voidSky band: 10 horizontal stripes at the top, each using one sky slot.
The slow cycling makes the twilight colors gently shift.renderSkyPanel();
this.Demo.renderFirePanel(): voidFire band: 6 vertical columns in the middle section.
Negative speed makes the colors cycle backward (rising flame illusion).renderFirePanel();
this.Demo.renderWaterPanel(): voidWater band: 8 vertical columns at the bottom.
Forward cycling at 4 steps/sec makes colors flow like water.renderWaterPanel();
}
/**
* Sky band: 10 horizontal stripes at the top, each using one sky slot.
* The slow cycling makes the twilight colors gently shift.
*/
Demo.renderSkyPanel(): voidSky band: 10 horizontal stripes at the top, each using one sky slot.
The slow cycling makes the twilight colors gently shift.renderSkyPanel() {
const const bandY: 6bandY = 6;
const const stripeH: 5stripeH = 5;
// The heading lives in a kit panel pinned to the band position. ui.end() draws
// the panel right away, so the stripes drawn after it land ON TOP of the panel
// background. ui.spacer() reserves empty rows for that artwork.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { x: numberx: 0, y: numbery: const bandY: 6bandY, width: numberwidth: 320 });
import uiui.panel('Sky (0.5 steps/sec, forward)');
import uiui.spacer(40);
import uiui.end();
// Draw 10 horizontal stripes, repeated twice for fullness.
for (let let row: numberrow = 0; let row: numberrow < 2; let row: numberrow++) {
for (let let i: numberi = 0; let i: numberi < const SKY_SLOTS: 10SKY_SLOTS; let i: numberi++) {
const const y: numbery = const bandY: 6bandY + 20 + (let row: numberrow * (const stripeH: 5stripeH * const SKY_SLOTS: 10SKY_SLOTS)) / 2 + let i: numberi * const stripeH: 5stripeH;
// Only draw stripes that fit inside the panel's content area.
if (const y: numbery + const stripeH: 5stripeH <= const bandY: 6bandY + 60) {
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(6, const y: numbery, 308, const stripeH: 5stripeH), const C_SKY_BASE: 10C_SKY_BASE + (let i: numberi % const SKY_SLOTS: 10SKY_SLOTS));
}
}
}
}
/**
* Fire band: 6 vertical columns in the middle section.
* Negative speed makes the colors cycle backward (rising flame illusion).
*/
Demo.renderFirePanel(): voidFire band: 6 vertical columns in the middle section.
Negative speed makes the colors cycle backward (rising flame illusion).renderFirePanel() {
const const bandY: 76bandY = 76;
const const colW: anycolW = Math.floor(308 / const FIRE_SLOTS: 6FIRE_SLOTS);
// Kit panel with the heading; the spacer reserves room for the fire columns.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { x: numberx: 0, y: numbery: const bandY: 76bandY, width: numberwidth: 320 });
import uiui.panel('Fire (-6 steps/sec, backward)');
import uiui.spacer(40);
import uiui.end();
// Draw fire columns.
for (let let i: numberi = 0; let i: numberi < const FIRE_SLOTS: 6FIRE_SLOTS; let i: numberi++) {
// Each column is drawn with multiple rows of the same slot to make it taller.
for (let let row: numberrow = 0; let row: numberrow < 8; let row: numberrow++) {
const const x: numberx = 6 + let i: numberi * const colW: anycolW;
const const y: numbery = const bandY: 76bandY + 20 + let row: numberrow * 5;
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(const x: numberx, const y: numbery, const colW: anycolW - 1, 5), const C_FIRE_BASE: 30C_FIRE_BASE + ((let i: numberi + let row: numberrow) % const FIRE_SLOTS: 6FIRE_SLOTS));
}
}
// Show swap label if active (shared caption helper, same as patterns.js).
if (this.Demo.showSwapLabel: booleanshowSwapLabel) {
import uiui.caption(6, const bandY: 76bandY + 44, `Swapped slots ${this.Demo.swappedA: numberswappedA} <-> ${this.Demo.swappedB: numberswappedB}`);
}
}
/**
* Water band: 8 vertical columns at the bottom.
* Forward cycling at 4 steps/sec makes colors flow like water.
*/
Demo.renderWaterPanel(): voidWater band: 8 vertical columns at the bottom.
Forward cycling at 4 steps/sec makes colors flow like water.renderWaterPanel() {
const const bandY: 146bandY = 146;
const const colW: anycolW = Math.floor(308 / const WATER_SLOTS: 8WATER_SLOTS);
// Kit panel with the heading; the spacer reserves room for the water tiles.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { x: numberx: 0, y: numbery: const bandY: 146bandY, width: numberwidth: 320 });
import uiui.panel('Water (4 steps/sec, forward)');
import uiui.spacer(54);
import uiui.end();
// Draw water tiles in a grid pattern.
for (let let row: numberrow = 0; let row: numberrow < 10; let row: numberrow++) {
for (let let col: numbercol = 0; let col: numbercol < const WATER_SLOTS: 8WATER_SLOTS; let col: numbercol++) {
const const x: numberx = 6 + let col: numbercol * const colW: anycolW;
const const y: numbery = const bandY: 146bandY + 20 + let row: numberrow * 5;
// Offset the slot index by the row to create a diagonal wave pattern.
const const slot: numberslot = const C_WATER_BASE: 50C_WATER_BASE + ((let col: numbercol + let row: numberrow) % const WATER_SLOTS: 8WATER_SLOTS);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(const x: numberx, const y: numbery, const colW: anycolW - 1, 5), const slot: numberslot);
}
}
// Explanatory text over the tiles (shared caption helper, same as patterns.js).
import uiui.caption(6, const bandY: 146bandY + 58, 'BT.paletteCycle() runs automatically');
}
}
function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>One-liner bootstrap function for BLIT386 demos.
Handles canvas retrieval and engine initialization. Backend selection
(WebGPU or software fallback) is managed internally by BTAPI.
This function provides a streamlined way to start a demo with sensible defaults
while allowing customization through options.bootstrap(class DemoDemonstrates BT.paletteCycle() for automatic palette rotation, plus
BT.paletteSwap() for instant entry exchange and BT.paletteClearEffects()
for stopping all running effects.Demo);