// 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 series.
//
// Prerequisites:
//   Basics             https://demos.blit386.dev/basics
//   Sprites            https://demos.blit386.dev/sprites
//   Palette Presets    https://demos.blit386.dev/palette-presets
//   Palette Fade       https://demos.blit386.dev/palette-fade
//     (guides: https://blit386.dev/docs/guides/palette-presets,
//      https://blit386.dev/docs/guides/palette#runtime-palette-effects)
//
// Live version: https://demos.blit386.dev/palette-exposure-fade
//
// TWO KINDS OF FADE
//
// In the Palette Fade demo we used BT.paletteFade(). It mixes the numbers a color
// is stored as. Every color gets dimmer by the same share at the same moment, so
// the whole picture sinks into gray together. That is what film editors do AFTER
// a movie is shot, on a computer.
//
// A real camera does something different. It has a hole called an iris, and
// closing it lets in less LIGHT. The picture keeps looking bright for a while,
// then falls away quickly near the end. Bright things like a fireball hang on far
// longer than the dark rocks around it, which go black almost at once.
//
// BT.paletteFadeExposure() copies the camera. It dims light instead of stored
// numbers, and it gives each color its own start time based on how bright that
// color is. 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, and that is on
// purpose. Every color's start time is set by how bright its LIT end is - but
// which end counts as "lit" depends on which way the fade is going:
//
//   Fading up   - timed by where each color is headed. A color heading toward
//                 white starts rising almost at once and arrives early. A color
//                 heading toward near-black waits, then rushes to catch up.
//   Fading down - timed by where each color started. A color that started bright
//                 keeps its brightness for a while before it begins to dim. A
//                 color that started dark begins dropping right away.
//
// Same rule both times - "the brightest end of the trip gets the head start" -
// just pointed at a different end of the trip. That is why, on the way up, the
// shadows are the ones that wait; on the way down, the fireball is the one that
// waits instead.
//
// WHY THE SCREEN IS SPLIT
//
// Both halves show the SAME picture with the SAME colors, start at the SAME
// moment, and take the SAME two seconds. The only thing that differs is which
// fade drives them:
//
//   Left  - BT.paletteFadeRange(), the plain "mix the numbers" fade
//   Right - BT.paletteFadeExposure(), the camera-style fade
//
// So anything you see differing between the halves comes from the curve alone.
//
// ONE PICTURE, DRAWN TWICE
//
// The picture is a real image file: public/sprites/mushroom-cloud.png, a 231x240
// painting of an explosion lighting up a canyon. It is drawn with exactly twelve
// different colors, and it happens to hold both extremes this demo needs - a
// blazing white-hot cloud at the top and near-black rock in the shadows.
//
// Getting it onto the screen takes three steps:
//
//   1. SpriteSheet.loadColorsIntoPalette() walks every pixel of the PNG, collects
//      the colors it finds, and writes them into palette slots 1..12, darkest
//      first. It hands the same list back to us as an array.
//   2. We copy that same list of twelve colors into slots 16..27 as well, so the
//      palette now holds the picture's colors twice over, in two separate places.
//   3. sheet.indexize() rewrites the image itself: 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, and no
// second copy of the image in memory. We met that trick in the Sprite Effects
// demo: https://demos.blit386.dev/sprite-effects
//
// HOW THE TWO FADES STAY OUT OF EACH OTHER'S WAY
//
// A palette is one long list of 256 color slots, and a fade writes into that
// list. If both fades wrote into the same slots they would fight over them, and
// the last one to run each frame 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 panel colors (neither fade touches these)
//
// BT.paletteFadeRange(start, end, ...) already takes a slot range, so the left
// half is easy. For the right half we use a trick the engine supports on
// purpose: BT.paletteFadeExposure() leaves alone any slot past the END of the
// target palette you hand it. Our exposure target is only 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 you can watch the slots themselves rather than only the pictures. Each
// small square is one of the 256 slots, laid out fifteen to a row. The first row
// holds the exposure fade's slots 1..12 and the second row holds the plain fade's
// 16..27, one group directly above the other, so during a fade you can see the
// first group's bright slots run ahead of the second group while its dark slots
// lag behind - the whole point of the effect, as raw numbers.
//
// Press ~ (or tap the symbol in the bottom-left corner) to close the overlay and
// watch just the pictures.
//
// WHAT YOU WILL SEE:
//   An explosion over a canyon, drawn twice. 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 die first.
//   Drag the "Highlight lead" slider to change how strong that difference is.
//   At 0 the right half behaves like a plain fade in light; higher is more
//   cinematic.

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;
    ... 106 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').IBTDemo} IBTDemo */ /** @typedef {import('blit386').HardwareSettings} HardwareSettings */ /** @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, counted 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, but in milliseconds, because the fade calls want // 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 simple map: each step says how long it lasts and what comes next. 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 own reach stops at slot 15, so this 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 our 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 in the engine overlay above the FPS bar. 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 here: 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 therefore 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 complains 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`. * * @param {Palette} palette - Palette to fill. * @param {number} firstSlot - Slot that receives the first color of the list. * @param {readonly Color32[]} colors - Colors to write, in order. */ function function fillColors(palette: Palette, firstSlot: number, colors: readonly Color32[]): void
Copies a list of colors into a palette, starting at `firstSlot`.
@parampalette - Palette to fill.@paramfirstSlot - Slot that receives the first color of the list.@paramcolors - Colors to write, in order.
fillColors
(palette: Palette
- Palette to fill.
@parampalette - Palette to fill.
palette
, firstSlot: number
- Slot that receives the first color of the list.
@paramfirstSlot - Slot that receives the first color of the list.
firstSlot
, colors: {}
- Colors to write, in order.
@paramcolors - Colors to write, in order.
colors
) {
for (let let i: numberi = 0; let i: numberi < colors: {}
- Colors to write, in order.
@paramcolors - Colors to write, in order.
colors
.length; let i: numberi++) {
palette: Palette
- Palette to fill.
@parampalette - Palette to fill.
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(firstSlot: number
- Slot that receives the first color of the list.
@paramfirstSlot - Slot that receives the first color of the list.
firstSlot
+ let i: numberi, colors: {}
- Colors to write, in order.
@paramcolors - Colors to write, in order.
colors
[let i: numberi]);
} } /** * Writes plain black into a run of slots, for the "faded out" target. * * @param {Palette} palette - Palette to fill. * @param {number} firstSlot - First slot of the run. * @param {number} count - How many slots to blacken. */ function function fillBlack(palette: Palette, firstSlot: number, count: number): void
Writes plain black into a run of slots, for the "faded out" target.
@parampalette - Palette to fill.@paramfirstSlot - First slot of the run.@paramcount - How many slots to blacken.
fillBlack
(palette: Palette
- Palette to fill.
@parampalette - Palette to fill.
palette
, firstSlot: number
- First slot of the run.
@paramfirstSlot - First slot of the run.
firstSlot
, count: number
- How many slots to blacken.
@paramcount - How many slots to blacken.
count
) {
for (let let i: numberi = 0; let i: numberi < count: number
- How many slots to blacken.
@paramcount - How many slots to blacken.
count
; let i: numberi++) {
palette: Palette
- Palette to fill.
@parampalette - Palette to fill.
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(firstSlot: number
- First slot of the run.
@paramfirstSlot - First slot of the run.
firstSlot
+ 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.paletteFade and BT.paletteFadeExposure by * running both on the same picture at the same time, on separate palette slots. * * @implements {IBTDemo} */ class class Demo
Shows the difference between BT.paletteFade and BT.paletteFadeExposure by running both on the same picture at the same time, on separate palette slots.
@implementsIBTDemo
Demo
{
/** @type {Palette | null} */ Demo.palette: Palette | null
@type{Palette | null}
palette
= null;
// The picture, and the rectangle covering all of it. /** @type {SpriteSheet | null} */ Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
= null;
/** @type {Rect2i | null} */ Demo.spriteRect: Rect2i | null
@type{Rect2i | null}
spriteRect
= null;
// The colors found inside the PNG, darkest first, and how many there were. /** @type {Color32[]} */ Demo.sceneColors: {}
@type{Color32[]}
sceneColors
= [];
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. /** @type {Palette | null} */ Demo.expLit: Palette | null
@type{Palette | null}
expLit
= null;
/** @type {Palette | null} */ Demo.expDark: Palette | null
@type{Palette | null}
expDark
= null;
/** @type {Palette | null} */ Demo.plainLit: Palette | null
@type{Palette | null}
plainLit
= null;
/** @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 single frame and never get anywhere. Demo.effectTriggered: booleaneffectTriggered = false; // How strongly the exposure fade separates bright colors from dark ones. // 0 makes the right half behave like a plain fade in light; 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 all 256 palette slots as small swatches, so you can watch the * two fades move their own slots at their own pace instead of inferring it 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 here because that grid is half the point of the demo. * Press ~ (or tap the symbol in the bottom-left corner) to close it and watch * the pictures on their own. * * @returns {Partial<HardwareSettings>} */ Demo.configure(): Partial<HardwareSettings>
Opens the engine overlay with its palette grid already switched on. The grid draws all 256 palette slots as small swatches, so you can watch the two fades move their own slots at their own pace instead of inferring it 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 here because that grid is half the point of the demo. Press ~ (or tap the symbol in the bottom-left corner) to close it and watch the pictures on their own.
@returns
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. * * @returns {Promise<boolean>} */ async Demo.init(): Promise<boolean>
Loads the picture, builds the palette and the four fade targets, and starts the cycle.
@returns
init
() {
// 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;
    ... 106 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 // the whole way through. 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 kills two birds: it becomes // the "fully lit" target for the right half, AND the palette we hand to // indexize() below, which is what decides that 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;
    ... 106 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;
    ... 106 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: Palette, firstSlot: number, colors: readonly Color32[]): void
Copies a list of colors into a palette, starting at `firstSlot`.
@parampalette - Palette to fill.@paramfirstSlot - Slot that receives the first color of the list.@paramcolors - Colors to write, in order.
fillColors
(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;
    ... 106 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: Palette, firstSlot: number, count: number): void
Writes plain black into a run of slots, for the "faded out" target.
@parampalette - Palette to fill.@paramfirstSlot - First slot of the run.@paramcount - How many slots to blacken.
fillBlack
(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;
    ... 106 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: Palette, firstSlot: number, count: number): void
Writes plain black into a run of slots, for the "faded out" target.
@parampalette - Palette to fill.@paramfirstSlot - First slot of the run.@paramcount - How many slots to blacken.
fillBlack
(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 very same image the // color scan above 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): 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`.
@paramurl - Path or URL to the image file.@returnsPromise resolving to the loaded SpriteSheet.
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(): void
Paints both halves of the picture black, undoing whatever a fade left behind.
resetPictureToBlack
();
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;
    ... 106 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(): void
Runs the cycle: fires both fades at the start of a fade step, then waits.
update
() {
// 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;
    ... 106 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(): void
Fires this step's two fades, once per step.
triggerPhaseEffect
();
this.Demo.advancePhaseIfExpired(elapsed: number, tick: number): void
Moves to the next step of the cycle once the current one has run long enough.
@paramelapsed - Ticks since this step started.@paramtick - The current tick.
advancePhaseIfExpired
(const tick: numbertick - this.Demo.phaseStartTick: numberphaseStartTick, const tick: numbertick);
} /** * Draws the picture twice and the UI panel on top. */ Demo.render(): void
Draws the picture twice and the UI panel on top.
render
() {
// 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;
    ... 106 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: number, firstSlot: number): void
Draws one copy of the picture, plus the swatch strip under it.
@paramoriginX - Left edge of this half of the screen.@paramfirstSlot - Slot holding the picture's first (darkest) color.
renderScene
(0, const PLAIN_FIRST_SLOT: 16PLAIN_FIRST_SLOT);
this.Demo.renderScene(originX: number, firstSlot: number): void
Draws one copy of the picture, plus the swatch strip under it.
@paramoriginX - Left edge of this half of the screen.@paramfirstSlot - Slot holding the picture's first (darkest) color.
renderScene
(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;
    ... 106 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;
    ... 106 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(): void
Cancels anything running and starts the cycle over from black.
restart
();
} import uiui.end(); } /** * Current cycle step, shown in the engine overlay above the FPS bar. * * @returns {readonly { leftText: string }[]} */
Demo.overlayRows(): readonly {
    leftText: string;
}[]
Current cycle step, shown in the engine overlay above the FPS bar.
@returns
overlayRows
() {
this.Demo.overlayRowData: {}overlayRowData[0].leftText = this.Demo.getPhaseLabel(): string
A readable name for the step of the cycle we are in.
@returns
getPhaseLabel
();
return this.Demo.overlayRowData: {}overlayRowData; } /** * A readable name for the step of the cycle we are in. * * @returns {string} */ Demo.getPhaseLabel(): string
A readable name for the step of the cycle we are in.
@returns
getPhaseLabel
() {
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(): void
Fires this step's two fades, once per step.
triggerPhaseEffect
() {
// 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: Palette, exposureTarget: Palette): void
Starts both fades on the same frame with the same duration and easing.
@paramplainTarget - Where the left half's colors should end up.@paramexposureTarget - Where the right half's colors should end up.
startFades
(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: Palette, exposureTarget: Palette): void
Starts both fades on the same frame with the same duration and easing.
@paramplainTarget - Where the left half's colors should end up.@paramexposureTarget - Where the right half's colors should end up.
startFades
(this.Demo.plainDark: Palette | null
@type{Palette | null}
plainDark
, this.Demo.expDark: Palette | null
@type{Palette | null}
expDark
);
} } /** * Starts both fades on the same frame with the same duration and easing. * * @param {Palette} plainTarget - Where the left half's colors should end up. * @param {Palette} exposureTarget - Where the right half's colors should end up. */ Demo.startFades(plainTarget: Palette, exposureTarget: Palette): void
Starts both fades on the same frame with the same duration and easing.
@paramplainTarget - Where the left half's colors should end up.@paramexposureTarget - Where the right half's colors should end up.
startFades
(plainTarget: Palette
- Where the left half's colors should end up.
@paramplainTarget - Where the left half's colors should end up.
plainTarget
, exposureTarget: Palette
- Where the right half's colors should end up.
@paramexposureTarget - Where the right half's colors should end up.
exposureTarget
) {
// 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;
    ... 106 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@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: Palette
- Where the left half's colors should end up.
@paramplainTarget - Where the left half's colors should end up.
plainTarget
, const FADE_MS: numberFADE_MS);
// Right half: the exposure fade. It only reaches slots 1..15 because the // target palette we hand 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;
    ... 106 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@paramtarget - Target palette to fade toward.@paramdurationMs - Fade duration in milliseconds.@paramoptions - Highlight lead and easing curve.
paletteFadeExposure
(exposureTarget: Palette
- Where the right half's colors should end up.
@paramexposureTarget - Where the right half's colors should end up.
exposureTarget
, 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. * * @param {number} elapsed - Ticks since this step started. * @param {number} tick - The current tick. */ Demo.advancePhaseIfExpired(elapsed: number, tick: number): void
Moves to the next step of the cycle once the current one has run long enough.
@paramelapsed - Ticks since this step started.@paramtick - The current tick.
advancePhaseIfExpired
(elapsed: number
- Ticks since this step started.
@paramelapsed - Ticks since this step started.
elapsed
, tick: number
- The current tick.
@paramtick - The current tick.
tick
) {
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: number
- Ticks since this step started.
@paramelapsed - Ticks since this step started.
elapsed
>= const current: anycurrent.duration) {
this.Demo.phase: stringphase = const current: anycurrent.next; this.Demo.phaseStartTick: numberphaseStartTick = tick: number
- The current tick.
@paramtick - The current tick.
tick
;
this.Demo.effectTriggered: booleaneffectTriggered = false; } } /** * Cancels anything running and starts the cycle over from black. */ Demo.restart(): void
Cancels anything running and starts the cycle over from black.
restart
() {
// Effects keep the palette wherever they left it, 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;
    ... 106 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(): void
Paints both halves of the picture black, undoing whatever a fade left behind.
resetPictureToBlack
();
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;
    ... 106 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(): void
Paints both halves of the picture black, undoing whatever a fade left behind.
resetPictureToBlack
() {
function fillBlack(palette: Palette, firstSlot: number, count: number): void
Writes plain black into a run of slots, for the "faded out" target.
@parampalette - Palette to fill.@paramfirstSlot - First slot of the run.@paramcount - How many slots to blacken.
fillBlack
(this.Demo.palette: Palette | null
@type{Palette | null}
palette
, const EXP_FIRST_SLOT: 1EXP_FIRST_SLOT, this.Demo.colorCount: numbercolorCount);
function fillBlack(palette: Palette, firstSlot: number, count: number): void
Writes plain black into a run of slots, for the "faded out" target.
@parampalette - Palette to fill.@paramfirstSlot - First slot of the run.@paramcount - How many slots to blacken.
fillBlack
(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. * * @param {number} originX - Left edge of this half of the screen. * @param {number} firstSlot - Slot holding the picture's first (darkest) color. */ Demo.renderScene(originX: number, firstSlot: number): void
Draws one copy of the picture, plus the swatch strip under it.
@paramoriginX - Left edge of this half of the screen.@paramfirstSlot - Slot holding the picture's first (darkest) color.
renderScene
(originX: number
- Left edge of this half of the screen.
@paramoriginX - Left edge of this half of the screen.
originX
, firstSlot: number
- Slot holding the picture's first (darkest) color.
@paramfirstSlot - Slot holding the picture's first (darkest) color.
firstSlot
) {
// Every pixel of the image stores a slot number counted from EXP_FIRST_SLOT. // drawSprite() adds this number 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: number
- Slot holding the picture's first (darkest) color.
@paramfirstSlot - Slot holding the picture's first (darkest) color.
firstSlot
- 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;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => void
Draws a sprite region from an indexed sprite sheet. Sprite draws are batched internally. Grouping draws from the same {@link SpriteSheet } minimizes batch flushes and reduces GPU state changes. The sprite sheet must have been converted to palette indices via `spriteSheet.indexize(palette)` before the first draw call. Prefer `SpriteSheet.loadIndexed(...)` for one-call setup. **Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1. Index 0 is always transparent and is discarded by the fragment shader. The final palette lookup is `storedIndex + paletteOffset`, so: - `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`. `palette[0]` is never reachable because stored indices start at 1. - `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`, and so on. Use this for palette-swap effects such as team colors or damage flashes. **Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's robust buffer access returns 0 for every component; because the fragment shader forces alpha to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also produces out-of-bounds black pixels.
@since0.1.0@paramspriteSheet - Indexed sprite sheet.@paramsrcRect - Source rectangle within the sprite sheet, in pixels.@paramdestPos - Destination top-left position in display coordinates.@parampaletteOffset - Shift added to every stored pixel index before palette lookup (default 0).@exampleBT.drawSprite(sheet, new Rect2i(0, 0, 16, 16), new Vector2i(10, 10)); BT.drawSprite(sheet, new Rect2i(0, 0, 16, 16), new Vector2i(10, 10), 16); // blue team
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: number
- Left edge of this half of the screen.
@paramoriginX - Left edge of this half of the screen.
originX
+ this.Demo.sceneX: numbersceneX, const SCENE_TOP: 62SCENE_TOP), const paletteOffset: numberpaletteOffset);
this.Demo.renderRamp(originX: number, firstSlot: number): void
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 very 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.
@paramoriginX - Left edge of this half of the screen.@paramfirstSlot - Slot holding the picture's first (darkest) color.
renderRamp
(originX: number
- Left edge of this half of the screen.
@paramoriginX - Left edge of this half of the screen.
originX
, firstSlot: number
- Slot holding the picture's first (darkest) color.
@paramfirstSlot - Slot holding the picture's first (darkest) color.
firstSlot
);
} /** * 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 very 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. * * @param {number} originX - Left edge of this half of the screen. * @param {number} firstSlot - Slot holding the picture's first (darkest) color. */ Demo.renderRamp(originX: number, firstSlot: number): void
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 very 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.
@paramoriginX - Left edge of this half of the screen.@paramfirstSlot - Slot holding the picture's first (darkest) color.
renderRamp
(originX: number
- Left edge of this half of the screen.
@paramoriginX - Left edge of this half of the screen.
originX
, firstSlot: number
- Slot holding the picture's first (darkest) color.
@paramfirstSlot - Slot holding the picture's first (darkest) color.
firstSlot
) {
// 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: number
- Left edge of this half of the screen.
@paramoriginX - Left edge of this half of the screen.
originX
+ 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;
    ... 106 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: number
- Slot holding the picture's first (darkest) color.
@paramfirstSlot - Slot holding the picture's first (darkest) color.
firstSlot
+ 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 Demo
Shows the difference between BT.paletteFade and BT.paletteFadeExposure by running both on the same picture at the same time, on separate palette slots.
@implementsIBTDemo
Demo
);