Palette
Palette setup, built-in presets, HUD preset, serialization, and palette effects.
The palette is the single color authority for all rendering. Index 0 is always transparent and is never drawn. Set an
active palette with BT.paletteSet() before any draw calls. Valid sizes: 2, 4, 16, 32, 64, 128, 256.
Palette addressing
A palette is a fixed-size table of color slots (positions 0 through size - 1). Docs and APIs use three related
terms; they are not interchangeable.
| Term | Role | Typical APIs |
|---|---|---|
| slot | Prose name for a position in the palette table | palette.set(), palette.get(), applyHUD(), effects |
paletteIndex | Absolute slot number written to the framebuffer | BT.clear, primitives, BT.systemPrint |
paletteOffset | Per-draw shift added to each stored texel index before lookup | BT.drawSprite, BT.printFont |
Slot and paletteIndex mean the same integer: which entry in the active palette a pixel uses. Parameter names use
paletteIndex on draw calls; guides may say "slot" when reserving ranges or editing colors with palette.set().
Absolute index
The draw call picks the slot directly. Slot 0 stays transparent (not drawn).
.(, 6); // every pixel uses palette slot 6 (absolute)
.(, 3, 'Score'); // glyphs use absolute slot 3Per-draw offset
Indexed sprites and bitmap fonts store small indices in the texture (starting at 1; stored 0 is transparent). At
draw time the WebGPU sprite shader computes combined = storedIndex + paletteOffset, then index = min(combined, 255u)
before palette lookup.
.(, , , 0); // stored 1 → palette[1], stored 2 → palette[2]
.(, , , 16); // stored 1 → palette[17], stored 2 → palette[18]Use offsets for team colors, tints, and variants without duplicating art. When combined exceeds 255, lookup uses
palette slot 255 from the clamp, not an inherent error color.
Active palette
| Action | Use |
|---|---|
First activation or swap to a different Palette instance | BT.paletteSet(palette) |
| Edit colors on the current active palette | BT.palette.set(slot, color) - live reference, no second paletteSet() needed |
| Read the active palette | BT.palette (throws if none set) |
BT.palette returns the same object the engine draws with. Mutating slots updates colors on the next frame. Call
BT.paletteSet() again only when replacing the whole palette object (for example a prebuilt day vs. night palette), or
after a layout swap when colors moved to different slot numbers (then also call BT.spritesRefresh()). See
Palette Guide section 6.
Palette setup
Palette Since 1.0.3
BT.paletteCreate Since 1.0.3
BT.paletteSet Since 1.0.3
BT.palette Since 1.0.4
// Create
const = new (256);
// const palette2 = BT.paletteCreate(256); // equivalent BT-namespace shorthand
// Set and get colors (slot = absolute palette index; see Palette addressing above)
.(1, new (255, 0, 0, 255)); // red at absolute slot 1
.(1); // → Color32 (defensive copy)
.(1); // → Color32 (live reference - do not store)
// Activate for rendering
.();
.; // → active Palette; throws if none setBuilt-in presets
Each preset returns a fully populated Palette instance.
.(); // VGA 256-color
.(); // CGA 16-color
.(); // Commodore 64 16-color
.(); // Game Boy 4-shade
.(); // PICO-8 16-color
.(); // NES 64-colorHUD preset
Fills six consecutive UI-purpose slots into an existing palette and registers hud_* name aliases.
.(); // fills slots 1-6 (default startSlot = 1)
.(10); // fills slots 10-15
.();The six slots in order, by alias:
| Alias | Purpose |
|---|---|
hud_white | Foreground / label text |
hud_bg | Panel background |
hud_label | Secondary label |
hud_header | Header / title text |
hud_dim | Dimmed / inactive text |
hud_code | Code / monospace text |
Override individual slots after applyHUD():
.(1);
.(2, new (20, 16, 32)); // override hud_bg
.();Fill a block of slots
Writes a transformed color into one contiguous block of slots, replacing a hand-written for loop of palette.set()
calls. Returns the next free slot so multiple blocks can be chained without recomputing offsets.
// Write a "fire" variant of baseColors starting at slot 20, one call instead of a loop.
const = .(20, , () => new (.(255, . + 80), ., .));
// next is 20 + baseColors.length - chain a second block right after the first.
.(, , () => new (., ., .(255, . + 80)));Named slot aliases
.('player', 3); // alias 'player' → slot 3
.('player'); // → 3 (the slot index)
.('player'); // → Color32 at that slothud_* aliases are registered automatically by applyHUD().
Serialization
// JSON round-trip (preserves colors and named aliases)
const = .();
const = .();
// Raw byte arrays (RGB triplets, no alpha channel)
const = .(); // Uint8Array, 3 bytes per slot
const = .(); // Float32Array, 4 floats per slot (RGBA 0..1)
.(); // zero-allocation variant into existing buffer
const = .(); // auto-detect size
const = .(, 16); // explicit size
// Clone
const = .();
// Search
const = .(); // → index, or -1 if not foundPalette effects
BT.paletteCycle Since 1.0.3
BT.paletteFade Since 1.0.3
BT.paletteFadeExposure Since 1.5.0
BT.paletteFadeRange Since 1.0.3
BT.paletteFlash Since 1.0.3
BT.paletteSwap Since 1.0.3
BT.paletteClearEffects Since 1.0.3
Animated effects run automatically once per fixed update, right after your update(), on the tick clock - so they keep
the same pace at any refresh rate, and BT.renderAt reproduces them exactly. Multiple effects can run simultaneously on
different palette ranges and will not conflict. The public Palette.isDirty getter reflects whether slots changed since
the last GPU upload - effects set this flag; no polling needed.
// Cycle a range of slots (water, fire, plasma)
.(, , );
// speed: steps/second, positive = forward, negative = backward
// Fade entire palette toward a target
.(, );
.(, , 'ease-in-out');
// Fade the whole palette like an iris pull rather than a crossfade
.(, );
.(, , { : 0.2, : 'ease-out' });
// Fade a sub-range only
.(, , , );
.(, , , , 'ease-out');
// Flash all non-zero slots to a color then restore (lightning, damage)
.(., 200); // 200 ms
// Instant swap of two slots
.(, );
// Cancel all active effects (palette stays at its current state)
.();Exposure fade
ExposureFadeOptions Since 1.5.0
BT.paletteFade interpolates encoded color values, which is what a post-production crossfade does: every entry drops by
the same proportion for the whole fade, and the image sags uniformly into a muddy gray. BT.paletteFadeExposure
interpolates each RGB channel in linear light instead, and gives each entry a timing offset derived from its luminance,
so bright entries come up first and hold on longest while dark entries arrive late and crush early. Both fades start and
end in the same place; only the middle differs.
Fading up from black or down to black - the usual case - interpolating in linear light is exactly scaling light up or down, which is what closing an iris does.
highlightLead is the single knob. At 0 every entry runs on one schedule and you get a plain linear-light fade; the
default 0.5 reads as a cinematic iris pull.
Slots past the end of the target palette are left alone, so passing a smaller target scopes the fade to its own slots and leaves the rest of the palette free for another effect.
Prop
Type
// Cinematic fade up from black
.(, 1500);
// Softer differential between highlights and shadows
.(, 1500, { : 0.2 });
// Fade out - the same curve mirrored, highlights linger
.(, 1000, { : 'ease-in-out' });Per-index, not per-pixel
A palette fade acts on color indices, not pixels. A dark object sitting in a bright scene fades on the dark schedule regardless of what surrounds it, because the engine only knows its palette slot - real exposure has no such notion. For a fade up from black or down to black this reads correctly; in a busier scene, check the result before shipping it.
Easing functions
Used by paletteFade, paletteFadeRange, and paletteFadeExposure. Type: EasingFunction. Common values:
| Value | Curve |
|---|---|
'linear' | Constant rate (default when omitted) |
'ease-in' | Quadratic slow start, fast end |
'ease-out' | Quadratic fast start, slow end |
'ease-in-out' | Quadratic slow start and end |
The full curve list (sine, cubic, bounce, and more) and the interpolate helper live in API: Easing.
Timing
Effects advance once per fixed update, right after demo.update(), and never during demo.render(). Your draw calls
therefore see one consistent palette for the whole frame, and the renderer uploads it once at the end of the frame. An
effect's clock starts when it is added, not at tick 0, and BT.ticksReset() pauses effects for one update instead of
running them backwards. A 'start' seek with BT.renderAt clears all effects before it replays init().
Effects that auto-remove (fade, flash) clean up when their duration elapses. paletteCycle runs indefinitely until
paletteClearEffects() is called.
API history
| Symbol | Since | Last changed | Status |
|---|---|---|---|
BT.palette | 1.0.4 | – | stable |
BT.paletteClearEffects | 1.0.3 | – | stable |
BT.paletteCreate | 1.0.3 | – | stable |
BT.paletteCycle | 1.0.3 | 1.8.0 | stable |
BT.paletteFade | 1.0.3 | 1.8.0 | stable |
BT.paletteFadeExposure | 1.5.0 | 1.8.0 | stable |
BT.paletteFadeRange | 1.0.3 | 1.8.0 | stable |
BT.paletteFlash | 1.0.3 | 1.8.0 | stable |
BT.paletteSet | 1.0.3 | – | stable |
BT.paletteSwap | 1.0.3 | – | stable |
ExposureFadeOptions | 1.5.0 | – | stable |
Palette | 1.0.3 | 1.7.0 | stable |
1.8.0
- Changed
BT.paletteCycleAdvances once per fixed update on the tick clock instead of once per rendered frame on wall time. - Changed
BT.paletteFadeAdvances once per fixed update on the tick clock instead of once per rendered frame on wall time. - Changed
BT.paletteFadeExposureAdvances once per fixed update on the tick clock instead of once per rendered frame on wall time. - Changed
BT.paletteFadeRangeAdvances once per fixed update on the tick clock instead of once per rendered frame on wall time. - Changed
BT.paletteFlashAdvances once per fixed update on the tick clock instead of once per rendered frame on wall time.
1.7.0 - Fri Sep 11 2026
- Changed
PaletteAdded the `fillBlock` method, writing a transformed block of colors into contiguous slots via `set()` and returning the next free slot, so consecutive blocks chain.
1.5.0 - Tue Aug 11 2026
- Added
BT.paletteFadeExposure - Added
ExposureFadeOptions
1.0.4 - Sun May 17 2026
- Added
BT.palette
1.0.3 - Sat May 16 2026
- Added
BT.paletteClearEffects - Added
BT.paletteCreate - Added
BT.paletteCycle - Added
BT.paletteFade - Added
BT.paletteFadeRange - Added
BT.paletteFlash - Added
BT.paletteSet - Added
BT.paletteSwap - Added
Palette
See also
API: Core
API: Rendering
API: Assets
Palette Guide
Palette Presets
Testing
Post-Process Effects
Overlay Guide
Last updated on October 18, 2026