/*
Palette Presets: six built-in color sets you can load instantly.
@description Six built-in color sets, including VGA, CGA, and C64, you can load into the active palette instantly.
Part of the BLIT386 demo series.
Prerequisites:
- Basics: https://demos.blit386.dev/basics
- Primitives: https://demos.blit386.dev/primitives
- Colors: https://demos.blit386.dev/colors
Live version: https://demos.blit386.dev/palette-presets
Guides:
- Palette Presets: https://blit386.dev/docs/guides/palette-presets
- Game Boy preset: https://blit386.dev/docs/guides/palette-presets#game-boy-4-slots
- CGA preset: https://blit386.dev/docs/guides/palette-presets#cga-16-slots
- C64 preset: https://blit386.dev/docs/guides/palette-presets#c64-16-slots
- PICO-8 preset: https://blit386.dev/docs/guides/palette-presets#pico8-16-slots
- NES preset: https://blit386.dev/docs/guides/palette-presets#nes-64-slots
- VGA preset: https://blit386.dev/docs/guides/palette-presets#vga-256-slots
- Palette: https://blit386.dev/docs/guides/palette#create-and-activate-a-palette
- Overlay: https://blit386.dev/docs/guides/overlay#custom-rows
Reference:
- Built-in presets: https://blit386.dev/docs/api/palette#built-in-presets
- Named slots: https://blit386.dev/docs/api/palette#named-slot-aliases
- Overlay: https://blit386.dev/docs/api/overlay#event-tags
---
WHAT YOU WILL SEE
-----------------
A 520 by 390 pixel screen with the engine overlay open, and two UI kit panels stacked in
the space between its top and bottom strips.
The "Built-in presets" panel has one row per preset, in this order: Game Boy, CGA, C64,
PICO-8, NES, VGA. Each row starts with the preset name and its slot count in a left
column, then the preset's slots as small 9 by 12 pixel swatches, all rows starting at the
same x. Rows stop at 32 swatches; the NES and VGA rows end with a dim "+32" or "+224" for
the slots not shown. The first swatch of every row is slot 0, the engine's reserved
transparent slot, drawn as a hollow outline because filling it would draw nothing. The
name of the preset the live view is showing is lit in green.
The "Live view" panel below names the current preset and shows 16 large swatches. Every 2
seconds it moves on to the next preset and copies that preset's first 16 slots into
palette slots 200-215, so the big swatches change color while the drawing code stays
exactly the same. Two dim lines in the panel show the setNamed() and getNamed() calls
from init().
The engine overlay also gets a custom row naming the current live-view preset and its
slot count, and its timing chart gets a tag at every switch.
WHAT YOU WILL LEARN
-------------------
- The six preset factories: Palette.gameboy(), Palette.cga(), Palette.c64(),
Palette.pico8(), Palette.nes(), and Palette.vga(). Each returns a ready-made Palette.
- palette.size tells you how many slots a palette has; palette.get(index) reads one slot.
- Copying colors from one Palette into another with palette.set(), so one active palette
can show samples of six presets at once.
- Naming a slot with palette.setNamed(name, index) and reading it back with
palette.getNamed(name).
- A custom overlay row via overlayRows() and timing-chart tags via BT.assignTag().
WHAT IS A PALETTE PRESET?
-------------------------
In the earlier demos we built a palette by hand, one palette.set() call per color.
BLIT386 also ships six ready-made palettes based on real hardware from video game
history:
- Game Boy 4 slots (1989 Nintendo handheld - shades of green)
- CGA 16 slots (1981 IBM PC graphics card - loud, iconic)
- C64 16 slots (1982 Commodore 64 - earthy, distinctive)
- PICO-8 16 slots (2015 fantasy console - soft, retro feel)
- NES 64 slots (1983 Nintendo console - 56 colors defined, the rest black)
- VGA 256 slots (1987 IBM PC graphics standard - rich range)
Slot 0 of every palette is reserved for transparency, so a preset's colors start at
slot 1. A preset palette gives you an authentic retro look in one call.
You can also NAME slots with setNamed() and getNamed() - like writing a label on a paint
can instead of only numbering it. "live-swatch-0" is easier to remember than 200.
TRY THIS
--------
- Press the backquote key (`) to open the engine overlay. The custom row near the bottom
names the live-view preset, and the timing chart gets a tag each time it switches.
- The overlay's palette grid is turned on with 64 columns. Find slots 200-215 in it: they
change every 2 seconds, while the swatch copies in slots 10-181 never move.
*/
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 Palette = Palette
class Palette
Mutable palette of indexed
{@link
Color32
}
entries.
The palette is the central color authority for all rendering:
- **Index 0 is always transparent.** It is initialized with `Color32.transparent`
and cannot be set to an opaque color. The primitive and sprite shaders discard
any fragment whose palette index resolves to alpha 0.
- **Variable sizes:** valid sizes are `2, 4, 16, 32, 64, 128, 256`. The active size
determines the range for `set()` / `get()` and named-color lookups.
- **Fixed GPU layout:** `toFloat32Array()` always outputs `256 * 4` floats so the
renderer can upload a stable 4 KB uniform block regardless of palette size.
Slots beyond the active size are padded with transparent black.
- **Named aliases:** optional string tags map human-readable names to indices,
e.g. `setNamed('player', 3)`. They carry no runtime cost when unused.
- **Mutable by design:** palette-effect features modify entries in place.
Use `clone()` when a snapshot is needed before modification.Palette, class Rect2iInteger rectangle for pixel-perfect bounds and regions.
Used throughout the engine for sprite regions, display-space bounds, and
zero-allocation geometry helpers. Both convenience getters and allocation-free
`*To()` helpers are provided so callers can choose between readability and
hot-path efficiency.Rect2i, 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 FONT_WFONT_W, import uiui, import UI_ANCHORSUI_ANCHORS } from './shared/ui.js';
/** @typedef {import('blit386').Palette} Palette */
// How many ticks the live view shows each preset. A tick is one fixed update; at 60 ticks
// per second, 120 ticks is 2 seconds.
const const LIVE_SWITCH_TICKS: 120LIVE_SWITCH_TICKS = 120;
// Maximum number of swatches per row, so the 64-slot NES and 256-slot VGA rows do not run
// off the screen.
const const MAX_SWATCHES_PER_ROW: 32MAX_SWATCHES_PER_ROW = 32;
// Where the two kit panels sit. The overlay starts visible and covers the top 64 and the
// bottom 100 pixels of the 390-pixel screen, so the panels stack in the band between:
// Built-in presets on top, Live view below it, both nearly full width.
const const PANEL_X: 6PANEL_X = 6;
const const PANEL_W: 508PANEL_W = 508;
const const PRESETS_PANEL_Y: 69PRESETS_PANEL_Y = 69;
const const PANEL_GAP: 6PANEL_GAP = 6;
// 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).
const const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP = 22;
const const PANEL_BOTTOM: 11PANEL_BOTTOM = 11;
// Inside the presets panel each row is a name column, then the swatches. The text starts
// 10 pixels in from the panel edge (the same inset as the panel title); the swatches all
// start at one shared x, 14 characters further right, so every row lines up.
const const ROW_TEXT_X: numberROW_TEXT_X = const PANEL_X: 6PANEL_X + 10;
const const SWATCH_X: numberSWATCH_X = const ROW_TEXT_X: numberROW_TEXT_X + 14 * import FONT_WFONT_W;
const const ROW_PITCH: 15ROW_PITCH = 15;
// Swatch size in pixels, and the distance from one swatch to the next (a 2-pixel gap).
const const SWATCH_W: 9SWATCH_W = 9;
const const SWATCH_H: 12SWATCH_H = 12;
const const SWATCH_STEP: numberSWATCH_STEP = const SWATCH_W: 9SWATCH_W + 2;
// The presets panel is tall enough for six rows; the last row needs no gap under it.
const const PRESETS_PANEL_H: numberPRESETS_PANEL_H = const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP + 6 * const ROW_PITCH: 15ROW_PITCH - (const ROW_PITCH: 15ROW_PITCH - const SWATCH_H: 12SWATCH_H) + const PANEL_BOTTOM: 11PANEL_BOTTOM;
const const LIVE_PANEL_Y: numberLIVE_PANEL_Y = const PRESETS_PANEL_Y: 69PRESETS_PANEL_Y + const PRESETS_PANEL_H: numberPRESETS_PANEL_H + const PANEL_GAP: 6PANEL_GAP;
// The 16 live-view swatches: 28 pixels wide with a 2-pixel gap, centered in the panel.
const const LIVE_SWATCH_COUNT: 16LIVE_SWATCH_COUNT = 16;
const const LIVE_SWATCH_W: 28LIVE_SWATCH_W = 28;
const const LIVE_SWATCH_H: 20LIVE_SWATCH_H = 20;
const const LIVE_SWATCH_STEP: numberLIVE_SWATCH_STEP = const LIVE_SWATCH_W: 28LIVE_SWATCH_W + 2;
const const LIVE_SWATCH_X: numberLIVE_SWATCH_X = const PANEL_X: 6PANEL_X + (const PANEL_W: 508PANEL_W - (const LIVE_SWATCH_COUNT: 16LIVE_SWATCH_COUNT * const LIVE_SWATCH_STEP: numberLIVE_SWATCH_STEP - 2)) / 2;
// First palette slot of the 16 live-view swatches (slots 200-215).
const const LIVE_SWATCH_SLOT: 200LIVE_SWATCH_SLOT = 200;
// Shared UI theme slots used by configure(). applyTheme() (see init()) writes the twelve
// shared UI colors into palette slots 240-251 - its default start slot, safely above this
// demo's swatch slots (10-181) and live-view slots (200-215). configure() runs BEFORE
// init(), so the overlay styles below have to name those slots as plain numbers instead
// of reading this.theme.
const const THEME_BG: 240THEME_BG = 240; // deep navy screen background
const const THEME_HEADER: 246THEME_HEADER = 246; // warm amber header text
const const THEME_ACCENT: 247THEME_ACCENT = 247; // phosphor green accent
const const THEME_WARM: 248THEME_WARM = 248; // warm orange (warnings)
const const THEME_INFO: 249THEME_INFO = 249; // info blue
// Shows the six built-in palette presets side by side, plus a live view that cycles
// through them and a named palette slot.
class class DemoDemo {
// The main palette: holds the swatch copies, the live-view slots, and the UI theme.
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = null;
// Palette slot map for the shared UI kit theme, filled in init() by applyTheme().
Demo.theme: nulltheme = null;
// The six preset Palette objects, loaded in init().
Demo.presets: {}presets = [];
// Display name for each preset, in the same order as `presets`.
Demo.presetNames: {}presetNames = ['Game Boy', 'CGA', 'C64', 'PICO-8', 'NES', 'VGA'];
// Which preset (0-5) the live view currently shows.
Demo.currentPresetIndex: numbercurrentPresetIndex = 0;
// Tick when the live view last switched presets.
Demo.lastSwitchTick: numberlastSwitchTick = 0;
// First palette slot of each preset's swatch row, filled in init().
Demo.swatchOffsets: {}swatchOffsets = [];
// Reused for every swatch rectangle in render(), so drawing allocates no new Rect2i.
Demo.swatchRect: Rect2iswatchRect = 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);
// Reused every frame for the overlay row, so no new array or object is allocated.
Demo.overlayRowData: {}overlayRowData = [{ leftText: stringleftText: 'Current Game Boy - 4 colors', textPaletteIndex: numbertextPaletteIndex: const THEME_HEADER: 246THEME_HEADER }];
// A wider canvas, the overlay's palette grid (64 columns), and overlay colors. The
// overlay bar, text, and timing chart all borrow shared UI theme slots (240-251,
// written by applyTheme() in init()) so the whole demo uses one color scheme.
Demo.configure(): {
displaySize: Vector2i;
maxCanvasSize: Vector2i;
isOverlayPaletteEnabled: boolean;
isOverlayVisibleAtStart: boolean;
overlayPaletteColumns: number;
overlayStyle: {
barPaletteIndex: number;
textPaletteIndex: number;
gapPaletteIndex: number;
};
isOverlayTimingChartEnabled: boolean;
overlayTimingChartStyle: {
updateBarPaletteIndex: number;
renderBarPaletteIndex: number;
warningPaletteIndex: number;
errorPaletteIndex: number;
tagPaletteIndex: number;
};
}
configure() {
return {
displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(520, 390),
maxCanvasSize: Vector2imaxCanvasSize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(520 * 2, 390 * 2),
isOverlayPaletteEnabled: booleanisOverlayPaletteEnabled: true,
isOverlayVisibleAtStart: booleanisOverlayVisibleAtStart: true,
overlayPaletteColumns: numberoverlayPaletteColumns: 64,
overlayStyle: {
barPaletteIndex: number;
textPaletteIndex: number;
gapPaletteIndex: number;
}
overlayStyle: {
barPaletteIndex: numberbarPaletteIndex: const THEME_BG: 240THEME_BG,
textPaletteIndex: numbertextPaletteIndex: const THEME_HEADER: 246THEME_HEADER,
gapPaletteIndex: numbergapPaletteIndex: const THEME_BG: 240THEME_BG,
},
isOverlayTimingChartEnabled: booleanisOverlayTimingChartEnabled: true,
overlayTimingChartStyle: {
updateBarPaletteIndex: number;
renderBarPaletteIndex: number;
warningPaletteIndex: number;
errorPaletteIndex: number;
tagPaletteIndex: number;
}
overlayTimingChartStyle: {
updateBarPaletteIndex: numberupdateBarPaletteIndex: const THEME_ACCENT: 247THEME_ACCENT,
renderBarPaletteIndex: numberrenderBarPaletteIndex: const THEME_INFO: 249THEME_INFO,
warningPaletteIndex: numberwarningPaletteIndex: const THEME_HEADER: 246THEME_HEADER,
errorPaletteIndex: numbererrorPaletteIndex: const THEME_WARM: 248THEME_WARM,
tagPaletteIndex: numbertagPaletteIndex: const THEME_HEADER: 246THEME_HEADER,
},
};
}
// Loads the six presets, copies their colors into one palette, installs the shared UI
// theme, names a live-view slot, and activates the palette.
async Demo.init(): unknowninit() {
console.log('[PalettePresetsDemo] Initializing...');
// Load all six preset palettes. These are static factory methods that return a
// ready-made Palette object - think of them as pre-sorted boxes of paint.
this.Demo.presets: {}presets = [
class PaletteMutable palette of indexed
{@link
Color32
}
entries.
The palette is the central color authority for all rendering:
- **Index 0 is always transparent.** It is initialized with `Color32.transparent`
and cannot be set to an opaque color. The primitive and sprite shaders discard
any fragment whose palette index resolves to alpha 0.
- **Variable sizes:** valid sizes are `2, 4, 16, 32, 64, 128, 256`. The active size
determines the range for `set()` / `get()` and named-color lookups.
- **Fixed GPU layout:** `toFloat32Array()` always outputs `256 * 4` floats so the
renderer can upload a stable 4 KB uniform block regardless of palette size.
Slots beyond the active size are padded with transparent black.
- **Named aliases:** optional string tags map human-readable names to indices,
e.g. `setNamed('player', 3)`. They carry no runtime cost when unused.
- **Mutable by design:** palette-effect features modify entries in place.
Use `clone()` when a snapshot is needed before modification.Palette.Palette.gameboy(): PaletteCreates a four-shade Game Boy palette.gameboy(), // 4 slots: shades of green
class PaletteMutable palette of indexed
{@link
Color32
}
entries.
The palette is the central color authority for all rendering:
- **Index 0 is always transparent.** It is initialized with `Color32.transparent`
and cannot be set to an opaque color. The primitive and sprite shaders discard
any fragment whose palette index resolves to alpha 0.
- **Variable sizes:** valid sizes are `2, 4, 16, 32, 64, 128, 256`. The active size
determines the range for `set()` / `get()` and named-color lookups.
- **Fixed GPU layout:** `toFloat32Array()` always outputs `256 * 4` floats so the
renderer can upload a stable 4 KB uniform block regardless of palette size.
Slots beyond the active size are padded with transparent black.
- **Named aliases:** optional string tags map human-readable names to indices,
e.g. `setNamed('player', 3)`. They carry no runtime cost when unused.
- **Mutable by design:** palette-effect features modify entries in place.
Use `clone()` when a snapshot is needed before modification.Palette.Palette.cga(): PaletteCreates the classic CGA 16-color palette.cga(), // 16 loud IBM PC colors
class PaletteMutable palette of indexed
{@link
Color32
}
entries.
The palette is the central color authority for all rendering:
- **Index 0 is always transparent.** It is initialized with `Color32.transparent`
and cannot be set to an opaque color. The primitive and sprite shaders discard
any fragment whose palette index resolves to alpha 0.
- **Variable sizes:** valid sizes are `2, 4, 16, 32, 64, 128, 256`. The active size
determines the range for `set()` / `get()` and named-color lookups.
- **Fixed GPU layout:** `toFloat32Array()` always outputs `256 * 4` floats so the
renderer can upload a stable 4 KB uniform block regardless of palette size.
Slots beyond the active size are padded with transparent black.
- **Named aliases:** optional string tags map human-readable names to indices,
e.g. `setNamed('player', 3)`. They carry no runtime cost when unused.
- **Mutable by design:** palette-effect features modify entries in place.
Use `clone()` when a snapshot is needed before modification.Palette.Palette.c64(): PaletteCreates the Commodore 64 16-color palette.c64(), // 16 Commodore 64 colors
class PaletteMutable palette of indexed
{@link
Color32
}
entries.
The palette is the central color authority for all rendering:
- **Index 0 is always transparent.** It is initialized with `Color32.transparent`
and cannot be set to an opaque color. The primitive and sprite shaders discard
any fragment whose palette index resolves to alpha 0.
- **Variable sizes:** valid sizes are `2, 4, 16, 32, 64, 128, 256`. The active size
determines the range for `set()` / `get()` and named-color lookups.
- **Fixed GPU layout:** `toFloat32Array()` always outputs `256 * 4` floats so the
renderer can upload a stable 4 KB uniform block regardless of palette size.
Slots beyond the active size are padded with transparent black.
- **Named aliases:** optional string tags map human-readable names to indices,
e.g. `setNamed('player', 3)`. They carry no runtime cost when unused.
- **Mutable by design:** palette-effect features modify entries in place.
Use `clone()` when a snapshot is needed before modification.Palette.Palette.pico8(): PaletteCreates the PICO-8 16-color palette.pico8(), // 16 soft fantasy console colors
class PaletteMutable palette of indexed
{@link
Color32
}
entries.
The palette is the central color authority for all rendering:
- **Index 0 is always transparent.** It is initialized with `Color32.transparent`
and cannot be set to an opaque color. The primitive and sprite shaders discard
any fragment whose palette index resolves to alpha 0.
- **Variable sizes:** valid sizes are `2, 4, 16, 32, 64, 128, 256`. The active size
determines the range for `set()` / `get()` and named-color lookups.
- **Fixed GPU layout:** `toFloat32Array()` always outputs `256 * 4` floats so the
renderer can upload a stable 4 KB uniform block regardless of palette size.
Slots beyond the active size are padded with transparent black.
- **Named aliases:** optional string tags map human-readable names to indices,
e.g. `setNamed('player', 3)`. They carry no runtime cost when unused.
- **Mutable by design:** palette-effect features modify entries in place.
Use `clone()` when a snapshot is needed before modification.Palette.Palette.nes(): PaletteCreates a 64-slot NES palette.nes(), // 64 slots, 56 of them defined
class PaletteMutable palette of indexed
{@link
Color32
}
entries.
The palette is the central color authority for all rendering:
- **Index 0 is always transparent.** It is initialized with `Color32.transparent`
and cannot be set to an opaque color. The primitive and sprite shaders discard
any fragment whose palette index resolves to alpha 0.
- **Variable sizes:** valid sizes are `2, 4, 16, 32, 64, 128, 256`. The active size
determines the range for `set()` / `get()` and named-color lookups.
- **Fixed GPU layout:** `toFloat32Array()` always outputs `256 * 4` floats so the
renderer can upload a stable 4 KB uniform block regardless of palette size.
Slots beyond the active size are padded with transparent black.
- **Named aliases:** optional string tags map human-readable names to indices,
e.g. `setNamed('player', 3)`. They carry no runtime cost when unused.
- **Mutable by design:** palette-effect features modify entries in place.
Use `clone()` when a snapshot is needed before modification.Palette.Palette.vga(): PaletteCreates the default VGA-style 256-color palette.vga(), // 256 VGA standard colors
];
// Build the main palette. It holds the swatch copies plus the shared UI colors.
// We keep it separate from the preset palettes so the UI is always readable.
this.Demo.palette: Palette | nullpalette = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteCreate: (size?: number) => PaletteCreates a standalone palette instance.paletteCreate(256);
// applyTheme() installs the twelve shared UI colors into slots 240-251 and
// returns a map of where each color landed (this.theme.bg, .text, .dim, ...).
// Every kit panel and label below draws with these colors automatically.
this.Demo.theme: nulltheme = import applyThemeapplyTheme(this.Demo.palette: Palettepalette);
// Slots 10-181 hold the swatch colors for the six static rows. We copy each
// preset's colors into this palette so we can draw swatches without switching the
// active palette. Layout: preset 0 at 10-13 (4 slots), preset 1 at 20-35 (16),
// and so on; the NES and VGA rows are capped at 32 slots each.
const const swatchOffsets: {}swatchOffsets = [10, 20, 40, 60, 80, 150];
for (let let p: numberp = 0; let p: numberp < this.Demo.presets: {}presets.length; let p: numberp++) {
const const preset: anypreset = this.Demo.presets: {}presets[let p: numberp];
const const maxColors: anymaxColors = Math.min(const preset: anypreset.size, const MAX_SWATCHES_PER_ROW: 32MAX_SWATCHES_PER_ROW);
const const offset: anyoffset = const swatchOffsets: {}swatchOffsets[let p: numberp];
for (let let c: numberc = 0; let c: numberc < const maxColors: anymaxColors; let c: numberc++) {
// palette.get(index) returns a copy of the Color32 stored at that slot.
const const color: anycolor = const preset: anypreset.get(let c: numberc);
if (const color: anycolor) {
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const offset: anyoffset + let c: numberc, const color: anycolor);
}
}
}
// Keep the offsets for render().
this.Demo.swatchOffsets: {}swatchOffsets = const swatchOffsets: {}swatchOffsets;
// Slots 200-215 are the live-view swatches. Start them black;
// updateLiveSwatches() fills them with the current preset's colors.
for (let let i: numberi = 0; let i: numberi < const LIVE_SWATCH_COUNT: 16LIVE_SWATCH_COUNT; let i: numberi++) {
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const LIVE_SWATCH_SLOT: 200LIVE_SWATCH_SLOT + let i: numberi, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(0, 0, 0));
}
// setNamed() lets you refer to a slot by a descriptive name instead of a number -
// like writing "live-swatch-0" on a label instead of "slot 200". (applyTheme()
// above did the same for its UI colors: palette.getNamed('ui_bg') answers 240.)
this.Demo.palette: Palettepalette.Palette.setNamed(name: string, index: number): voidAssociates a human-readable name with a palette index.setNamed('live-swatch-0', const LIVE_SWATCH_SLOT: 200LIVE_SWATCH_SLOT);
// Make this palette the one the engine draws with.
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteSet: (palette: Palette) => voidStores the active engine palette.
Use this to swap the **entire palette** (e.g. switch between a day and night
theme). After this call the renderer uploads the new palette uniform on the
next frame.
**Palette-value swap (change what a slot looks like):** mutate the live
{@link
BT.palette
}
in place with `palette.set(slot, newColor)`. The renderer
uploads dirty slots on the next frame; no `paletteSet()` or
{@link
BT.spritesRefresh
}
needed.
**Palette-layout swap (same colors, different slot positions):** build a new
palette with the same colors at new indices, call `paletteSet()`, then call
{@link
BT.spritesRefresh
}
so every sprite sheet re-maps its original RGBA
pixels against the new slot layout.paletteSet(this.Demo.palette: Palettepalette);
// Fill the live view with the first preset.
this.Demo.updateLiveSwatches(): voidupdateLiveSwatches();
console.log('[PalettePresetsDemo] Initialized');
return true;
}
// Advances the live view: every LIVE_SWITCH_TICKS ticks, move on to the next preset.
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 (const tick: numbertick - this.Demo.lastSwitchTick: numberlastSwitchTick >= const LIVE_SWITCH_TICKS: 120LIVE_SWITCH_TICKS) {
// Move to the next preset. `%` is the remainder after division, so the index
// wraps back to 0 after the last preset.
this.Demo.currentPresetIndex: numbercurrentPresetIndex = (this.Demo.currentPresetIndex: numbercurrentPresetIndex + 1) % this.Demo.presets: {}presets.length;
this.Demo.lastSwitchTick: numberlastSwitchTick = 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.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(`Preset: ${this.Demo.presetNames: {}presetNames[this.Demo.currentPresetIndex: numbercurrentPresetIndex]}`);
// Copy the new preset's colors into the live-view slots (200-215).
this.Demo.updateLiveSwatches(): voidupdateLiveSwatches();
}
}
// Draws the title strip, the six swatch rows with captions, and the live-view panel.
// No Color32 appears in a draw call here - only palette slot numbers (and the kit,
// which uses its own theme slots).
Demo.render(): voidrender() {
// Background: the shared theme's deep navy, so every demo looks alike.
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.clear: (paletteIndex: number) => voidSets the frame clear color using a palette index.
The renderer uses this color when clearing the full display at the start
of the next frame.clear(this.Demo.theme: nulltheme.bg);
// One row of swatches per preset.
this.Demo.renderSwatchRows(): voidrenderSwatchRows();
// The live cycling preview.
this.Demo.renderLivePreview(): voidrenderLivePreview();
}
// Names the current live-view preset and its slot count for the engine overlay.
Demo.overlayRows(): {}overlayRows() {
// The overlay can ask for rows before init() has filled the six presets; until
// then we answer with the prepared default row. After init(), update() keeps
// currentPresetIndex in range with %, so no other check is needed.
if (this.Demo.presets: {}presets.length === 0) {
return this.Demo.overlayRowData: {}overlayRowData;
}
const const name: anyname = this.Demo.presetNames: {}presetNames[this.Demo.currentPresetIndex: numbercurrentPresetIndex];
const const size: anysize = this.Demo.presets: {}presets[this.Demo.currentPresetIndex: numbercurrentPresetIndex].size;
this.Demo.overlayRowData: {}overlayRowData[0].leftText = `Current ${const name: anyname} - ${const size: anysize} colors`;
return this.Demo.overlayRowData: {}overlayRowData;
}
// Copies the current preset's first 16 slots into the live-view slots (200-215). Any
// slot past the preset's size turns black. render() shows the new colors on the next
// frame without changing a single draw call.
Demo.updateLiveSwatches(): voidupdateLiveSwatches() {
const const preset: anypreset = this.Demo.presets: {}presets[this.Demo.currentPresetIndex: numbercurrentPresetIndex];
const const maxColors: anymaxColors = Math.min(const preset: anypreset.size, 16);
for (let let i: numberi = 0; let i: numberi < 16; let i: numberi++) {
if (let i: numberi < const maxColors: anymaxColors) {
const const color: anycolor = const preset: anypreset.get(let i: numberi);
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const LIVE_SWATCH_SLOT: 200LIVE_SWATCH_SLOT + let i: numberi, const color: anycolor ?? new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(0, 0, 0));
} else {
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const LIVE_SWATCH_SLOT: 200LIVE_SWATCH_SLOT + let i: numberi, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(0, 0, 0));
}
}
}
// Draws one swatch inside a panel. Slot 0 of every palette is the reserved
// transparent slot, so filling it would draw nothing; it gets a hollow outline in the
// theme's border color instead, so you can see where it is. Both use the kit's
// ui.drawRectFill() / ui.drawRect(): kit panels are sprites, and the engine draws
// every BT.drawRectFill() before any sprite - so a plain one would hide under the
// panel. The kit versions take the same rectangle and slot but draw sprites, which
// land on top in the order we call them.
Demo.renderSwatch(x: any, y: any, width: any, height: any, slot: any, isTransparentSlot: any): voidrenderSwatch(x: anyx, y: anyy, width: anywidth, height: anyheight, slot: anyslot, isTransparentSlot: anyisTransparentSlot) {
this.Demo.swatchRect: Rect2iswatchRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(x: anyx, y: anyy, width: anywidth, height: anyheight);
if (isTransparentSlot: anyisTransparentSlot) {
import uiui.drawRect(this.Demo.swatchRect: Rect2iswatchRect, this.Demo.theme: nulltheme.border);
} else {
import uiui.drawRectFill(this.Demo.swatchRect: Rect2iswatchRect, slot: anyslot);
}
}
// Draws the Built-in presets panel: one row per preset with its name and slot count
// in a left column, then its swatches from the copies made in init(). The row that
// the live view is showing right now has its name lit in the accent color.
Demo.renderSwatchRows(): voidrenderSwatchRows() {
// The panel frame, with an empty body tall enough for six rows. Everything inside
// is drawn after ui.end(), so it lands on top of the panel.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { x: numberx: const PANEL_X: 6PANEL_X, y: numbery: const PRESETS_PANEL_Y: 69PRESETS_PANEL_Y, width: numberwidth: const PANEL_W: 508PANEL_W });
import uiui.panel('Built-in presets - Palette.gameboy(), .cga(), ...');
import uiui.spacer(const PRESETS_PANEL_H: numberPRESETS_PANEL_H - const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP - const PANEL_BOTTOM: 11PANEL_BOTTOM);
import uiui.end();
for (let let p: numberp = 0; let p: numberp < this.Demo.presets: {}presets.length; let p: numberp++) {
const const y: numbery = const PRESETS_PANEL_Y: 69PRESETS_PANEL_Y + const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP + let p: numberp * const ROW_PITCH: 15ROW_PITCH;
const const size: anysize = this.Demo.presets: {}presets[let p: numberp].size;
// Same math as init(): the preset's real slot count, capped at the row limit.
const const count: anycount = Math.min(const size: anysize, const MAX_SWATCHES_PER_ROW: 32MAX_SWATCHES_PER_ROW);
const const offset: anyoffset = this.Demo.swatchOffsets: {}swatchOffsets[let p: numberp];
// Name on the left, slot count right-aligned just before the swatches. Every
// character of the system font is FONT_W (6) pixels wide, so the count is
// text.length * FONT_W wide, and starting it that far left of SWATCH_X - 8
// makes it end exactly there.
//
// Vertical centering: a caption's letters start 1 pixel above the y it is
// given (the kit lifts its text 4 pixels, and capital letters begin 3 rows
// into the 14-pixel font cell). Passing labelY = y + 3 puts the 7-pixel-tall
// capitals on rows y + 2 to y + 8, the middle of the 12-pixel swatch.
const const labelY: numberlabelY = const y: numbery + 3;
const const isLive: booleanisLive = let p: numberp === this.Demo.currentPresetIndex: numbercurrentPresetIndex;
const const sizeText: anysizeText = String(const size: anysize);
import uiui.caption(const ROW_TEXT_X: numberROW_TEXT_X, const labelY: numberlabelY, this.Demo.presetNames: {}presetNames[let p: numberp], { color: stringcolor: const isLive: booleanisLive ? 'accent' : 'text' });
import uiui.caption(const SWATCH_X: numberSWATCH_X - 8 - const sizeText: anysizeText.length * import FONT_WFONT_W, const labelY: numberlabelY, const sizeText: anysizeText, { color: stringcolor: 'dim' });
for (let let c: numberc = 0; let c: numberc < const count: anycount; let c: numberc++) {
this.Demo.renderSwatch(x: any, y: any, width: any, height: any, slot: any, isTransparentSlot: any): voidrenderSwatch(const SWATCH_X: numberSWATCH_X + let c: numberc * const SWATCH_STEP: numberSWATCH_STEP, const y: numbery, const SWATCH_W: 9SWATCH_W, const SWATCH_H: 12SWATCH_H, const offset: anyoffset + let c: numberc, let c: numberc === 0);
}
// The NES and VGA rows stop at 32 swatches; say how many slots are not shown.
if (const size: anysize > const count: anycount) {
import uiui.caption(const SWATCH_X: numberSWATCH_X + const count: anycount * const SWATCH_STEP: numberSWATCH_STEP + 4, const labelY: numberlabelY, `+${const size: anysize - const count: anycount}`, { color: stringcolor: 'dim' });
}
}
}
// Draws the Live view panel: the current preset's name, 16 large swatches from the
// live slots (200-215), and the two named-slot calls from init(). The swatches never
// change their slot numbers - only the colors stored in those slots change.
Demo.renderLivePreview(): voidrenderLivePreview() {
const const name: anyname = this.Demo.presetNames: {}presetNames[this.Demo.currentPresetIndex: numbercurrentPresetIndex];
const const size: anysize = this.Demo.presets: {}presets[this.Demo.currentPresetIndex: numbercurrentPresetIndex].size;
// The kit flows the rows top to bottom: the preset name, then a ui.spacer()
// reserves an empty band for the swatches, then the two code lines. These code
// lines show real calls from init(), not decoration.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { x: numberx: const PANEL_X: 6PANEL_X, y: numbery: const LIVE_PANEL_Y: numberLIVE_PANEL_Y, width: numberwidth: const PANEL_W: 508PANEL_W });
import uiui.panel('Live view - slots 200-215, next preset every 2 seconds');
// Game Boy has only 4 slots, so "first 16" would be wrong for it.
const const shown: stringshown = const size: anysize > const LIVE_SWATCH_COUNT: 16LIVE_SWATCH_COUNT ? `first ${const LIVE_SWATCH_COUNT: 16LIVE_SWATCH_COUNT} of ${const size: anysize}` : `all ${const size: anysize}`;
import uiui.label(`${const name: anyname} - ${const shown: stringshown} slots`, { color: stringcolor: 'accent' });
import uiui.spacer(const LIVE_SWATCH_H: 20LIVE_SWATCH_H + 6);
import uiui.label("palette.setNamed('live-swatch-0', 200)", { color: stringcolor: 'dim' });
import uiui.label("palette.getNamed('live-swatch-0') => 200", { color: stringcolor: 'dim' });
import uiui.end();
// The swatches sit in the band the spacer reserved: below the title and the name
// row (PANEL_CONTENT_TOP + 10), plus 3 pixels of breathing room.
const const swatchY: numberswatchY = const LIVE_PANEL_Y: numberLIVE_PANEL_Y + const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP + 10 + 3;
for (let let i: numberi = 0; let i: numberi < const LIVE_SWATCH_COUNT: 16LIVE_SWATCH_COUNT; let i: numberi++) {
const const x: numberx = const LIVE_SWATCH_X: numberLIVE_SWATCH_X + let i: numberi * const LIVE_SWATCH_STEP: numberLIVE_SWATCH_STEP;
this.Demo.renderSwatch(x: any, y: any, width: any, height: any, slot: any, isTransparentSlot: any): voidrenderSwatch(const x: numberx, const swatchY: numberswatchY, const LIVE_SWATCH_W: 28LIVE_SWATCH_W, const LIVE_SWATCH_H: 20LIVE_SWATCH_H, const LIVE_SWATCH_SLOT: 200LIVE_SWATCH_SLOT + let i: numberi, let i: numberi === 0);
}
}
}
// 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);