// 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.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, 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.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.SpriteSheet, class TimerFixed-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.Timer, class Vector2iInteger 2D vector for pixel-perfect positioning.
Used for points, sizes, directions, and camera offsets throughout the engine.
The API includes both allocation-free `*To()` / `*InPlace()` variants and
convenience methods that return new vectors.Vector2i } from 'blit386';
import { import applyThemeapplyTheme, import uiui, import UI_ANCHORSUI_ANCHORS } from './shared/ui.js';
/** @typedef {import('blit386').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 DemoDemonstrates palette swap: building multiple palettes and switching between them
at runtime using BT.paletteSet() and BT.spritesRefresh().Demo {
// The sprite sheet loaded from test.png.
/** @type {SpriteSheet | null} */
Demo.sheet: SpriteSheet | nullsheet = null;
// The rectangular region of the sprite within the sheet.
/** @type {Rect2i | null} */
Demo.charSprite: Rect2i | nullcharSprite = 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): TimerCreates a timer that fires once per fixed-tick interval.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: anytheme = 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.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.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) => PaletteCreates a standalone palette instance.paletteCreate(256);
this.Demo.baseColors: {}baseColors = await class SpriteSheetSprite-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.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.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").PaletteBuilds 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.buildTheme('stone'), // Original rock colors.
this.Demo.buildTheme(themeName: "stone" | "fire" | "ice" | "void"): import("blit386").PaletteBuilds 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.buildTheme('fire'), // Warm reds and oranges.
this.Demo.buildTheme(themeName: "stone" | "fire" | "ice" | "void"): import("blit386").PaletteBuilds 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.buildTheme('ice'), // Cool blues and whites.
this.Demo.buildTheme(themeName: "stone" | "fire" | "ice" | "void"): import("blit386").PaletteBuilds 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.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) => 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.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 | nullsheet = await class SpriteSheetSprite-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.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`.load('/sprites/test.png');
// Grab a source rectangle that covers the whole sprite sheet.
this.Demo.charSprite: Rect2i | nullcharSprite = this.Demo.sheet: SpriteSheet | nullsheet.SpriteSheet.fullRect(): Rect2iReturns a source rectangle that covers the entire sprite sheet.fullRect();
// Link pixels to palette slots.
this.Demo.sheet: SpriteSheet | nullsheet.SpriteSheet.indexize(palette: Palette): voidConverts 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.indexize(this.Demo.themes: {}themes[0]);
console.log(`[PaletteSwapDemo] Sprite loaded: ${this.Demo.charSprite: Rect2icharSprite.Rect2i.width: numberWidth in pixels (defaults to 0).width}x${this.Demo.charSprite: Rect2icharSprite.Rect2i.height: numberHeight 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(): voidRuns 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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks;
if (this.Demo.swapTimer: TimerswapTimer.Timer.fireIfElapsed(currentTick?: number): booleanReturns true once per interval and advances the internal last-fired tick.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) => 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.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) => voidPlaces 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.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: () => voidRe-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
```spritesRefresh();
}
}
/**
* Draws the theme legend, cycling sprite, and code column.
* NO Color32 objects appear in draw calls - only palette indices and offsets.
*/
Demo.render(): voidDraws 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) => 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: anytheme.bg);
// Draw the three main sections.
this.Demo.renderThemeLegend(): voidDraws 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(): voidDraws 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(): voidDraws 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(): voidDraws 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) => voidDraws an unfilled rectangle outline.drawRect(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(4, const entryY: numberentryY - 1, const entryW: 70entryW, const entryH: 20entryH + 2), this.Demo.theme: anytheme.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) => 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(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: anytheme.text : this.Demo.theme: anytheme.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) => voidDraws 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.systemPrint(new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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(): voidDraws 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) => voidDraws 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.drawSprite(this.Demo.sheet: SpriteSheet | nullsheet, this.Demo.charSprite: Rect2i | nullcharSprite, new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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 | nullcharSprite.Rect2i.height: numberHeight 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(): voidDraws 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").PaletteBuilds 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.buildTheme(themeName: "stone" | "fire" | "ice" | "void"- 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) => PaletteCreates a standalone palette instance.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: anytheme = 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): voidWrites a color into a palette slot.set(const C_OVERLAY_BAR: 40C_OVERLAY_BAR, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(16, 18, 28)); // Dark navy bar background.
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_GOLD: 41C_OVERLAY_GOLD, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(255, 210, 80)); // Golden overlay text.
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_BLUE: 42C_OVERLAY_BLUE, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(100, 155, 210)); // Blue-gray chart bars.
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_DIM: 43C_OVERLAY_DIM, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(80, 80, 100)); // Dim chart warnings.
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_GRAY: 44C_OVERLAY_GRAY, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.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): numberWrites 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.fillBlock(const COLOR_BASE: 10COLOR_BASE, this.Demo.baseColors: {}baseColors, (base: Color32base) => this.Demo.tintColor(base: Color32, themeName: "stone" | "fire" | "ice" | "void"): Color32Applies 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.tintColor(base: Color32base, themeName: "stone" | "fire" | "ice" | "void"- 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): voidWrites a color into a palette slot.set(const SWATCH_STONE: 30SWATCH_STONE, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(130, 120, 110)); // Warm gray for stone.
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const SWATCH_FIRE: 31SWATCH_FIRE, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(220, 80, 20)); // Orange-red for fire.
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const SWATCH_ICE: 32SWATCH_ICE, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(80, 160, 220)); // Sky blue for ice.
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const SWATCH_VOID: 33SWATCH_VOID, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.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"): Color32Applies 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.tintColor(base: Color32- The original stone color from the sprite.base, themeName: "stone" | "fire" | "ice" | "void"- Which tint recipe to apply.themeName) {
if (themeName: "stone" | "fire" | "ice" | "void"- Which tint recipe to apply.themeName === 'stone') {
// Original colors - no tint.
return base: Color32- The original stone color from the sprite.base;
}
if (themeName: "fire" | "ice" | "void"- Which tint recipe to apply.themeName === 'fire') {
// Warm: boost red, reduce blue.
return new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(Math.min(255, base: Color32- The original stone color from the sprite.base.Color32.r: numberRed channel (0-255).r + 70), Math.max(0, base: Color32- The original stone color from the sprite.base.Color32.g: numberGreen channel (0-255).g - 20), Math.max(0, base: Color32- The original stone color from the sprite.base.Color32.b: numberBlue channel (0-255).b - 90));
}
if (themeName: "ice" | "void"- Which tint recipe to apply.themeName === 'ice') {
// Cool: boost blue, reduce red.
return new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(Math.max(0, base: Color32- The original stone color from the sprite.base.Color32.r: numberRed channel (0-255).r - 70), Math.min(255, base: Color32- The original stone color from the sprite.base.Color32.g: numberGreen channel (0-255).g + 20), Math.min(255, base: Color32- The original stone color from the sprite.base.Color32.b: numberBlue 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.base.Color32.luminance: numberPerceived (Rec. 601) luminance of this color, ignoring alpha.luminance);
return new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.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.bootstrap(class DemoDemonstrates palette swap: building multiple palettes and switching between them
at runtime using BT.paletteSet() and BT.spritesRefresh().Demo);