/*

Palette Exposure Fade: two ways to fade the same picture, side by side.
@description The plain palette fade and the camera-style exposure fade side by side, running on one shared palette.

Part of the BLIT386 demo series.

Prerequisites:
  - Basics: https://demos.blit386.dev/basics
  - Sprites: https://demos.blit386.dev/sprites
  - Sprite Effects: https://demos.blit386.dev/sprite-effects
  - Palette Fade: https://demos.blit386.dev/palette-fade

Live version: https://demos.blit386.dev/palette-exposure-fade

Guides:
  - Palette: https://blit386.dev/docs/guides/palette#runtime-palette-effects
  - Palette: https://blit386.dev/docs/guides/palette#index-sprites-preferred-and-manual-flows
  - Overlay: https://blit386.dev/docs/guides/overlay#optional-bands
  - Overlay: https://blit386.dev/docs/guides/overlay#custom-rows

Reference:
  - Palette: https://blit386.dev/docs/api/palette#exposure-fade
  - Assets: https://blit386.dev/docs/api/assets#register-colors-into-the-palette
  - Overlay: https://blit386.dev/docs/api/overlay#bottom-band

---

WHAT YOU WILL SEE
-----------------

A 480x480 display split into two 240-wide halves, each showing the same painting of an
explosion over a canyon (public/sprites/mushroom-cloud.png, 231x240, twelve colors) with
a strip of its twelve color swatches underneath. The left half is labeled "paletteFade:"
and the right half "paletteFadeExposure:". Below both sits a UI kit panel with a
"Highlight lead" slider and a "Restart [R]" button. The engine overlay is open from the
first frame, with its palette grid showing two rows of fifteen slots at the bottom. The
cycle repeats forever:

  1. Fade up from black - 2 seconds.
  2. Hold, fully lit - 1.5 seconds.
  3. Fade down to black - 2 seconds.
  4. Hold, dark - 1 second.

On the way up, the right fireball lights before the left one and the right shadows stay
black longer. On the way down, the right fireball is still glowing after the left one has
gone gray, and the right shadows go dark first.

WHAT YOU WILL LEARN
-------------------

- BT.paletteFadeExposure(target, durationMs, { highlightLead }) and how it differs from
  BT.paletteFade() / BT.paletteFadeRange().
- Scoping two simultaneous fades to disjoint slot ranges: an explicit range for the plain
  fade, and a deliberately small target palette for the exposure fade.
- SpriteSheet.loadColorsIntoPalette() and sheet.indexize() to turn a PNG into palette
  indices, then BT.drawSprite() with a palette offset to draw it from a second slot range.
- BT.paletteClearEffects() to cancel running fades before restarting.
- A tick-driven phase table, a custom overlay row via overlayRows(), and configure()
  options for the overlay palette grid (isOverlayPaletteEnabled, overlayPaletteColumns,
  overlayPaletteRowsVisible, isOverlayVisibleAtStart, overlayStyle).

TWO KINDS OF FADE
-----------------

BT.paletteFade() interpolates the stored (sRGB-encoded) color values. Every color drops by
the same proportion at the same moment, so the whole picture sinks into gray together.
That is a post-production crossfade.

A camera iris does something different: it scales light. Interpolating in linear light,
a picture keeps looking bright for a while and then falls away quickly near the end, and
bright elements such as a fireball hang on far longer than the dark rocks around them.

BT.paletteFadeExposure() models the iris. It interpolates each RGB channel in linear
light, and it gives each palette entry its own schedule offset derived from its luminance:
bright colors leave first and arrive last; dark colors leave last and arrive first.

WHY FADING UP LOOKS DIFFERENT FROM FADING DOWN?
-----------------------------------------------

The two directions are not simple reverses of each other, by design. Every entry's offset
is set by the luminance of its LIT end, and which end counts as lit depends on the
direction of travel:

  - Fading up - timed by where each entry is headed. An entry heading toward white starts
    rising almost at once and arrives early. An entry heading toward near-black waits,
    then catches up quickly.
  - Fading down - timed by where each entry started. An entry that started bright holds
    its brightness for a while before it begins to dim. An entry that started dark begins
    dropping right away.

Same rule both times - the brightest end of the trip gets the head start - pointed at a
different end of the trip. On the way up the shadows wait; on the way down the fireball
waits instead.

WHY IS THE SCREEN SPLIT?
------------------------

Both halves show the same picture with the same colors, start at the same moment, and take
the same two seconds. The only difference is which fade drives them:

  - Left - BT.paletteFadeRange(), the plain fade on an explicit slot range.
  - Right - BT.paletteFadeExposure(), the camera-style fade.

Any difference between the halves therefore comes from the curve alone.

ONE PICTURE, DRAWN TWICE
------------------------

The image is a real PNG painted with exactly twelve colors, and it happens to hold both
extremes this demo needs: a white-hot cloud at the top and near-black rock in the shadows.
Getting it onto the screen takes three steps:

  1. SpriteSheet.loadColorsIntoPalette() scans every pixel of the PNG, collects the
     distinct colors, and writes them into palette slots 1..12, darkest first. It returns
     the same list as an array.
  2. The same twelve colors are copied into slots 16..27 as well, so the palette holds
     the picture's colors twice, in two separate places.
  3. sheet.indexize() rewrites the image: every pixel stops storing a color and starts
     storing a slot number from 1 to 12.

After that, one BT.drawSprite() call draws the image as it is, reading slots 1..12. A
second call draws the very same image with a palette offset of 15, which adds 15 to every
pixel's slot number as it is drawn, so the identical pixels read slots 16..27 instead:
same picture, second set of colors, no second copy of the image in memory. The Sprite
Effects demo introduced this trick: https://demos.blit386.dev/sprite-effects

HOW THE TWO FADES STAY OUT OF EACH OTHER'S WAY
----------------------------------------------

A palette is one list of 256 color slots, and a fade writes into that list. If both fades
wrote into the same slots they would fight, and the last one to run each tick would win.
So each half owns its own slots:

  - Slots 1..12 - the right half's picture (the exposure fade).
  - Slots 16..27 - the left half's picture (the plain fade).
  - Slots 240..251 - the shared UI kit colors (neither fade touches these).

BT.paletteFadeRange(start, end, ...) takes a slot range, so the left half is explicit. For
the right half the demo relies on a documented property of BT.paletteFadeExposure(): it
leaves alone every slot past the end of the target palette it is handed. The exposure
targets are 16 slots long, so the fade can only ever reach slots 1..15 and never disturbs
the left half or the UI.

THE PALETTE GRID
----------------

The engine overlay is open from the first frame with its palette grid switched on, so the
slots themselves can be watched rather than only the pictures. The grid is laid out
fifteen slots to a row with two rows visible: the first row holds the exposure fade's
slots 1..12 and the second row the plain fade's slots 16..27, one group directly above the
other. During a fade the first group's bright slots run ahead of the second group while
its dark slots lag behind - the effect, as raw numbers.

TRY THIS
--------

- Drag the "Highlight lead" slider (0 to 0.95) to change how strongly the exposure fade
  separates bright colors from dark ones. At 0 the right half is a plain linear-light fade
  with every slot on one schedule; higher values are more cinematic. The new value applies
  to the next fade, not the one already running.
- Press R or tap "Restart [R]" to cancel the running fades, cut both pictures to black,
  and start the cycle over with a fade up.
- Press the backquote key (`) or tap the symbol in the bottom-left corner to close the
  engine overlay and watch just the pictures. Its custom row names the current phase.

*/

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.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
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 Color32
Mutable 32-bit RGBA color value with 8-bit channels.
@since0.1.0
Color32
, 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.
@since0.1.0
Rect2i
, 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.
@since0.1.0
SpriteSheet
, class Vector2i
Integer 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.
@since0.1.0
Vector2i
} from 'blit386';
import { import applyThemeapplyTheme, import THEME_DEFAULT_START_SLOTTHEME_DEFAULT_START_SLOT, import THEME_PANEL_OFFSETTHEME_PANEL_OFFSET, import THEME_TEXT_OFFSETTHEME_TEXT_OFFSET, import uiui, import UI_ANCHORSUI_ANCHORS, } from './shared/ui.js'; /** @typedef {import('blit386').Palette} Palette */ // The engine runs this many update ticks every second. const const TICKS_PER_SECOND: 60TICKS_PER_SECOND = 60; // How long each step of the cycle lasts, in ticks. const const FADE_TICKS: numberFADE_TICKS = 2 * const TICKS_PER_SECOND: 60TICKS_PER_SECOND; // 2 seconds const const HOLD_LIT_TICKS: 90HOLD_LIT_TICKS = 90; // 1.5 seconds const const HOLD_DARK_TICKS: 60HOLD_DARK_TICKS = 60; // 1 second // The fade duration again, in milliseconds, because the fade calls take milliseconds // rather than ticks. const const FADE_MS: numberFADE_MS = (const FADE_TICKS: numberFADE_TICKS / const TICKS_PER_SECOND: 60TICKS_PER_SECOND) * 1000; // The cycle as a lookup table: each step says how long it lasts and which step follows. const
const PHASE_TRANSITIONS: {
    'fade-in': {
        duration: number;
        next: string;
    };
    lit: {
        duration: number;
        next: string;
    };
    'fade-out': {
        duration: number;
        next: string;
    };
    dark: {
        duration: number;
        next: string;
    };
}
PHASE_TRANSITIONS
= {
'fade-in': { duration: numberduration: const FADE_TICKS: numberFADE_TICKS, next: stringnext: 'lit' },
lit: {
    duration: number;
    next: string;
}
lit
: { duration: numberduration: const HOLD_LIT_TICKS: 90HOLD_LIT_TICKS, next: stringnext: 'fade-out' },
'fade-out': { duration: numberduration: const FADE_TICKS: numberFADE_TICKS, next: stringnext: 'dark' },
dark: {
    duration: number;
    next: string;
}
dark
: { duration: numberduration: const HOLD_DARK_TICKS: 60HOLD_DARK_TICKS, next: stringnext: 'fade-in' },
}; // The picture both halves draw: a PNG painted with exactly twelve colors. const const SPRITE_URL: "/sprites/mushroom-cloud.png"SPRITE_URL = '/sprites/mushroom-cloud.png'; // The right half's slots. Slot 0 is always transparent, so scene colors start at 1. const const EXP_FIRST_SLOT: 1EXP_FIRST_SLOT = 1; // The left half's slots, just above the exposure target's 16 slots. The exposure fade's // reach stops at slot 15, so this range is out of its way. const const PLAIN_FIRST_SLOT: 16PLAIN_FIRST_SLOT = 16; // The exposure fade's target palette is deliberately small. 16 is the smallest legal // palette size that still holds the twelve scene colors, and its size is what fences the // fade off from every slot above it. const const EXPOSURE_TARGET_SIZE: 16EXPOSURE_TARGET_SIZE = 16; // Readable names for each step of the cycle, shown as a custom row in the engine overlay. const
const PHASE_LABELS: {
    'fade-in': string;
    lit: string;
    'fade-out': string;
    dark: string;
}
PHASE_LABELS
= {
'fade-in': 'Fading up', lit: stringlit: 'Lit', 'fade-out': 'Fading down', dark: stringdark: 'Dark', }; // Screen geometry. The display is 480x480, so each half of the split is 240 wide. The // vertical numbers are chosen around the engine overlay, which is open from the first // frame: it draws a few rows of text across the top and the palette grid across the // bottom, and it draws after the demo, so whatever it covers is lost. Everything this // demo draws lives in the free band between the two. const const HALF_WIDTH: 240HALF_WIDTH = 240; const const TITLE_Y: 44TITLE_Y = 44; const const SCENE_TOP: 62SCENE_TOP = 62; // The picture's own height in pixels - mushroom-cloud.png is 231x240 - and the rows below // it are stacked from this number. init() checks the loaded image against it and warns in // the console if the art is ever swapped for a different size. const const SCENE_HEIGHT: 240SCENE_HEIGHT = 240; // The strip of color swatches under each picture, four pixels below it. const const RAMP_TOP: numberRAMP_TOP = const SCENE_TOP: 62SCENE_TOP + const SCENE_HEIGHT: 240SCENE_HEIGHT + 4; const const RAMP_HEIGHT: 22RAMP_HEIGHT = 22; // Top of the demo's own control panel, just under the swatch strip and clear of the // overlay's palette band. Pinned rather than anchored to the bottom of the screen, which // is where the overlay lives. const const PANEL_Y: numberPANEL_Y = const RAMP_TOP: numberRAMP_TOP + const RAMP_HEIGHT: 22RAMP_HEIGHT + 6; // Copies a list of colors into a palette, starting at `firstSlot`. function function fillColors(palette: any, firstSlot: any, colors: any): voidfillColors(palette: anypalette, firstSlot: anyfirstSlot, colors: anycolors) { for (let let i: numberi = 0; let i: numberi < colors: anycolors.length; let i: numberi++) { palette: anypalette.set(firstSlot: anyfirstSlot + let i: numberi, colors: anycolors[let i: numberi]); } } // Writes plain black into `count` slots starting at `firstSlot`, for the "faded out" // targets. function function fillBlack(palette: any, firstSlot: any, count: any): voidfillBlack(palette: anypalette, firstSlot: anyfirstSlot, count: anycount) { for (let let i: numberi = 0; let i: numberi < count: anycount; let i: numberi++) { palette: anypalette.set(firstSlot: anyfirstSlot + let i: numberi, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(0, 0, 0));
} } // Shows the difference between BT.paletteFadeRange() and BT.paletteFadeExposure() by // running both on the same picture at the same time, on separate palette slots. class class DemoDemo { /** @type {Palette | null} */ Demo.palette: Palette | null
@type{Palette | null}
palette
= null;
// The picture. /** @type {SpriteSheet | null} */ Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
= null;
// The rectangle covering all of the picture. /** @type {Rect2i | null} */ Demo.spriteRect: Rect2i | null
@type{Rect2i | null}
spriteRect
= null;
// The colors found inside the PNG, darkest first. /** @type {Color32[]} */ Demo.sceneColors: {}
@type{Color32[]}
sceneColors
= [];
// How many colors the PNG holds. Demo.colorCount: numbercolorCount = 0; // How far in from the left edge of a half the picture sits, so it ends up centered. Demo.sceneX: numbersceneX = 0; // The four fade destinations, built once in init(). Each fade needs a palette holding // the colors it should arrive at: the exposure fade's lit target. /** @type {Palette | null} */ Demo.expLit: Palette | null
@type{Palette | null}
expLit
= null;
// The exposure fade's dark target. /** @type {Palette | null} */ Demo.expDark: Palette | null
@type{Palette | null}
expDark
= null;
// The plain fade's lit target. /** @type {Palette | null} */ Demo.plainLit: Palette | null
@type{Palette | null}
plainLit
= null;
// The plain fade's dark target. /** @type {Palette | null} */ Demo.plainDark: Palette | null
@type{Palette | null}
plainDark
= null;
// Palette slots of the shared UI colors, filled by applyTheme() in init(). Demo.theme: nulltheme = null; // Where we are in the fade-in / hold / fade-out / hold cycle. Demo.phase: stringphase = 'dark'; // The tick the current step of the cycle started on. Demo.phaseStartTick: numberphaseStartTick = 0; // Whether this step has already fired its fades. Without this the fades would restart // on every tick and never get anywhere. Demo.effectTriggered: booleaneffectTriggered = false; // How strongly the exposure fade separates bright colors from dark ones. 0 makes the // right half a plain linear-light fade; higher is more cinematic. The slider in the // UI panel writes to this. Demo.highlightLead: numberhighlightLead = 0.5; // Reused every frame so the engine overlay row does not allocate a new object. Demo.overlayRowData: {}overlayRowData = [{ leftText: stringleftText: 'Dark' }]; // Opens the engine overlay with its palette grid already switched on. The grid draws // the palette slots as small swatches, so the two fades can be watched moving their // own slots at their own pace instead of inferred from the picture. Slots 1..12 are // the exposure fade and 16..27 the plain fade, so during a fade the first group // visibly runs ahead of the second on the way up and behind it on the way down. The // overlay starts open because that grid is half the point of the demo; the backquote // key (`) or the symbol in the bottom-left corner closes it.
Demo.configure(): {
    displaySize: Vector2i;
    maxCanvasSize: Vector2i;
    isOverlayPaletteEnabled: boolean;
    isOverlayVisibleAtStart: boolean;
    overlayPaletteRowsVisible: number;
    overlayPaletteColumns: number;
    overlayStyle: {
        barPaletteIndex: any;
        textPaletteIndex: any;
        gapPaletteIndex: any;
    };
}
configure
() {
return { // A square screen: two 240-wide pictures side by side, with enough height // left over for the overlay's palette grid underneath them. displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(480, 480),
maxCanvasSize: Vector2imaxCanvasSize: new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(960, 960),
isOverlayPaletteEnabled: booleanisOverlayPaletteEnabled: true, isOverlayVisibleAtStart: booleanisOverlayVisibleAtStart: true, overlayPaletteRowsVisible: numberoverlayPaletteRowsVisible: 2, overlayPaletteColumns: numberoverlayPaletteColumns: 15, // The overlay defaults to drawing itself with slots 1 and 2, which in this // demo are two of the picture's colors and fade to black along with // everything else. So point it at the shared UI theme colors instead, which // neither fade can reach. configure() runs before init(), so the slot numbers // are derived here rather than read from this.theme.
overlayStyle: {
    barPaletteIndex: any;
    textPaletteIndex: any;
    gapPaletteIndex: any;
}
overlayStyle
: {
barPaletteIndex: anybarPaletteIndex: import THEME_DEFAULT_START_SLOTTHEME_DEFAULT_START_SLOT + import THEME_PANEL_OFFSETTHEME_PANEL_OFFSET, textPaletteIndex: anytextPaletteIndex: import THEME_DEFAULT_START_SLOTTHEME_DEFAULT_START_SLOT + import THEME_TEXT_OFFSETTHEME_TEXT_OFFSET, gapPaletteIndex: anygapPaletteIndex: import THEME_DEFAULT_START_SLOTTHEME_DEFAULT_START_SLOT + import THEME_PANEL_OFFSETTHEME_PANEL_OFFSET, }, }; } // Loads the picture, builds the palette and the four fade targets, and starts the // cycle. async Demo.init(): unknowninit() { // The live palette both halves draw from. this.Demo.palette: Palette | null
@type{Palette | null}
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
.paletteCreate: (size?: number) => Palette
Creates a standalone palette instance.
@since1.0.3@paramsize - Palette size. Defaults to 256 colors.@returnsNew mutable palette.
paletteCreate
(256);
// Install the shared UI colors in slots 240..251 before activating the palette. // Neither fade can reach that high, so the panel stays readable throughout. this.Demo.theme: nulltheme = import applyThemeapplyTheme(this.Demo.palette: Palette
@type{Palette | null}
palette
);
// The exposure fade's targets are small on purpose - see the header comment. // Reading the PNG's colors straight into this one serves two purposes: it becomes // the "fully lit" target for the right half, and it is the palette handed to // indexize() below, which is what makes pixel colors become slots 1..12 rather // than any other run of numbers. this.Demo.expLit: Palette | null
@type{Palette | null}
expLit
=
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) => Palette
Creates a standalone palette instance.
@since1.0.3@paramsize - Palette size. Defaults to 256 colors.@returnsNew mutable palette.
paletteCreate
(const EXPOSURE_TARGET_SIZE: 16EXPOSURE_TARGET_SIZE);
try { this.Demo.sceneColors: {}
@type{Color32[]}
sceneColors
= await 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.
@since0.1.0
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.
@paramurl - Path or URL to the PNG file.@parampalette - Target palette to populate.@paramstartSlot - First palette slot to write into.@paramoptions - Optional configuration.@paramoptions.sort - Color ordering. Defaults to `'luminance'`.@returnsRegistered colors in palette-write order.@throwsError if the image cannot be loaded.@throwsRangeError if the discovered colors do not fit in the palette starting at `startSlot`.
loadColorsIntoPalette
(const SPRITE_URL: "/sprites/mushroom-cloud.png"SPRITE_URL, this.Demo.expLit: Palette
@type{Palette | null}
expLit
, const EXP_FIRST_SLOT: 1EXP_FIRST_SLOT);
} catch (function (local var) error: unknownerror) { console.error('[PaletteExposureFadeDemo] Failed to read sprite colors:', function (local var) error: unknownerror); return false; } this.Demo.colorCount: numbercolorCount = this.Demo.sceneColors: {}
@type{Color32[]}
sceneColors
.length;
console.log(`[PaletteExposureFadeDemo] Found ${this.Demo.colorCount: numbercolorCount} unique colors in ${const SPRITE_URL: "/sprites/mushroom-cloud.png"SPRITE_URL}`); // Slot 0 is reserved for transparency, so the 16-slot exposure target has room // for 15 colors at most. Any more and the picture's colors would spill into the // left half's slots and the two fades would fight over them. const const maxColors: numbermaxColors = const EXPOSURE_TARGET_SIZE: 16EXPOSURE_TARGET_SIZE - const EXP_FIRST_SLOT: 1EXP_FIRST_SLOT; if (this.Demo.colorCount: numbercolorCount > const maxColors: numbermaxColors) { console.error( `[PaletteExposureFadeDemo] ${const SPRITE_URL: "/sprites/mushroom-cloud.png"SPRITE_URL} uses ${this.Demo.colorCount: numbercolorCount} colors, but only ${const maxColors: numbermaxColors} fit.`, ); return false; } // The same colors again, as the left half's "fully lit" target. The plain fade // takes an explicit slot range, so its targets are full size. this.Demo.plainLit: Palette | null
@type{Palette | null}
plainLit
=
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) => Palette
Creates a standalone palette instance.
@since1.0.3@paramsize - Palette size. Defaults to 256 colors.@returnsNew mutable palette.
paletteCreate
(256);
function fillColors(palette: any, firstSlot: any, colors: any): voidfillColors(this.Demo.plainLit: Palette
@type{Palette | null}
plainLit
, const PLAIN_FIRST_SLOT: 16PLAIN_FIRST_SLOT, this.Demo.sceneColors: {}
@type{Color32[]}
sceneColors
);
// The two "faded out" targets: the same runs of slots, but black. this.Demo.expDark: Palette | null
@type{Palette | null}
expDark
=
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) => Palette
Creates a standalone palette instance.
@since1.0.3@paramsize - Palette size. Defaults to 256 colors.@returnsNew mutable palette.
paletteCreate
(const EXPOSURE_TARGET_SIZE: 16EXPOSURE_TARGET_SIZE);
function fillBlack(palette: any, firstSlot: any, count: any): voidfillBlack(this.Demo.expDark: Palette
@type{Palette | null}
expDark
, const EXP_FIRST_SLOT: 1EXP_FIRST_SLOT, this.Demo.colorCount: numbercolorCount);
this.Demo.plainDark: Palette | null
@type{Palette | null}
plainDark
=
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) => Palette
Creates a standalone palette instance.
@since1.0.3@paramsize - Palette size. Defaults to 256 colors.@returnsNew mutable palette.
paletteCreate
(256);
function fillBlack(palette: any, firstSlot: any, count: any): voidfillBlack(this.Demo.plainDark: Palette
@type{Palette | null}
plainDark
, const PLAIN_FIRST_SLOT: 16PLAIN_FIRST_SLOT, this.Demo.colorCount: numbercolorCount);
// Now the image itself. This second read costs nothing extra: the engine's asset // loader caches the decoded PNG, so it is the same image the color scan walked. try { this.Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
= await 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.
@since0.1.0
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`.
@changed1.8.0 Added `options.tileSize`.@paramurl - Path or URL to the image file.@paramoptions - Optional configuration.@paramoptions.tileSize - Tile grid cell size (see {@link SpriteSheet.tileSize}).@returnsPromise resolving to the loaded SpriteSheet.@throwsRangeError if `options.tileSize` is not a positive integer on both axes.
load
(const SPRITE_URL: "/sprites/mushroom-cloud.png"SPRITE_URL);
} catch (function (local var) error: unknownerror) { console.error('[PaletteExposureFadeDemo] Failed to load sprite:', function (local var) error: unknownerror); return false; } // A source rectangle covering the whole image, and the gap that centers it inside // its 240-wide half of the screen. this.Demo.spriteRect: Rect2i | null
@type{Rect2i | null}
spriteRect
= this.Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
.SpriteSheet.fullRect(): Rect2i
Returns a source rectangle that covers the entire sprite sheet.
@returnsFull-sheet source rectangle.
fullRect
();
this.Demo.sceneX: numbersceneX = Math.floor((const HALF_WIDTH: 240HALF_WIDTH - this.Demo.spriteRect: Rect2i
@type{Rect2i | null}
spriteRect
.Rect2i.width: number
Width in pixels (defaults to 0).
width
) / 2);
if (this.Demo.spriteRect: Rect2i
@type{Rect2i | null}
spriteRect
.Rect2i.height: number
Height in pixels (defaults to 0).
height
!== const SCENE_HEIGHT: 240SCENE_HEIGHT) {
console.warn( `[PaletteExposureFadeDemo] ${const SPRITE_URL: "/sprites/mushroom-cloud.png"SPRITE_URL} is ${this.Demo.spriteRect: Rect2i
@type{Rect2i | null}
spriteRect
.Rect2i.height: number
Height in pixels (defaults to 0).
height
}px tall, ` +
`but the layout expects ${const SCENE_HEIGHT: 240SCENE_HEIGHT}px. Update SCENE_HEIGHT.`, ); } // Turn the picture's pixels into slot numbers. Every pixel is looked up in // expLit, where the colors sit at slots 1..12, so that is what each pixel stores // from here on. this.Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
.SpriteSheet.indexize(palette: Palette): void
Converts 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.
@parampalette - Active palette used for color-to-index mapping.@throwsIf any opaque pixel's color is not present in the palette.
indexize
(this.Demo.expLit: Palette
@type{Palette | null}
expLit
);
// Start the picture black, so the first thing a viewer sees is a fade up. this.Demo.resetPictureToBlack(): voidresetPictureToBlack();
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) => void
Stores 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.
@since1.0.3@parampalette - Palette to make active.
paletteSet
(this.Demo.palette: Palette
@type{Palette | null}
palette
);
return true; } // Runs the cycle: fires both fades at the start of a fade step, then waits. Demo.update(): voidupdate() { // Lets the UI kit latch key presses, taps, and slider drags. import uiui.tick(); 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: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
;
this.Demo.triggerPhaseEffect(): voidtriggerPhaseEffect(); this.Demo.advancePhaseIfExpired(elapsed: any, tick: any): voidadvancePhaseIfExpired(const tick: numbertick - this.Demo.phaseStartTick: numberphaseStartTick, const tick: numbertick); } // Draws the picture twice and the UI panel on top. Demo.render(): voidrender() { // A dark backdrop so the two halves read as separate pictures.
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) => void
Sets 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.
@since0.1.0@parampaletteIndex - Palette index for the full-screen clear pass.
clear
(this.Demo.theme: nulltheme.shadow);
// Left half: the plain fade. Right half: the exposure fade. The same sprite both // times - only the run of palette slots it reads differs. this.Demo.renderScene(originX: any, firstSlot: any): voidrenderScene(0, const PLAIN_FIRST_SLOT: 16PLAIN_FIRST_SLOT); this.Demo.renderScene(originX: any, firstSlot: any): voidrenderScene(const HALF_WIDTH: 240HALF_WIDTH, const EXP_FIRST_SLOT: 1EXP_FIRST_SLOT); // Name each half, using UI colors the fades never touch.
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
.systemPrint: (pos: Vector2i, paletteIndex: number, text: string) => void
Draws text using the built-in 6x14 system font. The system font covers printable ASCII (characters 32-126). For custom bitmap fonts with proportional glyphs, use {@link BT.printFont } instead.
@since1.0.3@parampos - Text origin in display coordinates.@parampaletteIndex - Palette color index for the text.@paramtext - String to render.
systemPrint
(new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(7, const TITLE_Y: 44TITLE_Y), this.Demo.theme: nulltheme.dim, 'paletteFade:');
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
.systemPrint: (pos: Vector2i, paletteIndex: number, text: string) => void
Draws text using the built-in 6x14 system font. The system font covers printable ASCII (characters 32-126). For custom bitmap fonts with proportional glyphs, use {@link BT.printFont } instead.
@since1.0.3@parampos - Text origin in display coordinates.@parampaletteIndex - Palette color index for the text.@paramtext - String to render.
systemPrint
(new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const HALF_WIDTH: 240HALF_WIDTH + 7, const TITLE_Y: 44TITLE_Y), this.Demo.theme: nulltheme.header, 'paletteFadeExposure:');
// Pinned to PANEL_Y rather than anchored to the bottom of the screen: the // overlay's palette grid owns the bottom, and it draws after this. import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { y: numbery: const PANEL_Y: numberPANEL_Y }); import uiui.panel('Exposure Fade'); // Dragging this changes the next fade, not the one already running - a fade // captures its settings the moment it starts. this.Demo.highlightLead: numberhighlightLead = import uiui.slider('Highlight lead', this.Demo.highlightLead: numberhighlightLead, { min: numbermin: 0, max: numbermax: 0.95, width: numberwidth: 456 }); // A tap target as well as a key, so the demo works on a phone. if (import uiui.button('Restart [R]', { key: stringkey: 'KeyR' })) { this.Demo.restart(): voidrestart(); } import uiui.end(); } // Supplies one custom overlay row naming the current cycle step. Demo.overlayRows(): {}overlayRows() { this.Demo.overlayRowData: {}overlayRowData[0].leftText = this.Demo.getPhaseLabel(): anygetPhaseLabel(); return this.Demo.overlayRowData: {}overlayRowData; } // A readable name for the step of the cycle we are in. Demo.getPhaseLabel(): anygetPhaseLabel() { return
const PHASE_LABELS: {
    'fade-in': string;
    lit: string;
    'fade-out': string;
    dark: string;
}
PHASE_LABELS
[this.Demo.phase: stringphase];
} // Fires this step's two fades, once per step. Demo.triggerPhaseEffect(): voidtriggerPhaseEffect() { // Already fired for this step, or this step has no fade at all. if (this.Demo.effectTriggered: booleaneffectTriggered) { return; } if (this.Demo.phase: stringphase === 'fade-in') { this.Demo.startFades(plainTarget: any, exposureTarget: any): voidstartFades(this.Demo.plainLit: Palette | null
@type{Palette | null}
plainLit
, this.Demo.expLit: Palette | null
@type{Palette | null}
expLit
);
} else if (this.Demo.phase: stringphase === 'fade-out') { this.Demo.startFades(plainTarget: any, exposureTarget: any): voidstartFades(this.Demo.plainDark: Palette | null
@type{Palette | null}
plainDark
, this.Demo.expDark: Palette | null
@type{Palette | null}
expDark
);
} } // Starts both fades on the same tick with the same duration. `plainTarget` is where // the left half's colors should end up, `exposureTarget` where the right half's // should. Demo.startFades(plainTarget: any, exposureTarget: any): voidstartFades(plainTarget: anyplainTarget, exposureTarget: anyexposureTarget) { // Left half: the plain fade, limited to the slots the left picture uses.
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
.paletteFadeRange: (start: number, end: number, target: Palette, durationMs: number, easing?: EasingFunction) => void
Fades only a subset of palette indices toward a target over time. Same as {@link BT.paletteFade } but restricted to the range `[start, end]`. Indices outside the range are left untouched.
@since1.0.3@changed1.8.0 Advances once per fixed update on the tick clock instead of once per rendered frame on wall time.@paramstart - First palette index to fade (inclusive).@paramend - Last palette index to fade (inclusive).@paramtarget - Target palette to fade toward.@paramdurationMs - Fade duration in milliseconds.@parameasing - Easing curve. Defaults to `'linear'`.
paletteFadeRange
(const PLAIN_FIRST_SLOT: 16PLAIN_FIRST_SLOT, const PLAIN_FIRST_SLOT: 16PLAIN_FIRST_SLOT + this.Demo.colorCount: numbercolorCount - 1, plainTarget: anyplainTarget, const FADE_MS: numberFADE_MS);
// Right half: the exposure fade. It only reaches slots 1..15 because the target // palette handed to it is 16 slots long.
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
.paletteFadeExposure: (target: Palette, durationMs: number, options?: ExposureFadeOptions) => void
Fades all palette entries toward a target the way an iris pull does. {@link BT.paletteFade } interpolates encoded color values, which is a post-production crossfade: every entry drops by the same proportion for the whole fade and the image sags uniformly into gray. This one interpolates each RGB channel in linear light instead, and offsets each entry's schedule by its luminance, so bright entries come up first and hold on longest while dark entries arrive late and crush early. The fade still lands exactly on the target at completion. Fading up from black or down to black, which is the usual case, that interpolation is exactly scaling light the way an iris does. `highlightLead` is the knob: `0` is a plain linear-light fade with every entry on one schedule, higher values push the highlights further ahead. Defaults to `0.5`. This is a per-index effect, not a per-pixel one - a dark object in a bright scene fades on the dark schedule regardless of what surrounds it, because the engine only knows its palette slot. Slots past the end of `target` are left alone, so passing a smaller target palette scopes the fade to its own slots. Common patterns: - Cinematic fade up from black: `BT.paletteFadeExposure(gamePalette, 1500)` - Subtle version: `BT.paletteFadeExposure(gamePalette, 1500, { highlightLead: 0.2 })` - Fade out: `BT.paletteFadeExposure(blackPalette, 1000)`
@since1.5.0@changed1.8.0 Advances once per fixed update on the tick clock instead of once per rendered frame on wall time.@paramtarget - Target palette to fade toward.@paramdurationMs - Fade duration in milliseconds.@paramoptions - Highlight lead and easing curve.
paletteFadeExposure
(exposureTarget: anyexposureTarget, const FADE_MS: numberFADE_MS, { ExposureFadeOptions.highlightLead?: number | undefined
How far ahead of the schedule a fully lit entry runs, in range 0-1. `0` is a plain linear-light fade - every entry on the same schedule. Higher values push bright entries further ahead on the way up and further behind on the way down. Defaults to `0.5`. Values outside the range are clamped; anything at or above the cap is held just below `1` so the schedule stays divisible.
highlightLead
: this.Demo.highlightLead: numberhighlightLead });
this.Demo.effectTriggered: booleaneffectTriggered = true; } // Moves to the next step of the cycle once the current one has run long enough. // `elapsed` is ticks since this step started, `tick` the current tick. Demo.advancePhaseIfExpired(elapsed: any, tick: any): voidadvancePhaseIfExpired(elapsed: anyelapsed, tick: anytick) { const const current: anycurrent =
const PHASE_TRANSITIONS: {
    'fade-in': {
        duration: number;
        next: string;
    };
    lit: {
        duration: number;
        next: string;
    };
    'fade-out': {
        duration: number;
        next: string;
    };
    dark: {
        duration: number;
        next: string;
    };
}
PHASE_TRANSITIONS
[this.Demo.phase: stringphase];
if (elapsed: anyelapsed >= const current: anycurrent.duration) { this.Demo.phase: stringphase = const current: anycurrent.next; this.Demo.phaseStartTick: numberphaseStartTick = tick: anytick; this.Demo.effectTriggered: booleaneffectTriggered = false; } } // Cancels anything running and starts the cycle over from black. Demo.restart(): voidrestart() { // Effects leave the palette wherever they stopped, so paint black by hand.
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
.paletteClearEffects: () => void
Cancels all running palette effects immediately. The palette stays at whatever state it was in when canceled.
@since1.0.3
paletteClearEffects
();
this.Demo.resetPictureToBlack(): voidresetPictureToBlack(); this.Demo.phase: stringphase = 'fade-in'; this.Demo.phaseStartTick: numberphaseStartTick =
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: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
;
this.Demo.effectTriggered: booleaneffectTriggered = false; } // Paints both halves of the picture black, undoing whatever a fade left behind. Demo.resetPictureToBlack(): voidresetPictureToBlack() { function fillBlack(palette: any, firstSlot: any, count: any): voidfillBlack(this.Demo.palette: Palette | null
@type{Palette | null}
palette
, const EXP_FIRST_SLOT: 1EXP_FIRST_SLOT, this.Demo.colorCount: numbercolorCount);
function fillBlack(palette: any, firstSlot: any, count: any): voidfillBlack(this.Demo.palette: Palette | null
@type{Palette | null}
palette
, const PLAIN_FIRST_SLOT: 16PLAIN_FIRST_SLOT, this.Demo.colorCount: numbercolorCount);
} // Draws one copy of the picture, plus the swatch strip under it. `originX` is the // left edge of this half of the screen, `firstSlot` the slot holding the picture's // first (darkest) color. Demo.renderScene(originX: any, firstSlot: any): voidrenderScene(originX: anyoriginX, firstSlot: anyfirstSlot) { // Every pixel of the image stores a slot number counted from EXP_FIRST_SLOT. // drawSprite() adds this offset to each of them as it draws, which is how the one // image reads two different runs of slots: 0 leaves it on slots 1..12, and 15 // shifts it up onto slots 16..27. const const paletteOffset: numberpaletteOffset = firstSlot: anyfirstSlot - const EXP_FIRST_SLOT: 1EXP_FIRST_SLOT;
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 | null
@type{SpriteSheet | null}
sheet
, this.Demo.spriteRect: Rect2i | null
@type{Rect2i | null}
spriteRect
, new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(originX: anyoriginX + this.Demo.sceneX: numbersceneX, const SCENE_TOP: 62SCENE_TOP), const paletteOffset: numberpaletteOffset);
this.Demo.renderRamp(originX: any, firstSlot: any): voidrenderRamp(originX: anyoriginX, firstSlot: anyfirstSlot); } // Draws the strip of the picture's colors under one copy of it. The colors arrive // from the loader sorted darkest first, so the strip reads as a plain climb in // brightness from left to right. Seeing those steps side by side makes the ordering // of a fade easy to spot: the plain fade dims all eleven gaps by the same share at // once, while the exposure fade lets the right-hand swatches lead and the left-hand // ones lag. Demo.renderRamp(originX: any, firstSlot: any): voidrenderRamp(originX: anyoriginX, firstSlot: anyfirstSlot) { // Share the width evenly between the swatches, then center the whole strip. const const stepWidth: anystepWidth = Math.floor((const HALF_WIDTH: 240HALF_WIDTH - 8) / this.Demo.colorCount: numbercolorCount); const const stripWidth: numberstripWidth = const stepWidth: anystepWidth * this.Demo.colorCount: numbercolorCount; const const left: anyleft = originX: anyoriginX + Math.floor((const HALF_WIDTH: 240HALF_WIDTH - const stripWidth: numberstripWidth) / 2); for (let let i: numberi = 0; let i: numberi < this.Demo.colorCount: numbercolorCount; let i: numberi++) {
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
.drawRectFill: (rect: Rect2i, paletteIndex: number) => void
Draws a filled rectangle.
@since0.1.0@paramrect - Rectangle bounds in display coordinates.@parampaletteIndex - Palette color index.
drawRectFill
(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(const left: anyleft + let i: numberi * const stepWidth: anystepWidth, const RAMP_TOP: numberRAMP_TOP, const stepWidth: anystepWidth, const RAMP_HEIGHT: 22RAMP_HEIGHT), firstSlot: anyfirstSlot + let i: numberi);
} } } 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.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
(class DemoDemo);