BLIT386BLIT386

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.

TermRoleTypical APIs
slotProse name for a position in the palette tablepalette.set(), palette.get(), applyHUD(), effects
paletteIndexAbsolute slot number written to the framebufferBT.clear, primitives, BT.systemPrint
paletteOffsetPer-draw shift added to each stored texel index before lookupBT.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 3

Per-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

ActionUse
First activation or swap to a different Palette instanceBT.paletteSet(palette)
Edit colors on the current active paletteBT.palette.set(slot, color) - live reference, no second paletteSet() needed
Read the active paletteBT.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 set

Built-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-color

HUD 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:

AliasPurpose
hud_whiteForeground / label text
hud_bgPanel background
hud_labelSecondary label
hud_headerHeader / title text
hud_dimDimmed / inactive text
hud_codeCode / 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 slot

hud_* 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 found

Palette 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:

ValueCurve
'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

SymbolSinceLast changedStatus
BT.palette1.0.4–stable
BT.paletteClearEffects1.0.3–stable
BT.paletteCreate1.0.3–stable
BT.paletteCycle1.0.31.8.0stable
BT.paletteFade1.0.31.8.0stable
BT.paletteFadeExposure1.5.01.8.0stable
BT.paletteFadeRange1.0.31.8.0stable
BT.paletteFlash1.0.31.8.0stable
BT.paletteSet1.0.3–stable
BT.paletteSwap1.0.3–stable
ExposureFadeOptions1.5.0–stable
Palette1.0.31.7.0stable

1.8.0

  • ChangedBT.paletteCycleAdvances once per fixed update on the tick clock instead of once per rendered frame on wall time.
  • ChangedBT.paletteFadeAdvances once per fixed update on the tick clock instead of once per rendered frame on wall time.
  • ChangedBT.paletteFadeExposureAdvances once per fixed update on the tick clock instead of once per rendered frame on wall time.
  • ChangedBT.paletteFadeRangeAdvances once per fixed update on the tick clock instead of once per rendered frame on wall time.
  • ChangedBT.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

  • ChangedPaletteAdded 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

  • AddedBT.paletteFadeExposure
  • AddedExposureFadeOptions

1.0.4 - Sun May 17 2026

  • AddedBT.palette

1.0.3 - Sat May 16 2026

  • AddedBT.paletteClearEffects
  • AddedBT.paletteCreate
  • AddedBT.paletteCycle
  • AddedBT.paletteFade
  • AddedBT.paletteFadeRange
  • AddedBT.paletteFlash
  • AddedBT.paletteSet
  • AddedBT.paletteSwap
  • AddedPalette

See also

Last updated on October 18, 2026

On this page