/*
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.bootstrap, const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT, class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32, class Rect2iInteger rectangle for pixel-perfect bounds and regions.
Used throughout the engine for sprite regions, display-space bounds, and
zero-allocation geometry helpers. Both convenience getters and allocation-free
`*To()` helpers are provided so callers can choose between readability and
hot-path efficiency.Rect2i, class SpriteSheetSprite-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.SpriteSheet, class Vector2iInteger 2D vector for pixel-perfect positioning.
Used for points, sizes, directions, and camera offsets throughout the engine.
The API includes both allocation-free `*To()` / `*InPlace()` variants and
convenience methods that return new vectors.Vector2i } from 'blit386';
import {
import applyThemeapplyTheme,
import 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): Color32Creates a clamped 8-bit RGBA color.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 | nullpalette = null;
// The picture.
/** @type {SpriteSheet | null} */
Demo.sheet: SpriteSheet | nullsheet = null;
// The rectangle covering all of the picture.
/** @type {Rect2i | null} */
Demo.spriteRect: Rect2i | nullspriteRect = null;
// The colors found inside the PNG, darkest first.
/** @type {Color32[]} */
Demo.sceneColors: {}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 | nullexpLit = null;
// The exposure fade's dark target.
/** @type {Palette | null} */
Demo.expDark: Palette | nullexpDark = null;
// The plain fade's lit target.
/** @type {Palette | null} */
Demo.plainLit: Palette | nullplainLit = null;
// The plain fade's dark target.
/** @type {Palette | null} */
Demo.plainDark: Palette | nullplainDark = 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): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(480, 480),
maxCanvasSize: Vector2imaxCanvasSize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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 | nullpalette = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteCreate: (size?: number) => PaletteCreates a standalone palette instance.paletteCreate(256);
// 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: Palettepalette);
// 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 | nullexpLit = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteCreate: (size?: number) => PaletteCreates a standalone palette instance.paletteCreate(const EXPOSURE_TARGET_SIZE: 16EXPOSURE_TARGET_SIZE);
try {
this.Demo.sceneColors: {}sceneColors = await class SpriteSheetSprite-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.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.loadColorsIntoPalette(const SPRITE_URL: "/sprites/mushroom-cloud.png"SPRITE_URL, this.Demo.expLit: PaletteexpLit, 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: {}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 | nullplainLit = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteCreate: (size?: number) => PaletteCreates a standalone palette instance.paletteCreate(256);
function fillColors(palette: any, firstSlot: any, colors: any): voidfillColors(this.Demo.plainLit: PaletteplainLit, const PLAIN_FIRST_SLOT: 16PLAIN_FIRST_SLOT, this.Demo.sceneColors: {}sceneColors);
// The two "faded out" targets: the same runs of slots, but black.
this.Demo.expDark: Palette | nullexpDark = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteCreate: (size?: number) => PaletteCreates a standalone palette instance.paletteCreate(const EXPOSURE_TARGET_SIZE: 16EXPOSURE_TARGET_SIZE);
function fillBlack(palette: any, firstSlot: any, count: any): voidfillBlack(this.Demo.expDark: PaletteexpDark, const EXP_FIRST_SLOT: 1EXP_FIRST_SLOT, this.Demo.colorCount: numbercolorCount);
this.Demo.plainDark: Palette | nullplainDark = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteCreate: (size?: number) => PaletteCreates a standalone palette instance.paletteCreate(256);
function fillBlack(palette: any, firstSlot: any, count: any): voidfillBlack(this.Demo.plainDark: PaletteplainDark, 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 | nullsheet = await class SpriteSheetSprite-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.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`.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 | nullspriteRect = this.Demo.sheet: SpriteSheet | nullsheet.SpriteSheet.fullRect(): Rect2iReturns a source rectangle that covers the entire sprite sheet.fullRect();
this.Demo.sceneX: numbersceneX = Math.floor((const HALF_WIDTH: 240HALF_WIDTH - this.Demo.spriteRect: Rect2ispriteRect.Rect2i.width: numberWidth in pixels (defaults to 0).width) / 2);
if (this.Demo.spriteRect: Rect2ispriteRect.Rect2i.height: numberHeight 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: Rect2ispriteRect.Rect2i.height: numberHeight 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 | nullsheet.SpriteSheet.indexize(palette: Palette): voidConverts 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.indexize(this.Demo.expLit: PaletteexpLit);
// 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) => voidStores the active engine palette.
Use this to swap the **entire palette** (e.g. switch between a day and night
theme). After this call the renderer uploads the new palette uniform on the
next frame.
**Palette-value swap (change what a slot looks like):** mutate the live
{@link
BT.palette
}
in place with `palette.set(slot, newColor)`. The renderer
uploads dirty slots on the next frame; no `paletteSet()` or
{@link
BT.spritesRefresh
}
needed.
**Palette-layout swap (same colors, different slot positions):** build a new
palette with the same colors at new indices, call `paletteSet()`, then call
{@link
BT.spritesRefresh
}
so every sprite sheet re-maps its original RGBA
pixels against the new slot layout.paletteSet(this.Demo.palette: Palettepalette);
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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.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) => voidSets the frame clear color using a palette index.
The renderer uses this color when clearing the full display at the start
of the next frame.clear(this.Demo.theme: nulltheme.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) => voidDraws 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.systemPrint(new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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) => voidDraws 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.systemPrint(new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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 | nullplainLit, this.Demo.expLit: Palette | nullexpLit);
} else if (this.Demo.phase: stringphase === 'fade-out') {
this.Demo.startFades(plainTarget: any, exposureTarget: any): voidstartFades(this.Demo.plainDark: Palette | nullplainDark, this.Demo.expDark: Palette | nullexpDark);
}
}
// 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) => voidFades 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.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) => voidFades 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)`paletteFadeExposure(exposureTarget: anyexposureTarget, const FADE_MS: numberFADE_MS, { ExposureFadeOptions.highlightLead?: number | undefinedHow 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: () => voidCancels all running palette effects immediately.
The palette stays at whatever state it was in when canceled.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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.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 | nullpalette, const EXP_FIRST_SLOT: 1EXP_FIRST_SLOT, this.Demo.colorCount: numbercolorCount);
function fillBlack(palette: any, firstSlot: any, count: any): voidfillBlack(this.Demo.palette: Palette | nullpalette, 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 | nullsheet, this.Demo.spriteRect: Rect2i | nullspriteRect, new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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) => voidDraws a filled rectangle.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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.bootstrap(class DemoDemo);