/*
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 demo series.
Prerequisites:
- Basics: https://demos.blit386.dev/basics
- Primitives: https://demos.blit386.dev/primitives
- Sprites: https://demos.blit386.dev/sprites
- Palette Animation: https://demos.blit386.dev/palette-animation
Live version: https://demos.blit386.dev/palette-swap
Guides:
- Palette: https://blit386.dev/docs/guides/palette#layout-swap-vs-value-swap
- Index sprites: https://blit386.dev/docs/guides/palette#index-sprites-preferred-and-manual-flows
- Overlay: https://blit386.dev/docs/guides/overlay#colors-and-hud-palette-slots
Reference:
- Sprites: https://blit386.dev/docs/api/rendering#sprites
- Sprite refresh: https://blit386.dev/docs/api/rendering#refreshing-after-a-palette-layout-swap
- Active palette: https://blit386.dev/docs/api/palette#active-palette
- Fill a block: https://blit386.dev/docs/api/palette#fill-a-block-of-slots
- Overlay: https://blit386.dev/docs/api/overlay#event-tags
---
WHAT YOU WILL SEE
-----------------
Three UI kit panels on a 320x240 screen. Themes, in the top-left corner, lists the four
themes - Stone, Fire, Ice, Void - each with a small color square and its name; the active
one has a green frame around its square and a bright name, and a meter under the list
fills up toward the next swap. Sprite, beside it, shows public/sprites/test.png at 3
times its size, with the current theme, the palette slots it reads, and its palette
offset listed to its right. How it works, across the bottom, shows the three calls the
demo makes, each under a comment saying when it runs.
Every 2 seconds the demo activates the next theme palette. The sprite changes color
instantly; the legend squares, the text, and the engine overlay keep their colors because
every theme palette carries the same values in those slots.
WHAT YOU WILL LEARN
-------------------
- Building several Palette objects with the same slot layout but different colors, and
switching between them with BT.paletteSet(palette).
- SpriteSheet.loadColorsIntoPalette(url, palette, startSlot) to read a PNG's unique
colors before the sheet itself is loaded, and palette.fillBlock(start, colors,
transform) to write a tinted copy of them into a block of slots.
- SpriteSheet.load(url) and sheet.indexize(palette) to turn RGBA pixels into palette
indices.
- Value swap vs. layout swap: when BT.spritesRefresh() is needed and when it is not.
- Color32.luminance and simple channel math for tinting colors.
- A tick-based Timer and timing-chart tags via BT.assignTag().
WHAT IS PALETTE SWAP?
---------------------
In the Palette Animation demo we changed the VALUES in palette slots while keeping the
same Palette object. A palette swap goes further: you build several Palette objects, one
per color theme, and at runtime you switch WHICH one 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.
Sprites store palette indices, not colors. If the new palette keeps every color at the
same slot number and only changes the color values (a "value swap"), the sprites are
already correct. If the new palette moves the same colors to different slot numbers (a
"layout swap"), call BT.spritesRefresh(): the engine re-matches every sprite's original
RGBA pixels against the new palette, the same way indexize() did the first time.
This demo is a value swap: every theme palette keeps the sprite colors at the same slot
numbers, just tinted differently, so BT.spritesRefresh() is not needed. update() calls it
anyway to show the call - see the note there for what actually happens.
TRY THIS
--------
- Press the backquote key (`) to open the engine overlay. The timing chart gets a tag
with the theme name at every swap, and the overlay's own colors (slots 40-44) stay put
because all four palettes hold the same values there.
- Open the browser console: on the first swap the engine reports that spritesRefresh()
could not re-index the sheet. The note in update() explains why.
*/
import { function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>One-liner bootstrap function for BLIT386 demos.
Handles canvas retrieval and engine initialization. Backend selection
(WebGPU or software fallback) is managed internally by BTAPI.
This function provides a streamlined way to start a demo with sensible defaults
while allowing customization through options.bootstrap, const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT, class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32, 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').SpriteSheet} SpriteSheet */
/** @typedef {import('blit386').Rect2i} Rect2i */
// How many ticks to hold each theme before switching. A tick is one fixed update; at 60
// ticks per second, 120 ticks is 2 seconds.
const const SWAP_PERIOD_TICKS: 120SWAP_PERIOD_TICKS = 120;
// First palette slot of the sprite's colors.
const const COLOR_BASE: 10COLOR_BASE = 10;
// One fixed swatch color per theme, for the legend on the left. These slots hold the same
// colors in ALL four theme palettes, so the legend never changes.
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;
// Layout of the three kit panels on the 320x240 screen, 4 pixels from the edges: Themes
// (top left) and Sprite (top right) side by side and equally tall, then How it works
// across the full width below them. The bottom 13 pixels stay clear for the overlay's
// toggle hint.
const const EDGE: 4EDGE = 4;
const const PANEL_GAP: 4PANEL_GAP = 4;
const const TOP_PANELS_Y: 4TOP_PANELS_Y = const EDGE: 4EDGE;
const const THEMES_PANEL_W: 88THEMES_PANEL_W = 88;
const const SPRITE_PANEL_X: numberSPRITE_PANEL_X = const EDGE: 4EDGE + const THEMES_PANEL_W: 88THEMES_PANEL_W + const PANEL_GAP: 4PANEL_GAP;
const const SPRITE_PANEL_W: numberSPRITE_PANEL_W = 320 - const EDGE: 4EDGE - const SPRITE_PANEL_X: numberSPRITE_PANEL_X;
const const CODE_PANEL_W: numberCODE_PANEL_W = 320 - 2 * const EDGE: 4EDGE;
// A kit panel with a title starts its content 22 pixels below its top edge and ends 11
// pixels below the last row (the rim and the title strip). Text inside starts 10 pixels
// in from the panel edge.
const const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP = 22;
const const PANEL_BOTTOM: 11PANEL_BOTTOM = 11;
const const PANEL_TEXT_INSET: 10PANEL_TEXT_INSET = 10;
// The sprite is drawn 3 times its size. Its visible pixels cover x = 4 to 38 and
// y = 7 to 35 of the 44x44 frame, a 35x29 block that becomes 105x87 at 3x.
const const SPRITE_SCALE: 3SPRITE_SCALE = 3;
const const ART_LEFT: 4ART_LEFT = 4;
const const ART_TOP: 7ART_TOP = 7;
const const ART_W: numberART_W = 35 * const SPRITE_SCALE: 3SPRITE_SCALE;
const const ART_H: numberART_H = 29 * const SPRITE_SCALE: 3SPRITE_SCALE;
// Both top panels reserve the same content height (the 87-pixel artwork plus 1 pixel
// above and below), so they end on the same line.
const const TOP_CONTENT_H: numberTOP_CONTENT_H = const ART_H: numberART_H + 2;
const const TOP_PANELS_H: numberTOP_PANELS_H = const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP + const TOP_CONTENT_H: numberTOP_CONTENT_H + const PANEL_BOTTOM: 11PANEL_BOTTOM;
const const CODE_PANEL_Y: numberCODE_PANEL_Y = const TOP_PANELS_Y: 4TOP_PANELS_Y + const TOP_PANELS_H: numberTOP_PANELS_H + const PANEL_GAP: 4PANEL_GAP;
// Legend rows inside the Themes panel: a 10x10 swatch and the name, 14 pixels apart.
const const LEGEND_ROW_H: 14LEGEND_ROW_H = 14;
const const LEGEND_SWATCH: 10LEGEND_SWATCH = 10;
// Engine overlay color slots (the 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
// Builds four theme palettes with the same slot layout and switches between them at
// runtime with BT.paletteSet(), recoloring one sprite without changing a draw call.
class class DemoDemo {
// The sprite sheet loaded from test.png.
/** @type {SpriteSheet | null} */
Demo.sheet: SpriteSheet | nullsheet = null;
// The rectangle covering the whole sprite within the sheet.
/** @type {Rect2i | null} */
Demo.charSprite: Rect2i | nullcharSprite = null;
// How many unique colors the sprite image contains.
Demo.colorCount: numbercolorCount = 0;
// The sprite's original Color32 values, used to build every theme palette.
Demo.baseColors: {}baseColors = [];
// The four theme palettes, switched in update(): 0 stone, 1 fire, 2 ice, 3 void.
Demo.themes: {}themes = [];
// Display name for each theme, shown in the legend.
Demo.themeNames: {}themeNames = ['Stone', 'Fire', 'Ice', 'Void'];
// Index of the 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;
// Reused for every rectangle and sprite position drawn in render(), so drawing
// allocates no new objects. spriteParams draws the sprite 3 times its size.
Demo.tempRect: Rect2itempRect = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(0, 0, 0, 0);
Demo.spritePos: Vector2ispritePos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(0, 0);
Demo.spriteParams: {
scale: number;
}
spriteParams = { scale: numberscale: const SPRITE_SCALE: 3SPRITE_SCALE };
// The timing chart helps compare CPU cost while palettes swap on a timer. Its colors
// come from the fixed overlay slots above.
Demo.configure(): {
isOverlayTimingChartEnabled: boolean;
overlayStyle: {
barPaletteIndex: number;
textPaletteIndex: number;
gapPaletteIndex: number;
};
overlayTimingChartStyle: {
updateBarPaletteIndex: number;
renderBarPaletteIndex: number;
warningPaletteIndex: number;
errorPaletteIndex: number;
tagPaletteIndex: number;
};
}
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 the four theme palettes, and indexizes the sheet. The
// ORDER matters:
// 1. Read the sprite PNG's unique colors.
// 2. Build the stone (base) palette with those colors.
// 3. Build the fire, ice, and 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 true when everything is ready, false if the sprite failed to load.
async Demo.init(): unknowninit() {
console.log('[PaletteSwapDemo] Initializing...');
// Step 1: read the sprite colors. We read the PNG ahead of time so we know the
// exact RGBA values every theme palette has to hold. The engine helper
// SpriteSheet.loadColorsIntoPalette() writes the colors into a palette AND
// returns them as an array. We only need the array, so we hand it a throwaway
// "scratch" palette and keep the returned colors for buildTheme().
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;
... 124 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 and 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 is
// active.
this.Demo.themes: {}themes = [
this.Demo.buildTheme(themeName: any): PalettebuildTheme('stone'), // the original rock colors
this.Demo.buildTheme(themeName: any): PalettebuildTheme('fire'), // warm reds and oranges
this.Demo.buildTheme(themeName: any): PalettebuildTheme('ice'), // cool blues and whites
this.Demo.buildTheme(themeName: any): PalettebuildTheme('void'), // dark, washed-out greens
];
// Step 4: activate the starting palette (stone). This must happen BEFORE
// indexize() so the sprite's pixels are matched against the right palette.
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.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() looks up every pixel's color in the given
// palette, so afterwards each 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, options?: {
tileSize?: number | Vector2i;
}): 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');
// A source rectangle covering the whole 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 once per tick. When SWAP_PERIOD_TICKS have passed, switches to the next theme.
Demo.update(): voidupdate() {
const const tick: numbertick = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.ticks: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks;
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; `%` wraps the index back to 0 after void (index 3).
this.Demo.currentTheme: numbercurrentTheme = (this.Demo.currentTheme: numbercurrentTheme + 1) % this.Demo.themes: {}themes.length;
// The palette swap. BT.paletteSet() uploads the new palette to the GPU, and
// every draw call uses the new colors from now on. Because each theme palette
// keeps the sprite colors at the SAME slot numbers (COLOR_BASE and up), 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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteSet: (palette: Palette) => voidStores the active engine palette.
Use this to swap the **entire palette** (e.g. switch between a day and night
theme). After this call the renderer uploads the new palette uniform on the
next frame.
**Palette-value swap (change what a slot looks like):** mutate the live
{@link
BT.palette
}
in place with `palette.set(slot, newColor)`. The renderer
uploads dirty slots on the next frame; no `paletteSet()` or
{@link
BT.spritesRefresh
}
needed.
**Palette-layout swap (same colors, different slot positions):** build a new
palette with the same colors at new indices, call `paletteSet()`, then call
{@link
BT.spritesRefresh
}
so every sprite sheet re-maps its original RGBA
pixels against the new slot layout.paletteSet(this.Demo.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;
... 124 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 for a LAYOUT swap, when the same colors move to
// different slot numbers; the engine docs say to skip it after a value swap
// like this one. It is called here only to show where it would go. What it
// really does: it re-matches the sprite's original stone colors against the
// new palette, and a tinted palette does not contain them, so the engine logs
// an error to the console and stops tracking this sheet. The sprite itself is
// untouched and keeps drawing correctly.
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.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, the sprite, and the code column. No Color32 appears in a
// draw call - only palette indices.
Demo.render(): voidrender() {
// Clear to the shared UI theme background (the 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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.clear: (paletteIndex: number) => voidSets the frame clear color using a palette index.
The renderer uses this color when clearing the full display at the start
of the next frame.clear(this.Demo.theme: anytheme.bg);
// The three sections.
this.Demo.renderThemeLegend(): voidrenderThemeLegend();
this.Demo.renderCyclingSprite(): voidrenderCyclingSprite();
this.Demo.renderCodePanel(): voidrenderCodePanel();
}
// Draws the Themes panel in the top-left corner: one row per theme with a color
// square and the name, the active one marked, and a meter filling up toward the next
// swap. The squares use this demo's own scene slots (30-33), which hold the same
// colors in every theme palette, so the legend never changes when the theme swaps.
Demo.renderThemeLegend(): voidrenderThemeLegend() {
// The kit flows the rows top to bottom: an empty band for the four legend rows
// (drawn after ui.end(), so they land on top of the panel), a small gap, then the
// countdown. Its rows add up to TOP_CONTENT_H, so this panel ends exactly where
// the Sprite panel beside it ends.
const const rowsH: numberrowsH = this.Demo.themes: {}themes.length * const LEGEND_ROW_H: 14LEGEND_ROW_H;
// remainingTicks() says how long until the timer fires next; dividing by the
// period turns that into a fraction from 1 (just swapped) down to 0, and 1 minus
// that is how far along the wait is - the meter fills up toward the next swap.
const const progress: numberprogress = 1 - this.Demo.swapTimer: TimerswapTimer.Timer.remainingTicks(currentTick?: number): numberReturns ticks remaining until the timer will fire.remainingTicks(const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.ticks: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks) / const SWAP_PERIOD_TICKS: 120SWAP_PERIOD_TICKS;
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { x: numberx: const EDGE: 4EDGE, y: numbery: const TOP_PANELS_Y: 4TOP_PANELS_Y, width: numberwidth: const THEMES_PANEL_W: 88THEMES_PANEL_W });
import uiui.panel('Themes');
import uiui.spacer(const rowsH: numberrowsH);
import uiui.spacer(9);
import uiui.label('Next swap', { color: stringcolor: 'dim' });
import uiui.meter(null, const progress: numberprogress, { width: numberwidth: const THEMES_PANEL_W: 88THEMES_PANEL_W - 2 * const PANEL_TEXT_INSET: 10PANEL_TEXT_INSET });
import uiui.spacer(const TOP_CONTENT_H: numberTOP_CONTENT_H - const rowsH: numberrowsH - 9 - 10 - 13);
import uiui.end();
for (let let i: numberi = 0; let i: numberi < this.Demo.themes: {}themes.length; let i: numberi++) {
const const rowY: numberrowY = const TOP_PANELS_Y: 4TOP_PANELS_Y + const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP + let i: numberi * const LEGEND_ROW_H: 14LEGEND_ROW_H;
const const swatchX: numberswatchX = const EDGE: 4EDGE + const PANEL_TEXT_INSET: 10PANEL_TEXT_INSET;
const const swatchY: numberswatchY = const rowY: numberrowY + 2;
const const isActive: booleanisActive = let i: numberi === this.Demo.currentTheme: numbercurrentTheme;
// The color square, drawn with ui.drawRectFill(): kit panels are sprites, and
// the engine draws every BT.drawRectFill() before any sprite - so a plain one
// would hide under the panel. The active theme also gets a 1-pixel green
// frame around its square, one pixel out.
this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(const swatchX: numberswatchX, const swatchY: numberswatchY, const LEGEND_SWATCH: 10LEGEND_SWATCH, const LEGEND_SWATCH: 10LEGEND_SWATCH);
import uiui.drawRectFill(this.Demo.tempRect: Rect2itempRect, const SWATCH_STONE: 30SWATCH_STONE + let i: numberi);
if (const isActive: booleanisActive) {
this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(const swatchX: numberswatchX - 2, const swatchY: numberswatchY - 2, const LEGEND_SWATCH: 10LEGEND_SWATCH + 4, const LEGEND_SWATCH: 10LEGEND_SWATCH + 4);
import uiui.drawRect(this.Demo.tempRect: Rect2itempRect, this.Demo.theme: anytheme.accent);
}
// The theme name, bright for the active theme and dim for the others. A
// caption's capital letters cover 7 rows starting 1 row above the y it is
// given, so swatchY + 3 centers them on the 10-pixel square.
import uiui.caption(const swatchX: numberswatchX + const LEGEND_SWATCH: 10LEGEND_SWATCH + 6, const swatchY: numberswatchY + 3, this.Demo.themeNames: {}themeNames[let i: numberi], {
color: stringcolor: const isActive: booleanisActive ? 'text' : 'dim',
});
}
}
// Draws the Sprite panel in the top-right corner: the sprite 3 times its size on the
// left, and beside it the active theme, the slots the sprite reads, and its palette
// offset. It is drawn with palette offset 0, so it reads slots COLOR_BASE and up -
// whichever colors the active theme put there.
Demo.renderCyclingSprite(): voidrenderCyclingSprite() {
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { x: numberx: const SPRITE_PANEL_X: numberSPRITE_PANEL_X, y: numbery: const TOP_PANELS_Y: 4TOP_PANELS_Y, width: numberwidth: const SPRITE_PANEL_W: numberSPRITE_PANEL_W });
import uiui.panel('Sprite - one sheet, four looks');
import uiui.spacer(const TOP_CONTENT_H: numberTOP_CONTENT_H);
import uiui.end();
// Place the frame so its visible pixels (not its transparent border) start 10
// pixels in and 1 pixel below the title strip. The params object scales the
// sprite; with no paletteOffset in it, the offset is 0.
const const artX: numberartX = const SPRITE_PANEL_X: numberSPRITE_PANEL_X + const PANEL_TEXT_INSET: 10PANEL_TEXT_INSET;
const const artY: numberartY = const TOP_PANELS_Y: 4TOP_PANELS_Y + const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP + 1;
this.Demo.spritePos: Vector2ispritePos.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(const artX: numberartX - const ART_LEFT: 4ART_LEFT * const SPRITE_SCALE: 3SPRITE_SCALE, const artY: numberartY - const ART_TOP: 7ART_TOP * const SPRITE_SCALE: 3SPRITE_SCALE);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => void (+1 overload)drawSprite(this.Demo.sheet: SpriteSheet | nullsheet, this.Demo.charSprite: Rect2i | nullcharSprite, this.Demo.spritePos: Vector2ispritePos, this.Demo.spriteParams: {
scale: number;
}
spriteParams);
// Three key-value lines right of the artwork: a dim key, then the value 7
// characters further right.
const const keyX: numberkeyX = const artX: numberartX + const ART_W: numberART_W + 12;
const const valueX: numbervalueX = const keyX: numberkeyX + 7 * 6;
// The three rows of capitals span 31 pixels (rows firstY - 1 to firstY + 29), so
// starting 14 above the artwork's middle centers the block on it.
const const firstY: numberfirstY = const artY: numberartY + Math.floor(const ART_H: numberART_H / 2) - 14;
const const slots: stringslots = `${const COLOR_BASE: 10COLOR_BASE}-${const COLOR_BASE: 10COLOR_BASE + this.Demo.colorCount: numbercolorCount - 1}`;
import uiui.caption(const keyX: numberkeyX, const firstY: numberfirstY, 'Theme', { color: stringcolor: 'dim' });
import uiui.caption(const valueX: numbervalueX, const firstY: numberfirstY, this.Demo.themeNames: {}themeNames[this.Demo.currentTheme: numbercurrentTheme], { color: stringcolor: 'header' });
import uiui.caption(const keyX: numberkeyX, const firstY: numberfirstY + 12, 'Slots', { color: stringcolor: 'dim' });
import uiui.caption(const valueX: numbervalueX, const firstY: numberfirstY + 12, const slots: stringslots, { color: stringcolor: 'info' });
import uiui.caption(const keyX: numberkeyX, const firstY: numberfirstY + 24, 'Offset', { color: stringcolor: 'dim' });
import uiui.caption(const valueX: numbervalueX, const firstY: numberfirstY + 24, '0', { color: stringcolor: 'info' });
}
// Draws the How it works panel across the bottom: the three calls this demo really
// makes, each under a dim comment saying when it runs.
Demo.renderCodePanel(): voidrenderCodePanel() {
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { x: numberx: const EDGE: 4EDGE, y: numbery: const CODE_PANEL_Y: numberCODE_PANEL_Y, width: numberwidth: const CODE_PANEL_W: numberCODE_PANEL_W });
import uiui.panel('How it works - a value swap');
import uiui.label('// Each theme: same slots, tinted colors', { color: stringcolor: 'dim' });
import uiui.label('palette.fillBlock(10, colors, tint)', { color: stringcolor: 'info' });
import uiui.label('// Every 2 seconds', { color: stringcolor: 'dim' });
import uiui.label('BT.paletteSet(this.themes[next])', { color: stringcolor: 'info' });
import uiui.label('// Needed only after a layout swap', { color: stringcolor: 'dim' });
import uiui.label('BT.spritesRefresh()', { color: stringcolor: 'info' });
import uiui.end();
}
// Builds a complete Palette for one theme. Every palette has the SAME slot layout:
// - slots 10 and up: the sprite colors for this theme (different RGBA per theme),
// - slots 30-33: one 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 are the same in every palette, the sprite sheet
// does not need re-indexing when the theme swaps - only the colors in those slots
// differ.
// `themeName` is 'stone', 'fire', 'ice', or 'void'.
Demo.buildTheme(themeName: any): PalettebuildTheme(themeName: anythemeName) {
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;
... 124 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 (the 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. This is CRITICAL here: 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. The same
// colors at the same slots in all four palettes keep the UI rock-steady across
// swaps.
this.Demo.theme: anytheme = import applyThemeapplyTheme(const palette: Palettepalette);
// Engine overlay colors (the same in every palette). These must match the slot
// numbers used in configure() 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
// The sprite colors for this theme. We take the original stone RGBA values from
// baseColors and apply a tint; tintColor() below picks the recipe for the theme
// name. palette.fillBlock() writes one tinted color per base color starting at
// COLOR_BASE, so every theme registers its 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: any, themeName: any): anytintColor(base: Color32base, themeName: anythemeName));
// The legend swatch colors (the same in every palette). 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. `base` is the original stone color from the sprite,
// `themeName` is 'stone', 'fire', 'ice', or 'void'.
Demo.tintColor(base: any, themeName: any): anytintColor(base: anybase, themeName: anythemeName) {
if (themeName: anythemeName === 'stone') {
return base: anybase; // original colors - no tint
}
if (themeName: anythemeName === 'fire') {
// Warm: boost red, reduce green a little, reduce blue a lot.
return new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(Math.min(255, base: anybase.r + 70), Math.max(0, base: anybase.g - 20), Math.max(0, base: anybase.b - 90));
}
if (themeName: anythemeName === 'ice') {
// Cool: reduce red, boost green a little, boost blue a lot.
return new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(Math.max(0, base: anybase.r - 70), Math.min(255, base: anybase.g + 20), Math.min(255, base: anybase.b + 90));
}
// Void: desaturate and shift toward green. Color32.luminance is the color's
// perceived brightness as one number.
const const luma: anyluma = Math.floor(base: anybase.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 DemoDemo);