// Palette Swap: change the active palette at runtime to switch color themes.
// @description Switch the active palette at runtime to recolor an entire scene without touching the drawing code.
//
// Part of the BLIT386 series.
//
// Prerequisites:
//   Basics            https://demos.blit386.dev/basics
//   Sprites           https://demos.blit386.dev/sprites
//   Palette Presets   https://demos.blit386.dev/palette-presets
//   Palette Animation https://demos.blit386.dev/palette-animation
//     (guides: https://blit386.dev/docs/api/rendering#sprites,
//      https://blit386.dev/docs/guides/palette-presets,
//      https://blit386.dev/docs/guides/palette#runtime-palette-effects)
//
// Live version: https://demos.blit386.dev/palette-swap
// Guide: https://blit386.dev/docs/guides/palette#layout-swap-vs-value-swap
//
// WHAT IS PALETTE SWAP?
//
// In Palette Animation demo we changed palette SLOT VALUES while keeping the same Palette object.
// "Palette swap" goes further: you have MULTIPLE Palette objects (one per color theme),
// and at runtime you switch WHICH palette is active.
//
// Imagine painting with a box of paints. Instead of mixing new colors one at a time,
// you grab a completely different paint box. Every slot gets replaced at once.
//
// HOW DOES THE ENGINE STAY IN SYNC?
//
// When you call BT.paletteSet(newPalette):
//   1. The engine uploads the new palette to the GPU.
//   2. All drawing calls now look up colors from the new palette.
//
// If you also want loaded sprite sheets to find their colors in the new palette
// (because the new palette reorganizes WHICH SLOT holds each color), you can call
// BT.spritesRefresh(). That re-maps every sprite's pixels against the new palette
// using the same RGBA matching that indexize() does the first time.
//
// In this demo, each theme palette keeps the sprite colors at the SAME SLOT NUMBERS
// (just with different color values), so BT.spritesRefresh() is not needed.
// We demonstrate it as a call with no visual side-effect and explain when it matters.
//
// WHAT YOU WILL SEE:
//   Left column: a legend of the four themes, each with its own color swatch.
//   Center: one large sprite that changes theme every 2 seconds.
//   Right: code snippet showing how to build and swap palettes.
//
// All captions and the code column are drawn with the shared UI kit (src/shared/ui.js).
// Because this demo swaps the WHOLE palette every 2 seconds, every theme palette must
// carry the same UI kit colors in the same slots - see buildTheme() for the details.

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;
    ... 106 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
,
type Rect2i = Rect2i
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
,
type SpriteSheet = SpriteSheet
class SpriteSheet
Sprite-sheet wrapper around a loaded image asset. The class keeps the original image available for CPU-side inspection while lazily creating and caching a GPU texture for rendering. When possible, `load()` also pre-decodes the source into an `ImageBitmap` so texture uploads preserve pixel-art alpha and color values more reliably. After calling `indexize()`, the sheet stores palette indices rather than RGBA data. The GPU texture becomes an `r8uint` format uploaded via `writeTexture`. The original RGBA bytes are retained so `reindexize()` can re-convert without reloading the image.
@since0.1.0
SpriteSheet
, class Timer
Fixed-tick interval helper for {@link IBTDemo.update } loops. Counts engine ticks ( {@link BT.ticks } ), which advance once per fixed update at {@link HardwareSettings.targetFPS } , not once per {@link IBTDemo.render } frame. Convert ticks to seconds with `intervalTicks / BT.targetFPS`. Tracks a "last fired" tick and reports when a configured interval has elapsed. Useful for periodic events such as particle spawning, score ticks, or palette swaps.
@since1.0.3
Timer
, class Vector2i
Integer 2D vector for pixel-perfect positioning. Used for points, sizes, directions, and camera offsets throughout the engine. The API includes both allocation-free `*To()` / `*InPlace()` variants and convenience methods that return new vectors.
@since0.1.0
Vector2i
} 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').SpriteSheet} SpriteSheet */ /** @typedef {import('blit386').Rect2i} Rect2i */ // How many ticks to hold each theme before switching (2 seconds at 60 FPS). const const SWAP_PERIOD_TICKS: 120SWAP_PERIOD_TICKS = 120; // Where in the palette the sprite's base colors begin. const const COLOR_BASE: 10COLOR_BASE = 10; // Static representative swatches for each theme (one color each, for the theme legend). // These stay the same across ALL theme palettes so the legend looks stable. const const SWATCH_STONE: 30SWATCH_STONE = 30; const const SWATCH_FIRE: 31SWATCH_FIRE = 31; const const SWATCH_ICE: 32SWATCH_ICE = 32; const const SWATCH_VOID: 33SWATCH_VOID = 33; // Engine overlay color slots (same in every theme palette). // The overlay style is declared in configure(), which runs BEFORE init(), so it // needs fixed slot NUMBERS known ahead of time. buildTheme() writes the same // colors into these slots in all four palettes so the overlay never shifts color // when the theme swaps. const const C_OVERLAY_BAR: 40C_OVERLAY_BAR = 40; // Dark navy bar behind overlay text rows. const const C_OVERLAY_GOLD: 41C_OVERLAY_GOLD = 41; // Golden overlay text and update-timing bars. const const C_OVERLAY_BLUE: 42C_OVERLAY_BLUE = 42; // Blue-gray render-timing bars and chart tags. const const C_OVERLAY_DIM: 43C_OVERLAY_DIM = 43; // Dim purple-gray chart warnings. const const C_OVERLAY_GRAY: 44C_OVERLAY_GRAY = 44; // Light gray chart error bars. /** * Demonstrates palette swap: building multiple palettes and switching between them * at runtime using BT.paletteSet() and BT.spritesRefresh(). * * @implements {IBTDemo} */ class class Demo
Demonstrates palette swap: building multiple palettes and switching between them at runtime using BT.paletteSet() and BT.spritesRefresh().
@implementsIBTDemo
Demo
{
// The sprite sheet loaded from test.png. /** @type {SpriteSheet | null} */ Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
= null;
// The rectangular region of the sprite within the sheet. /** @type {Rect2i | null} */ Demo.charSprite: Rect2i | null
@type{Rect2i | null}
charSprite
= null;
// How many unique colors were extracted from the sprite image. Demo.colorCount: numbercolorCount = 0; // Original Color32 objects for the sprite's base colors (used to build theme palettes). Demo.baseColors: {}baseColors = []; // The four theme palettes. We switch between them in update(). // 0 = stone, 1 = fire, 2 = ice, 3 = void. Demo.themes: {}themes = []; // Display names for each theme (shown in the left column). Demo.themeNames: {}themeNames = ['Stone', 'Fire', 'Ice', 'Void']; // Index of the currently active theme (0..3). Demo.currentTheme: numbercurrentTheme = 0; // Fires every 120 ticks (2 seconds) to switch to the next theme. Demo.swapTimer: TimerswapTimer = new new Timer(intervalTicks: number): Timer
Creates a timer that fires once per fixed-tick interval.
@paramintervalTicks - Number of ticks required between firings; must be a positive integer.
Timer
(const SWAP_PERIOD_TICKS: 120SWAP_PERIOD_TICKS);
// Slot map for the shared UI kit theme, filled in init() by applyTheme(). // Maps friendly names (bg, text, dim, header, accent, info, ...) to slot numbers. /** @type {ReturnType<typeof applyTheme> | null} */ Demo.theme: any
@type{ReturnType<typeof applyTheme> | null}
theme
= null;
/** * Timing chart helps compare CPU cost while palettes swap on a timer. * * @returns {Partial<HardwareSettings>} */ Demo.configure(): Partial<HardwareSettings>
Timing chart helps compare CPU cost while palettes swap on a timer.
@returns
configure
() {
return { isOverlayTimingChartEnabled: booleanisOverlayTimingChartEnabled: true,
overlayStyle: {
    barPaletteIndex: number;
    textPaletteIndex: number;
    gapPaletteIndex: number;
}
overlayStyle
: {
barPaletteIndex: numberbarPaletteIndex: const C_OVERLAY_BAR: 40C_OVERLAY_BAR, textPaletteIndex: numbertextPaletteIndex: const C_OVERLAY_GOLD: 41C_OVERLAY_GOLD, gapPaletteIndex: numbergapPaletteIndex: const C_OVERLAY_BAR: 40C_OVERLAY_BAR, },
overlayTimingChartStyle: {
    updateBarPaletteIndex: number;
    renderBarPaletteIndex: number;
    warningPaletteIndex: number;
    errorPaletteIndex: number;
    tagPaletteIndex: number;
}
overlayTimingChartStyle
: {
updateBarPaletteIndex: numberupdateBarPaletteIndex: const C_OVERLAY_GOLD: 41C_OVERLAY_GOLD, renderBarPaletteIndex: numberrenderBarPaletteIndex: const C_OVERLAY_BLUE: 42C_OVERLAY_BLUE, warningPaletteIndex: numberwarningPaletteIndex: const C_OVERLAY_DIM: 43C_OVERLAY_DIM, errorPaletteIndex: numbererrorPaletteIndex: const C_OVERLAY_GRAY: 44C_OVERLAY_GRAY, tagPaletteIndex: numbertagPaletteIndex: const C_OVERLAY_BLUE: 42C_OVERLAY_BLUE, }, }; } /** * Loads the sprite, builds four theme palettes, and calls sheet.indexize(). * * ORDER MATTERS: * 1. Extract unique colors from the sprite PNG. * 2. Build the stone (base) palette with those colors. * 3. Build fire, ice, void palettes by tinting the base colors. * 4. BT.paletteSet(stonePalette) - activate the starting palette. * 5. SpriteSheet.load() + indexize() - link pixels to slot numbers. * * @returns {Promise<boolean>} True when everything is ready. */ async Demo.init(): Promise<boolean>
Loads the sprite, builds four theme palettes, and calls sheet.indexize(). ORDER MATTERS: 1. Extract unique colors from the sprite PNG. 2. Build the stone (base) palette with those colors. 3. Build fire, ice, void palettes by tinting the base colors. 4. BT.paletteSet(stonePalette) - activate the starting palette. 5. SpriteSheet.load() + indexize() - link pixels to slot numbers.
@returnsTrue when everything is ready.
init
() {
console.log('[PaletteSwapDemo] Initializing...'); // Step 1: Extract sprite colors // We read the sprite PNG ahead of time so we know the exact RGBA values // that need to be registered in each theme palette below. // // The engine's helper (SpriteSheet.loadColorsIntoPalette) writes the colors into // a palette AND returns them as an array. Here we only need the array, so we hand // it a throwaway "scratch" palette as a sink and keep the returned array for later. // Each theme palette gets built from this.baseColors with its own tint applied. const const scratchPalette: PalettescratchPalette =
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) => Palette
Creates a standalone palette instance.
@since1.0.3@paramsize - Palette size. Defaults to 256 colors.@returnsNew mutable palette.
paletteCreate
(256);
this.Demo.baseColors: {}baseColors = await class SpriteSheet
Sprite-sheet wrapper around a loaded image asset. The class keeps the original image available for CPU-side inspection while lazily creating and caching a GPU texture for rendering. When possible, `load()` also pre-decodes the source into an `ImageBitmap` so texture uploads preserve pixel-art alpha and color values more reliably. After calling `indexize()`, the sheet stores palette indices rather than RGBA data. The GPU texture becomes an `r8uint` format uploaded via `writeTexture`. The original RGBA bytes are retained so `reindexize()` can re-convert without reloading the image.
@since0.1.0
SpriteSheet
.
SpriteSheet.loadColorsIntoPalette(url: string, palette: Palette, startSlot: number, options?: {
    sort?: "luminance" | "none";
}): Promise<Color32[]>
Walks a PNG's pixels and registers every unique opaque color into the supplied palette starting at `startSlot`. Pixels with alpha 0 are skipped - they map to the engine's transparent sentinel slot 0 at draw time. Opaque pixels are deduplicated on RGB and stored with alpha forced to 255, matching the lookup performed by `indexize()` so a subsequent `sheet.indexize(palette)` call resolves without throwing on missing colors. By default colors are sorted darkest-first by perceived luminance ( {@link Color32.luminance } ); pass `{ sort: 'none' }` to keep the row-major scan order of the source image. Image loading goes through {@link AssetLoader.loadImage } , so the call shares cache and in-flight deduplication with {@link SpriteSheet.load } . The destination range is validated before any write, so the palette is never left partially mutated: if the collected colors would not fit (`startSlot < 1` or `startSlot + count > palette.size`), the method throws without touching any slot.
@paramurl - Path or URL to the PNG file.@parampalette - Target palette to populate.@paramstartSlot - First palette slot to write into.@paramoptions - Optional configuration.@paramoptions.sort - Color ordering. Defaults to `'luminance'`.@returnsRegistered colors in palette-write order.@throwsError if the image cannot be loaded.@throwsRangeError if the discovered colors do not fit in the palette starting at `startSlot`.
loadColorsIntoPalette
('/sprites/test.png', const scratchPalette: PalettescratchPalette, const COLOR_BASE: 10COLOR_BASE);
this.Demo.colorCount: numbercolorCount = this.Demo.baseColors: {}baseColors.length; console.log(`[PaletteSwapDemo] Found ${this.Demo.colorCount: numbercolorCount} unique sprite colors`); // Steps 2 & 3: Build all four theme palettes // buildTheme() also installs the shared UI kit colors (slots 240-251) into // EVERY palette and stores the returned slot map in this.theme, so the kit // keeps its colors no matter which theme palette is active. this.Demo.themes: {}themes = [ this.Demo.buildTheme(themeName: "stone" | "fire" | "ice" | "void"): import("blit386").Palette
Builds a complete Palette for the given theme name. Every palette has the SAME slot layout: Slots 10..N: Sprite colors for this theme (different RGBA per theme). Slots 30..33: Representative swatch color per theme (identical in all palettes). Slots 40..44: Engine overlay colors (identical in all palettes). Slots 240..251: Shared UI kit theme colors (identical in all palettes). Because the sprite slot NUMBERS (10..N) are the same in every palette, the sprite sheet does not need re-indexization when we swap themes. The sprite's stored indices already point to the right slots - just the colors in those slots differ.
@paramthemeName - Which tint to apply.@returnsReady-to-use Palette object.
buildTheme
('stone'), // Original rock colors.
this.Demo.buildTheme(themeName: "stone" | "fire" | "ice" | "void"): import("blit386").Palette
Builds a complete Palette for the given theme name. Every palette has the SAME slot layout: Slots 10..N: Sprite colors for this theme (different RGBA per theme). Slots 30..33: Representative swatch color per theme (identical in all palettes). Slots 40..44: Engine overlay colors (identical in all palettes). Slots 240..251: Shared UI kit theme colors (identical in all palettes). Because the sprite slot NUMBERS (10..N) are the same in every palette, the sprite sheet does not need re-indexization when we swap themes. The sprite's stored indices already point to the right slots - just the colors in those slots differ.
@paramthemeName - Which tint to apply.@returnsReady-to-use Palette object.
buildTheme
('fire'), // Warm reds and oranges.
this.Demo.buildTheme(themeName: "stone" | "fire" | "ice" | "void"): import("blit386").Palette
Builds a complete Palette for the given theme name. Every palette has the SAME slot layout: Slots 10..N: Sprite colors for this theme (different RGBA per theme). Slots 30..33: Representative swatch color per theme (identical in all palettes). Slots 40..44: Engine overlay colors (identical in all palettes). Slots 240..251: Shared UI kit theme colors (identical in all palettes). Because the sprite slot NUMBERS (10..N) are the same in every palette, the sprite sheet does not need re-indexization when we swap themes. The sprite's stored indices already point to the right slots - just the colors in those slots differ.
@paramthemeName - Which tint to apply.@returnsReady-to-use Palette object.
buildTheme
('ice'), // Cool blues and whites.
this.Demo.buildTheme(themeName: "stone" | "fire" | "ice" | "void"): import("blit386").Palette
Builds a complete Palette for the given theme name. Every palette has the SAME slot layout: Slots 10..N: Sprite colors for this theme (different RGBA per theme). Slots 30..33: Representative swatch color per theme (identical in all palettes). Slots 40..44: Engine overlay colors (identical in all palettes). Slots 240..251: Shared UI kit theme colors (identical in all palettes). Because the sprite slot NUMBERS (10..N) are the same in every palette, the sprite sheet does not need re-indexization when we swap themes. The sprite's stored indices already point to the right slots - just the colors in those slots differ.
@paramthemeName - Which tint to apply.@returnsReady-to-use Palette object.
buildTheme
('void'), // Dark desaturated greens.
]; // Step 4: Activate the starting palette (stone theme) // This must happen BEFORE indexize() so the sprite's pixels are mapped // against the correct starting 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) => 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.themes: {}themes[0]);
// Step 5: Load and indexize the sprite // SpriteSheet.load() fetches the PNG from the public folder. // indexize() scans every pixel and finds its color in the active palette. // After this, every pixel stores a palette slot number instead of an RGBA value. try { this.Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
= await class SpriteSheet
Sprite-sheet wrapper around a loaded image asset. The class keeps the original image available for CPU-side inspection while lazily creating and caching a GPU texture for rendering. When possible, `load()` also pre-decodes the source into an `ImageBitmap` so texture uploads preserve pixel-art alpha and color values more reliably. After calling `indexize()`, the sheet stores palette indices rather than RGBA data. The GPU texture becomes an `r8uint` format uploaded via `writeTexture`. The original RGBA bytes are retained so `reindexize()` can re-convert without reloading the image.
@since0.1.0
SpriteSheet
.SpriteSheet.load(url: string): Promise<SpriteSheet>
Loads a sprite sheet from an image URL. Attempts to create an `ImageBitmap` with explicit alpha and color-space settings for more predictable GPU uploads. If bitmap creation fails, the instance still works and falls back to uploading the `HTMLImageElement`.
@paramurl - Path or URL to the image file.@returnsPromise resolving to the loaded SpriteSheet.
load
('/sprites/test.png');
// Grab a source rectangle that covers the whole sprite sheet. this.Demo.charSprite: Rect2i | null
@type{Rect2i | null}
charSprite
= this.Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
.SpriteSheet.fullRect(): Rect2i
Returns a source rectangle that covers the entire sprite sheet.
@returnsFull-sheet source rectangle.
fullRect
();
// Link pixels to palette slots. this.Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
.SpriteSheet.indexize(palette: Palette): void
Converts the sprite sheet's RGBA pixels to palette indices. Each non-transparent pixel is looked up in the provided palette via exact color matching. Index 0 is always transparent. The resulting indices are stored internally; an `r8uint` GPU texture is created lazily on the next `getTexture()` call. The original RGBA data is retained so `reindexize()` can re-convert after a palette swap without reloading the image.
@parampalette - Active palette used for color-to-index mapping.@throwsIf any opaque pixel's color is not present in the palette.
indexize
(this.Demo.themes: {}themes[0]);
console.log(`[PaletteSwapDemo] Sprite loaded: ${this.Demo.charSprite: Rect2i
@type{Rect2i | null}
charSprite
.Rect2i.width: number
Width in pixels (defaults to 0).
width
}x${this.Demo.charSprite: Rect2i
@type{Rect2i | null}
charSprite
.Rect2i.height: number
Height in pixels (defaults to 0).
height
}px`);
} catch (function (local var) error: unknownerror) { console.error('[PaletteSwapDemo] Failed to load sprite:', function (local var) error: unknownerror); return false; } console.log('[PaletteSwapDemo] Initialization complete!'); return true; } /** * Runs 60 times per second to advance the theme cycling. * When SWAP_PERIOD_TICKS have passed, switch to the next theme palette. */ Demo.update(): void
Runs 60 times per second to advance the theme cycling. When SWAP_PERIOD_TICKS have passed, switch to the next theme palette.
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: 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
;
if (this.Demo.swapTimer: TimerswapTimer.Timer.fireIfElapsed(currentTick?: number): boolean
Returns true once per interval and advances the internal last-fired tick.
@paramcurrentTick - Tick to evaluate against; defaults to engine tick counter.@returnsTrue when at least `intervalTicks` have elapsed since the last fire/reset.
fireIfElapsed
(const tick: numbertick)) {
// Move to the next theme; wrap around after void (index 3). this.Demo.currentTheme: numbercurrentTheme = (this.Demo.currentTheme: numbercurrentTheme + 1) % this.Demo.themes: {}themes.length; // Palette swap! // BT.paletteSet() uploads the new palette to the GPU. // All drawing calls immediately use the new colors. // Because every theme palette keeps the sprite colors at the SAME SLOT NUMBERS // (COLOR_BASE..COLOR_BASE+N-1), the sprite's stored indices are still correct.
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) => 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.themes: {}themes[this.Demo.currentTheme: numbercurrentTheme]);
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
.assignTag: (label?: string) => void
Places a labeled marker on the overlay timing chart at the current tick. Requires `isOverlayTimingChartEnabled: true` in `configure()`. Tags scroll with the chart history and are pruned when they leave the visible window. Empty labels become `"Untitled"`. Chart width resets add an automatic `"Start"` tag.
@since1.1.0@paramlabel - Short event name (for example `'Round start'`).
assignTag
(`Theme: ${this.Demo.themeNames: {}themeNames[this.Demo.currentTheme: numbercurrentTheme]}`);
// BT.spritesRefresh() is needed when the new palette REORGANIZES slots // i.e., the same RGBA colors appear at different slot NUMBERS than before. // In this demo the slot layout is identical across all palettes, so // spritesRefresh() is a no-op here, but we call it to show the pattern.
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
.spritesRefresh: () => void
Re-indexizes all tracked sprite sheets against the current active palette. Only call this after a **palette-layout swap** - when the same colors have moved to different slot positions and existing sprite indices now point to the wrong slots. Each sheet re-runs exact RGBA-to-index matching against the active palette via `SpriteSheet.reindexize()`. If any opaque pixel's original color is missing from the new palette, `reindexize()` throws, and `spritesRefresh()` catches that error and removes the affected sheet from the registry (it will no longer render). **Do not call this after a palette-value swap.** If you changed what color a slot holds (e.g. palette animation, theme tinting), the stored indices are still correct - the fragment shader picks up the new color automatically. Calling `spritesRefresh()` in that case is wasteful at best; at worst, if the original RGBA values are gone from the palette, sheets with missing colors will fail reindexing and be removed from the registry. Typical usage after a layout swap: ```ts BT.paletteSet(newLayoutPalette); BT.spritesRefresh(); // re-map all sheets to the new slot positions ```
@since1.0.3@throwsIf no active palette has been set.
spritesRefresh
();
} } /** * Draws the theme legend, cycling sprite, and code column. * NO Color32 objects appear in draw calls - only palette indices and offsets. */ Demo.render(): void
Draws the theme legend, cycling sprite, and code column. NO Color32 objects appear in draw calls - only palette indices and offsets.
render
() {
// Clear to the shared UI theme background (same slot in all theme palettes).
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) => 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: any
@type{ReturnType<typeof applyTheme> | null}
theme
.bg);
// Draw the three main sections. this.Demo.renderThemeLegend(): void
Draws the four-entry theme legend on the left side of the screen. Each entry shows a color swatch (from a stable static slot) and the theme name. A highlight box shows which theme is currently active. The swatch squares use this demo's own scene slots (30..33), which the UI kit cannot draw, so the legend rows stay hand-rolled - but their text and outline use the shared theme slots so the colors match the rest of the UI.
renderThemeLegend
();
this.Demo.renderCyclingSprite(): void
Draws the large cycling sprite in the center of the screen. The sprite uses offset 0 - it draws from COLOR_BASE..COLOR_BASE+N-1, which contains the current theme's colors in whichever palette is active.
renderCyclingSprite
();
this.Demo.renderCodePanel(): void
Draws a code snippet column on the right side showing how palette swap works. A borderless kit group (no ui.panel call) keeps the original loose-text look: dim lines are code comments, blue "info" lines are the code itself.
renderCodePanel
();
} /** * Draws the four-entry theme legend on the left side of the screen. * Each entry shows a color swatch (from a stable static slot) and the theme name. * A highlight box shows which theme is currently active. * * The swatch squares use this demo's own scene slots (30..33), which the UI kit * cannot draw, so the legend rows stay hand-rolled - but their text and outline * use the shared theme slots so the colors match the rest of the UI. */ Demo.renderThemeLegend(): void
Draws the four-entry theme legend on the left side of the screen. Each entry shows a color swatch (from a stable static slot) and the theme name. A highlight box shows which theme is currently active. The swatch squares use this demo's own scene slots (30..33), which the UI kit cannot draw, so the legend rows stay hand-rolled - but their text and outline use the shared theme slots so the colors match the rest of the UI.
renderThemeLegend
() {
const const startY: 20startY = 20; const const entryH: 20entryH = 20; const const entryW: 70entryW = 70; const const gap: 4gap = 4; for (let let i: numberi = 0; let i: numberi < this.Demo.themes: {}themes.length; let i: numberi++) { const const entryY: numberentryY = const startY: 20startY + let i: numberi * (const entryH: 20entryH + const gap: 4gap); // Highlight box around the active theme, in the kit's "active" green accent. if (let i: numberi === this.Demo.currentTheme: numbercurrentTheme) {
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
.drawRect: (rect: Rect2i, paletteIndex: number) => void
Draws an unfilled rectangle outline.
@since0.1.0@paramrect - Rectangle bounds in display coordinates.@parampaletteIndex - Palette color index.
drawRect
(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
(4, const entryY: numberentryY - 1, const entryW: 70entryW, const entryH: 20entryH + 2), this.Demo.theme: any
@type{ReturnType<typeof applyTheme> | null}
theme
.accent);
} // Color swatch dot: a small filled square using the representative swatch slot. // These slots (30..33) are the SAME in every theme palette, so the dots never change.
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) => void
Draws a filled rectangle.
@since0.1.0@paramrect - Rectangle bounds in display coordinates.@parampaletteIndex - Palette color index.
drawRectFill
(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
(8, const entryY: numberentryY + 4, 12, 12), const SWATCH_STONE: 30SWATCH_STONE + let i: numberi);
// Theme name. systemPrint takes (position, paletteIndex, text). // The active theme's name is bright; the other names are dimmed. const const nameSlot: anynameSlot = let i: numberi === this.Demo.currentTheme: numbercurrentTheme ? this.Demo.theme: any
@type{ReturnType<typeof applyTheme> | null}
theme
.text : this.Demo.theme: any
@type{ReturnType<typeof applyTheme> | null}
theme
.dim;
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
.systemPrint: (pos: Vector2i, paletteIndex: number, text: string) => void
Draws text using the built-in 6x14 system font. The system font covers printable ASCII (characters 32-126). For custom bitmap fonts with proportional glyphs, use {@link BT.printFont } instead.
@since1.0.3@parampos - Text origin in display coordinates.@parampaletteIndex - Palette color index for the text.@paramtext - String to render.
systemPrint
(new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(24, const entryY: numberentryY + 4), const nameSlot: anynameSlot, this.Demo.themeNames: {}themeNames[let i: numberi]);
} // Section caption below the legend, drawn as a borderless kit group. // Passing x and y to ui.begin() pins the group's top-left corner exactly // there, so the caption sits right under the last legend entry. import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { x: numberx: 0, y: numbery: 113 }); import uiui.label('Themes:', { color: stringcolor: 'header' }); import uiui.label('2 s each', { color: stringcolor: 'dim' }); import uiui.end(); } /** * Draws the large cycling sprite in the center of the screen. * The sprite uses offset 0 - it draws from COLOR_BASE..COLOR_BASE+N-1, * which contains the current theme's colors in whichever palette is active. */ Demo.renderCyclingSprite(): void
Draws the large cycling sprite in the center of the screen. The sprite uses offset 0 - it draws from COLOR_BASE..COLOR_BASE+N-1, which contains the current theme's colors in whichever palette is active.
renderCyclingSprite
() {
const const spriteX: 90spriteX = 90; const const spriteY: 30spriteY = 30; // Draw the sprite at its natural size. // Offset 0 means: draw using palette slots starting at the sprite's base index.
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
.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => void
Draws a sprite region from an indexed sprite sheet. Sprite draws are batched internally. Grouping draws from the same {@link SpriteSheet } minimizes batch flushes and reduces GPU state changes. The sprite sheet must have been converted to palette indices via `spriteSheet.indexize(palette)` before the first draw call. Prefer `SpriteSheet.loadIndexed(...)` for one-call setup. **Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1. Index 0 is always transparent and is discarded by the fragment shader. The final palette lookup is `storedIndex + paletteOffset`, so: - `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`. `palette[0]` is never reachable because stored indices start at 1. - `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`, and so on. Use this for palette-swap effects such as team colors or damage flashes. **Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's robust buffer access returns 0 for every component; because the fragment shader forces alpha to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also produces out-of-bounds black pixels.
@since0.1.0@paramspriteSheet - Indexed sprite sheet.@paramsrcRect - Source rectangle within the sprite sheet, in pixels.@paramdestPos - Destination top-left position in display coordinates.@parampaletteOffset - Shift added to every stored pixel index before palette lookup (default 0).@exampleBT.drawSprite(sheet, new Rect2i(0, 0, 16, 16), new Vector2i(10, 10)); BT.drawSprite(sheet, new Rect2i(0, 0, 16, 16), new Vector2i(10, 10), 16); // blue team
drawSprite
(this.Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
, this.Demo.charSprite: Rect2i | null
@type{Rect2i | null}
charSprite
, new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const spriteX: 90spriteX, const spriteY: 30spriteY), 0);
// Captions below the sprite, drawn as a borderless kit group pinned right // under the artwork. The group's inner padding (6 px) shifts its text right // and down a little, so the pin point compensates by starting 6 px to the // left of the sprite and just 1 px below it. import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { x: numberx: const spriteX: 90spriteX - 6, y: numbery: const spriteY: 30spriteY + this.Demo.charSprite: Rect2i | null
@type{Rect2i | null}
charSprite
.Rect2i.height: number
Height in pixels (defaults to 0).
height
+ 1 });
// Which theme the sprite is currently drawn with. import uiui.label(`Theme: ${this.Demo.themeNames: {}themeNames[this.Demo.currentTheme: numbercurrentTheme]}`, { color: stringcolor: 'header' }); // Explain the draw call: BT.drawSprite() above passes palette offset 0, so // the sprite reads its colors straight from slots COLOR_BASE and up. import uiui.label('offset = 0', { color: stringcolor: 'info' }); import uiui.label(`slots ${const COLOR_BASE: 10COLOR_BASE}..${const COLOR_BASE: 10COLOR_BASE + this.Demo.colorCount: numbercolorCount - 1}`, { color: stringcolor: 'info' }); import uiui.end(); } /** * Draws a code snippet column on the right side showing how palette swap works. * A borderless kit group (no ui.panel call) keeps the original loose-text look: * dim lines are code comments, blue "info" lines are the code itself. */ Demo.renderCodePanel(): void
Draws a code snippet column on the right side showing how palette swap works. A borderless kit group (no ui.panel call) keeps the original loose-text look: dim lines are code comments, blue "info" lines are the code itself.
renderCodePanel
() {
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_RIGHT); import uiui.label('How it works:', { color: stringcolor: 'header' }); import uiui.label('// Build palettes', { color: stringcolor: 'dim' }); import uiui.label('stone = clone()', { color: stringcolor: 'info' }); import uiui.label('fire = clone()', { color: stringcolor: 'info' }); import uiui.label('fire.set(10, red)', { color: stringcolor: 'info' }); import uiui.spacer(); import uiui.label('// Swap theme', { color: stringcolor: 'dim' }); import uiui.label('BT.paletteSet(', { color: stringcolor: 'info' }); import uiui.label(' firePalette)', { color: stringcolor: 'info' }); import uiui.spacer(); // spritesRefresh() only matters when the new palette moves colors to // DIFFERENT slot numbers - see the header comment at the top of this file. import uiui.label('// Sync sprites', { color: stringcolor: 'dim' }); import uiui.label('BT.spritesRefresh()', { color: stringcolor: 'info' }); import uiui.spacer(); import uiui.label('// Snapshot:', { color: stringcolor: 'dim' }); import uiui.label('copy = pal.clone()', { color: stringcolor: 'info' }); import uiui.end(); } /** * Builds a complete Palette for the given theme name. * * Every palette has the SAME slot layout: * Slots 10..N: Sprite colors for this theme (different RGBA per theme). * Slots 30..33: Representative swatch color per theme (identical in all palettes). * Slots 40..44: Engine overlay colors (identical in all palettes). * Slots 240..251: Shared UI kit theme colors (identical in all palettes). * * Because the sprite slot NUMBERS (10..N) are the same in every palette, * the sprite sheet does not need re-indexization when we swap themes. * The sprite's stored indices already point to the right slots - just the colors * in those slots differ. * * @param {'stone'|'fire'|'ice'|'void'} themeName - Which tint to apply. * @returns {import('blit386').Palette} Ready-to-use Palette object. */ Demo.buildTheme(themeName: "stone" | "fire" | "ice" | "void"): import("blit386").Palette
Builds a complete Palette for the given theme name. Every palette has the SAME slot layout: Slots 10..N: Sprite colors for this theme (different RGBA per theme). Slots 30..33: Representative swatch color per theme (identical in all palettes). Slots 40..44: Engine overlay colors (identical in all palettes). Slots 240..251: Shared UI kit theme colors (identical in all palettes). Because the sprite slot NUMBERS (10..N) are the same in every palette, the sprite sheet does not need re-indexization when we swap themes. The sprite's stored indices already point to the right slots - just the colors in those slots differ.
@paramthemeName - Which tint to apply.@returnsReady-to-use Palette object.
buildTheme
(themeName: "stone" | "fire" | "ice" | "void"
- Which tint to apply.
@paramthemeName - Which tint to apply.
themeName
) {
const const palette: Palettepalette =
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) => Palette
Creates a standalone palette instance.
@since1.0.3@paramsize - Palette size. Defaults to 256 colors.@returnsNew mutable palette.
paletteCreate
(256);
// Shared UI kit colors (same in every palette) // applyTheme() writes the twelve kit colors into high slots (240-251), far // above this demo's scene slots, and returns a map of slot numbers. // CRITICAL for this demo: BT.paletteSet() replaces the ENTIRE palette, so // if only one theme palette carried the UI colors, all the text and panels // would turn black after the first swap. Installing the same colors at the // same slots in all four palettes keeps the UI rock-steady across swaps. this.Demo.theme: any
@type{ReturnType<typeof applyTheme> | null}
theme
= import applyThemeapplyTheme(const palette: Palettepalette);
// Engine overlay colors (same in every palette) // These must match the slot numbers used in configure().overlayStyle above. const palette: Palettepalette.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: 40C_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
(16, 18, 28)); // Dark navy bar background.
const palette: Palettepalette.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_GOLD: 41C_OVERLAY_GOLD, 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)); // Golden overlay text.
const palette: Palettepalette.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_BLUE: 42C_OVERLAY_BLUE, 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
(100, 155, 210)); // Blue-gray chart bars.
const palette: Palettepalette.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_DIM: 43C_OVERLAY_DIM, 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
(80, 80, 100)); // Dim chart warnings.
const palette: Palettepalette.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_GRAY: 44C_OVERLAY_GRAY, 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
(180, 180, 180)); // Gray chart error bars.
// Sprite colors for this theme // We take the original stone RGBA values from baseColors and apply a tint. // tintColor() below picks the right recipe for the theme name. palette.fillBlock() // writes the tinted color for each base color starting at COLOR_BASE, so every theme // registers its tinted colors at the SAME slot numbers. const palette: Palettepalette.Palette.fillBlock(start: number, source: readonly Color32[], transform: (color: Color32, index: number) => Color32): number
Writes a transformed block of colors into contiguous palette slots. Writes `transform(source[i], i)` into slot `start + i` for every `i` in `[0, source.length)`, delegating each write to {@link set } so it inherits {@link set } 's validation, including the rule that slot 0 must stay transparent. Collapses the common pattern of looping `palette.set(start + i, transform(baseColors[i]))` into one call.
@since1.7.0@paramstart - First palette index to write.@paramsource - Source colors to read from, in order. `source[i]` maps to slot `start + i`.@paramtransform - Called once per source color as `transform(color, i)`; its return value is written to slot `start + i`.@returnsThe next free slot after the written block (`start + source.length`), for chaining further writes.@throwsError if `start` is not a non-negative integer.@throwsError if the block would exceed the palette size.@throwsError if an individual slot write is invalid - see {@link set}.
fillBlock
(const COLOR_BASE: 10COLOR_BASE, this.Demo.baseColors: {}baseColors, (base: Color32base) => this.Demo.tintColor(base: Color32, themeName: "stone" | "fire" | "ice" | "void"): Color32
Applies one theme's tint to a single base color. A "tint" is a simple recipe: nudge the red, green, and blue channels up or down, clamped to the valid 0..255 range so the math never overflows.
@parambase - The original stone color from the sprite.@paramthemeName - Which tint recipe to apply.@returnsThe tinted color for this theme.
tintColor
(base: Color32base, themeName: "stone" | "fire" | "ice" | "void"
- Which tint to apply.
@paramthemeName - Which tint to apply.
themeName
));
// Representative swatch colors (same in every palette) // These are used for the theme legend on the left side. // They do NOT change with the theme so the legend always shows all four options. const palette: Palettepalette.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 SWATCH_STONE: 30SWATCH_STONE, 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
(130, 120, 110)); // Warm gray for stone.
const palette: Palettepalette.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 SWATCH_FIRE: 31SWATCH_FIRE, 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
(220, 80, 20)); // Orange-red for fire.
const palette: Palettepalette.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 SWATCH_ICE: 32SWATCH_ICE, 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
(80, 160, 220)); // Sky blue for ice.
const palette: Palettepalette.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 SWATCH_VOID: 33SWATCH_VOID, 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
(40, 90, 50)); // Dim green for void.
return const palette: Palettepalette; } /** * Applies one theme's tint to a single base color. * A "tint" is a simple recipe: nudge the red, green, and blue channels up or * down, clamped to the valid 0..255 range so the math never overflows. * * @param {Color32} base - The original stone color from the sprite. * @param {'stone'|'fire'|'ice'|'void'} themeName - Which tint recipe to apply. * @returns {Color32} The tinted color for this theme. */ Demo.tintColor(base: Color32, themeName: "stone" | "fire" | "ice" | "void"): Color32
Applies one theme's tint to a single base color. A "tint" is a simple recipe: nudge the red, green, and blue channels up or down, clamped to the valid 0..255 range so the math never overflows.
@parambase - The original stone color from the sprite.@paramthemeName - Which tint recipe to apply.@returnsThe tinted color for this theme.
tintColor
(base: Color32
- The original stone color from the sprite.
@parambase - The original stone color from the sprite.
base
, themeName: "stone" | "fire" | "ice" | "void"
- Which tint recipe to apply.
@paramthemeName - Which tint recipe to apply.
themeName
) {
if (themeName: "stone" | "fire" | "ice" | "void"
- Which tint recipe to apply.
@paramthemeName - Which tint recipe to apply.
themeName
=== 'stone') {
// Original colors - no tint. return base: Color32
- The original stone color from the sprite.
@parambase - The original stone color from the sprite.
base
;
} if (themeName: "fire" | "ice" | "void"
- Which tint recipe to apply.
@paramthemeName - Which tint recipe to apply.
themeName
=== 'fire') {
// Warm: boost red, reduce blue. return 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.min(255, base: Color32
- The original stone color from the sprite.
@parambase - The original stone color from the sprite.
base
.Color32.r: number
Red channel (0-255).
r
+ 70), Math.max(0, base: Color32
- The original stone color from the sprite.
@parambase - The original stone color from the sprite.
base
.Color32.g: number
Green channel (0-255).
g
- 20), Math.max(0, base: Color32
- The original stone color from the sprite.
@parambase - The original stone color from the sprite.
base
.Color32.b: number
Blue channel (0-255).
b
- 90));
} if (themeName: "ice" | "void"
- Which tint recipe to apply.
@paramthemeName - Which tint recipe to apply.
themeName
=== 'ice') {
// Cool: boost blue, reduce red. return 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.max(0, base: Color32
- The original stone color from the sprite.
@parambase - The original stone color from the sprite.
base
.Color32.r: number
Red channel (0-255).
r
- 70), Math.min(255, base: Color32
- The original stone color from the sprite.
@parambase - The original stone color from the sprite.
base
.Color32.g: number
Green channel (0-255).
g
+ 20), Math.min(255, base: Color32
- The original stone color from the sprite.
@parambase - The original stone color from the sprite.
base
.Color32.b: number
Blue channel (0-255).
b
+ 90));
} // void: desaturate and shift toward green. const const luma: anyluma = Math.floor(base: Color32
- The original stone color from the sprite.
@parambase - The original stone color from the sprite.
base
.Color32.luminance: number
Perceived (Rec. 601) luminance of this color, ignoring alpha.
@returnsLuminance value in range 0-255 using 0.299*R + 0.587*G + 0.114*B.
luminance
);
return 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(const luma: anyluma * 0.4), Math.min(255, Math.floor(const luma: anyluma * 0.8 + 20)), Math.floor(const luma: anyluma * 0.4));
} } // Hand the Demo class to BLIT386 to start the demo loop. function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@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 Demo
Demonstrates palette swap: building multiple palettes and switching between them at runtime using BT.paletteSet() and BT.spritesRefresh().
@implementsIBTDemo
Demo
);