// @pageTitle BLIT386 Demo – CRT Toggle
// @description Turn a whole CRT post-process stack on and off at runtime, switching every two seconds as it runs.
//
// CRT Toggle: turn the post-process effects on and off in flight.
//
// Part of the BLIT386 demo series.
// Prerequisites:
// Basics https://demos.blit386.dev/basics
// PipBoy CRT https://demos.blit386.dev/crt-pipboy
//
// Live version: https://demos.blit386.dev/crt-toggle
//
// Guide: https://blit386.dev/docs/guides/post-process-effects
//
// WHAT YOU WILL SEE
// A colorful, simple scene - bouncing squares and a few horizontal bars. Every two seconds
// the CRT preset flips on and off automatically. Status text sits in a small panel drawn
// with the shared UI kit. While it is on
// you see scanlines, the RGB shadow mask, smooth barrel curvature, and a soft phosphor
// glow; while it is off the pixels are exactly what the engine drew (no post-processing).
// The bouncing keeps going either way, so you can compare the two looks side by side.
//
// Notice that the lines stay STRAIGHT through the toggle: barrel distortion is display-tier,
// so it runs on RGBA after palette resolve + upscale to the canvas size - not on the
// 320x240 index buffer - which avoids stair-step artifacts on diagonals.
//
// WHAT YOU WILL LEARN
// - How to add and remove a STACK of post-process effects at runtime.
// - That the effect chain is "free" when nothing is registered: the engine renders straight
// to the screen with zero extra cost. The first call to BT.effectAdd allocates an off-
// screen texture; the last BT.effectRemove or BT.effectClear frees it again.
// - That you keep the SAME effect instances across toggles. Demos that destroy and
// recreate them on every toggle would also work, but they would re-create the GPU
// pipeline on every toggle - wasteful when the look is the same.
// - The convenience of `BT.preset.crtPipBoy()`: returns a fresh array of pre-configured
// display-tier effects so you do not have to wire them up by hand.
//
// HOW THE TOGGLE WORKS
// We measure time in ticks (60 per second). Every TOGGLE_PERIOD_TICKS the demo flips a
// boolean and either adds or removes the entire preset stack. A small kit panel shows
// "CRT: ON" or "CRT: OFF" so you always know which side you are looking at.
//
// SOFTWARE FALLBACK
// In software renderer mode, post-process effects are unavailable. The bouncing
// squares and color bars still animate; CRT toggle is disabled and the status panel
// explains why.
//
// Why auto-toggle instead of a button? This page focuses on the effect API, not controls.
// Auto-toggling keeps the ON/OFF comparison hands-free. Pointer and keyboard input are
// covered in the pointer and keyboard demos.
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;
... 106 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 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 isAvailableisAvailable, import SOFTWARE_FALLBACK_NOTESOFTWARE_FALLBACK_NOTE } from './shared/post-process-backend.js';
import { import applyThemeapplyTheme, import uiui, import UI_ANCHORSUI_ANCHORS } from './shared/ui.js';
// Internal pixel resolution.
const const DISPLAY_W: 320DISPLAY_W = 320;
const const DISPLAY_H: 240DISPLAY_H = 240;
// Output drawing-buffer resolution. Setting it 4x larger than the logical size unlocks
// the display-tier effects (barrel, scanlines, mask, etc.) and gives them enough output
// pixels to render smoothly. Each logical pixel maps to a 4x4 output block.
const const OUTPUT_W: 1280OUTPUT_W = 1280;
const const OUTPUT_H: 960OUTPUT_H = 960;
const const TARGET_FPS: 60TARGET_FPS = 60;
// Palette indices. Index 0 is always transparent.
const const C_BG: 1C_BG = 1; // Dark navy: the background fill.
const const C_LABEL: 2C_LABEL = 2; // White: the corner label.
const const C_RED: 3C_RED = 3;
const const C_GREEN: 4C_GREEN = 4;
const const C_BLUE: 5C_BLUE = 5;
const const C_YELLOW: 6C_YELLOW = 6;
const const C_CYAN: 7C_CYAN = 7;
const const C_MAGENTA: 8C_MAGENTA = 8;
const const C_OVERLAY_BAR: 9C_OVERLAY_BAR = 9; // Overlay row background
// The five colors used for the bouncing squares. We list them in order so each
// square gets a distinct color from the palette.
const const SQUARE_COLORS: {}SQUARE_COLORS = [const C_RED: 3C_RED, const C_GREEN: 4C_GREEN, const C_BLUE: 5C_BLUE, const C_YELLOW: 6C_YELLOW, const C_MAGENTA: 8C_MAGENTA];
// How big each bouncing square is (in pixels).
const const SQUARE_SIZE: 24SQUARE_SIZE = 24;
// How many bouncing squares to show.
const const SQUARE_COUNT: 5SQUARE_COUNT = 5;
// How fast each square moves (pixels per tick). One value per square so the
// motion looks irregular - if all five moved at the same speed, they would
// line up vertically and the demo would look duller.
const const SQUARE_SPEEDS: {}SQUARE_SPEEDS = [
new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(2, 1),
new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(1, 2),
new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(3, 1),
new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(1, 3),
new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(2, 2),
];
// How many ticks between toggles. 120 ticks at 60 FPS = 2 seconds per state.
// You should be able to read the "CRT: ON / OFF" label and watch the scene
// switch at a leisurely pace.
const const TOGGLE_PERIOD_TICKS: 120TOGGLE_PERIOD_TICKS = 120;
// Static horizontal bars across the middle of the screen. They give the CRT scanlines
// something high-contrast to chew on so the difference between ON and OFF is obvious.
const const BAR_HEIGHT: 18BAR_HEIGHT = 18;
const const BAR_GAP: 6BAR_GAP = 6;
const const BAR_TOP: 60BAR_TOP = 60;
const const BAR_COLORS: {}BAR_COLORS = [const C_RED: 3C_RED, const C_YELLOW: 6C_YELLOW, const C_GREEN: 4C_GREEN, const C_CYAN: 7C_CYAN, const C_BLUE: 5C_BLUE, const C_MAGENTA: 8C_MAGENTA];
/** @typedef {import('blit386').IBTDemo} IBTDemo */
/** @typedef {import('blit386').HardwareSettings} HardwareSettings */
/**
* Toggle demo: a small animated scene with the CRT effect stack flipping on and off
* every two seconds. Demonstrates the dynamic add/remove path of the post-process chain.
*
* The effect stack comes from `BT.preset.crtPipBoy()`, a one-line helper that returns a
* fresh array of display-tier effects (BarrelDistortion + ChromaticAberration + ... +
* Bloom). We hold onto the array so we can re-add the SAME instances on each toggle -
* that way the GPU pipelines stay alive across toggles instead of being torn down and
* rebuilt every two seconds.
*
* @implements {IBTDemo}
*/
class class DemoToggle demo: a small animated scene with the CRT effect stack flipping on and off
every two seconds. Demonstrates the dynamic add/remove path of the post-process chain.
The effect stack comes from `BT.preset.crtPipBoy()`, a one-line helper that returns a
fresh array of display-tier effects (BarrelDistortion + ChromaticAberration + ... +
Bloom). We hold onto the array so we can re-add the SAME instances on each toggle -
that way the GPU pipelines stay alive across toggles instead of being torn down and
rebuilt every two seconds.Demo {
/**
* Same 320x240 logical / 1280x960 output setup as the PipBoy CRT demo for display-tier CRT presets.
*
* @returns {Partial<HardwareSettings>}
*/
Demo.configure(): Partial<HardwareSettings>Same 320x240 logical / 1280x960 output setup as the PipBoy CRT demo for display-tier CRT presets.configure() {
return {
// Internal pixel-art resolution. Game logic and draws operate at this size.
displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const DISPLAY_W: 320DISPLAY_W, const DISPLAY_H: 240DISPLAY_H),
// drawingBufferSize is required for display-tier effects (barrel, scanlines,
// mask, bloom). Flow: draw palette indices at 320x240, optional pixel-tier on
// that r8uint buffer, then the engine resolves indices through the palette LUT
// and upscales to RGBA at this size, then the display chain runs on RGBA.
// Without this field, BT.effectAdd would throw for display-tier effects.
drawingBufferSize: Vector2idrawingBufferSize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const OUTPUT_W: 1280OUTPUT_W, const OUTPUT_H: 960OUTPUT_H),
// Match output buffer size so the CRT picture is not capped at 960x720 CSS.
maxCanvasSize: Vector2imaxCanvasSize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const OUTPUT_W: 1280OUTPUT_W, const OUTPUT_H: 960OUTPUT_H),
// 'nearest' keeps each source pixel as a crisp 4x4 block. 'linear' would
// soften them into bilinear-blended squishes - a different look, also valid.
outputUpscaleFilter: stringoutputUpscaleFilter: 'nearest',
targetFPS: numbertargetFPS: const TARGET_FPS: 60TARGET_FPS,
overlayStyle: {
barPaletteIndex: number;
textPaletteIndex: number;
gapPaletteIndex: number;
}
overlayStyle: {
barPaletteIndex: numberbarPaletteIndex: const C_OVERLAY_BAR: 9C_OVERLAY_BAR,
textPaletteIndex: numbertextPaletteIndex: const C_LABEL: 2C_LABEL,
gapPaletteIndex: numbergapPaletteIndex: const C_OVERLAY_BAR: 9C_OVERLAY_BAR,
},
isOverlayTimingChartEnabled: booleanisOverlayTimingChartEnabled: true,
overlayTimingChartStyle: {
updateBarPaletteIndex: number;
renderBarPaletteIndex: number;
warningPaletteIndex: number;
errorPaletteIndex: number;
tagPaletteIndex: number;
}
overlayTimingChartStyle: {
updateBarPaletteIndex: numberupdateBarPaletteIndex: const C_CYAN: 7C_CYAN,
renderBarPaletteIndex: numberrenderBarPaletteIndex: const C_YELLOW: 6C_YELLOW,
warningPaletteIndex: numberwarningPaletteIndex: const C_MAGENTA: 8C_MAGENTA,
errorPaletteIndex: numbererrorPaletteIndex: const C_RED: 3C_RED,
tagPaletteIndex: numbertagPaletteIndex: const C_GREEN: 4C_GREEN,
},
};
}
/**
* @returns {Promise<boolean>}
*/
async Demo.init(): Promise<boolean>Called once after the selected rendering backend has been initialized.
Load assets and prepare a demo state here.init() {
// Step 1: build the palette
// A small, colorful set of scene colors in the low slots. Bright primaries make
// the CRT effect visually obvious - soft pastels would look the same with or
// without. The palette is 256 entries long so the shared UI theme can live in
// the high slots (240-251), far away from the scene colors.
const const palette: Palettepalette = 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) => PaletteCreates a standalone palette instance.paletteCreate(256);
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_BG: 1C_BG, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(20, 30, 50, 255));
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_LABEL: 2C_LABEL, class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32.Color32.white: Color32Pure white color (255, 255, 255, 255).
Cached frozen singleton - do not modify.white);
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_RED: 3C_RED, class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32.Color32.red: Color32Pure red color (255, 0, 0, 255).
Cached frozen singleton - do not modify.red);
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_GREEN: 4C_GREEN, class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32.Color32.green: Color32Pure green color (0, 255, 0, 255).
Cached frozen singleton - do not modify.green);
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_BLUE: 5C_BLUE, class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32.Color32.blue: Color32Pure blue color (0, 0, 255, 255).
Cached frozen singleton - do not modify.blue);
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_YELLOW: 6C_YELLOW, class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32.Color32.yellow: Color32Yellow color (255, 255, 0, 255).
Cached frozen singleton - do not modify.yellow);
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_CYAN: 7C_CYAN, class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32.Color32.cyan: Color32Cyan color (0, 255, 255, 255).
Cached frozen singleton - do not modify.cyan);
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_MAGENTA: 8C_MAGENTA, class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32.Color32.magenta: Color32Magenta color (255, 0, 255, 255).
Cached frozen singleton - do not modify.magenta);
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_BAR: 9C_OVERLAY_BAR, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(10, 15, 25, 220));
// Install the shared UI kit colors into slots 240-251. The kit draws the small
// CRT status panel; the animated scene keeps its own bright primaries above.
import applyThemeapplyTheme(const palette: Palettepalette);
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) => 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(const palette: Palettepalette);
this.Demo.effectsAvailable: anyeffectsAvailable = import isAvailableisAvailable();
if (this.Demo.effectsAvailable: anyeffectsAvailable) {
// Step 2: build the CRT preset ONCE up front
// BT.preset.crtPipBoy() returns a fresh array of pre-configured display-tier
// effects (BarrelDistortion + ChromaticAberration + Interference + RollLine +
// Scanlines + RGBMask + Vignette + Noise + Flicker + Bloom).
//
// We hold onto the array so we can re-add the SAME instances on each toggle.
// Re-creating them every toggle would also work, but it would re-allocate the
// GPU pipelines and uniform buffers each time - wasteful when the look is the
// same.
this.Demo.stack: {} | undefinedstack = 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.preset: {
crtPipBoy: typeof crtPipBoy;
amber: typeof amber;
green: typeof green;
}
Pre-configured display-tier effect stacks ("looks").
Each function returns a fresh array of effects. Add them to the engine
via
{@link
BT.effectAdd
}
.preset.crtPipBoy: () => Effect[]Preset bundle that recreates the original "PipBoy" CRT look using the
decomposed display-tier effects.
Order matters: barrel curvature applies before scanlines/mask so the curve
carries the rest of the effects with it; bloom comes last so the phosphor
glow blends across the already-modulated output.
The returned effects can be added in order to the engine's display chain.crtPipBoy();
// Step 3: pick out the time-driven effects so update() can animate them
// Some effects (RollLine, Noise, Interference) animate using a `time` field;
// we filter the array once and remember the references so we don't iterate
// the whole stack on every frame.
this.Demo.timedEffects: anytimedEffects = this.Demo.stack: {}stack.filter((fx: anyfx) => 'time' in fx: anyfx);
} else {
this.Demo.stack: {} | undefinedstack = [];
this.Demo.timedEffects: anytimedEffects = [];
}
// Step 4: start in the OFF state
// PipBoy CRT demo already shows what the CRT looks like straight away; here it's nicer
// to begin clean and then have the effect arrive after the first toggle.
this.Demo.enabled: anyenabled = false;
this.Demo.lastToggleTick: number | undefinedlastToggleTick = 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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks;
// Step 5: place the bouncing squares
// Place them evenly across the bottom half so they don't all start in the same
// spot. Each square keeps its own position (pos) and velocity (vel) as Vector2i
// instances - the engine convention for all pixel-level coordinates.
this.Demo.squares: {} | undefinedsquares = [];
for (let let i: numberi = 0; let i: numberi < const SQUARE_COUNT: 5SQUARE_COUNT; let i: numberi++) {
const const startPos: Vector2istartPos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(20 + let i: numberi * 50, 150 + (let i: numberi % 2) * 30);
this.Demo.squares: {}squares.push({
pos: Vector2ipos: const startPos: Vector2istartPos,
// prevPos remembers where the square was at the START of the most recent
// update() tick. render() blends between prevPos and pos using
// BT.renderAlpha so each square glides smoothly between physics ticks
// instead of jumping - see "Interpolating render state with renderAlpha"
// in the engine's docs/api-game-loop.md.
prevPos: Vector2iprevPos: const startPos: Vector2istartPos,
vel: Vector2ivel: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const SQUARE_SPEEDS: {}SQUARE_SPEEDS[let i: numberi].x, const SQUARE_SPEEDS: {}SQUARE_SPEEDS[let i: numberi].y),
color: anycolor: const SQUARE_COLORS: {}SQUARE_COLORS[let i: numberi % const SQUARE_COLORS: {}SQUARE_COLORS.length],
});
}
return true;
}
Demo.update(): voidCalled zero or more times per frame at the fixed timestep declared by
`targetFPS`. The accumulator pattern ensures the target rate is met on
average, but a single frame may invoke this multiple times (catch-up) or
not at all. Update simulation, timers, and input-driven state here.
This is a hot path. Minimize allocations, reuse objects, and prefer
in-place vector operations where possible.
Avoid rendering work here; draw in `render()` instead.update() {
// 1. Time-based toggle (WebGPU only - effectAdd throws in software mode)
// Every TOGGLE_PERIOD_TICKS we flip the boolean and either add or remove the
// entire preset stack. The engine handles the GPU pipeline lifecycle for us.
if (this.Demo.effectsAvailable: anyeffectsAvailable && 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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks - this.Demo.lastToggleTick: number | undefinedlastToggleTick >= const TOGGLE_PERIOD_TICKS: 120TOGGLE_PERIOD_TICKS) {
this.Demo.lastToggleTick: number | undefinedlastToggleTick = 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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks;
this.Demo.enabled: anyenabled = !this.Demo.enabled: anyenabled;
if (this.Demo.enabled: anyenabled) {
// Add every effect from the preset to the chain. Each one declares its
// own tier ('display' for the CRT effects), so the engine routes them
// automatically.
for (const const fx: anyfx of this.Demo.stack: {} | undefinedstack) {
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.effectAdd: (effect: Effect) => voidAppends a fullscreen post-processing effect to whichever chain matches
its declared
{@link
Effect.tier
}
.
- `tier='pixel'` -> pixel chain (logical resolution).
- `tier='display'` -> display chain (output resolution); requires
`drawingBufferSize` in effective hardware settings (`configure()` or
`defaultConfig()`).
Effects run in registration order within each tier. The pixel chain runs
first, followed by the upscale pass, followed by the display chain. Each
{@link
Effect
}
instance owns its own GPU resources and may be mutated
each frame from demo code.effectAdd(const fx: anyfx);
}
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.assignTag: (label?: string) => voidPlaces a labeled marker on the overlay timing chart at the current tick.
Requires `isOverlayTimingChartEnabled: true` in `configure()`. Tags scroll with the chart
history and are pruned when they leave the visible window. Empty labels become
`"Untitled"`. Chart width resets add an automatic `"Start"` tag.assignTag('CRT: ON');
} else {
// Remove them all. When the last effect is removed, the engine drops
// the off-screen ping-pong textures and reverts to drawing straight
// through the upscale pass to the swap chain.
for (const const fx: anyfx of this.Demo.stack: {} | undefinedstack) {
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.effectRemove: (effect: Effect) => voidRemoves a previously registered post-processing effect.
Searches both tiers and disposes the effect from whichever chain holds
it. Removing an effect that was never added is a no-op.effectRemove(const fx: anyfx);
}
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.assignTag: (label?: string) => voidPlaces a labeled marker on the overlay timing chart at the current tick.
Requires `isOverlayTimingChartEnabled: true` in `configure()`. Tags scroll with the chart
history and are pruned when they leave the visible window. Empty labels become
`"Untitled"`. Chart width resets add an automatic `"Start"` tag.assignTag('CRT: OFF');
}
}
if (this.Demo.effectsAvailable: anyeffectsAvailable) {
// The CRT shaders use `time` for their rolling line and noise. Feed it seconds.
// Safe to set even when the effects are not in the chain - the field is just a
// number on the JS instance until the next encode pass reads it.
const const seconds: numberseconds = 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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks / const TARGET_FPS: 60TARGET_FPS;
for (const const fx: anyfx of this.Demo.timedEffects: anytimedEffects) {
const fx: anyfx.time = const seconds: numberseconds;
}
}
// 2. Move each square and bounce off the screen edges
// Vector2i is immutable, so we assign new instances rather than mutating components.
// Reassigning sq.pos and sq.vel is allowed for per-frame demo state (see CLAUDE.md).
for (const const sq: anysq of this.Demo.squares: {} | undefinedsquares) {
// Snapshot "where was this square a moment ago" BEFORE moving it, so
// render() can draw a smooth in-between position instead of a pop.
const sq: anysq.prevPos = const sq: anysq.pos;
const sq: anysq.pos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const sq: anysq.pos.x + const sq: anysq.vel.x, const sq: anysq.pos.y + const sq: anysq.vel.y);
// Bounce against the left/right walls. We compare against [0, DISPLAY_W - SQUARE_SIZE]
// because the square's anchor is its top-left corner.
if (const sq: anysq.pos.x <= 0 || const sq: anysq.pos.x >= const DISPLAY_W: 320DISPLAY_W - const SQUARE_SIZE: 24SQUARE_SIZE) {
const sq: anysq.vel = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(-const sq: anysq.vel.x, const sq: anysq.vel.y);
// Nudge the position back inside the playfield so we don't bounce twice.
const sq: anysq.pos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(Math.max(0, Math.min(const sq: anysq.pos.x, const DISPLAY_W: 320DISPLAY_W - const SQUARE_SIZE: 24SQUARE_SIZE)), const sq: anysq.pos.y);
}
// Same for the top/bottom walls. We let the squares roam the full screen.
if (const sq: anysq.pos.y <= 0 || const sq: anysq.pos.y >= const DISPLAY_H: 240DISPLAY_H - const SQUARE_SIZE: 24SQUARE_SIZE) {
const sq: anysq.vel = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const sq: anysq.vel.x, -const sq: anysq.vel.y);
const sq: anysq.pos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const sq: anysq.pos.x, Math.max(0, Math.min(const sq: anysq.pos.y, const DISPLAY_H: 240DISPLAY_H - const SQUARE_SIZE: 24SQUARE_SIZE)));
}
}
}
Demo.render(): voidCalled once per `requestAnimationFrame` tick (browser refresh rate).
Issue all draw calls for the current frame here.
When
{@link
HardwareSettings.isOverlayEnabled
}
is `true` (default), the engine
draws a screen-space overlay HUD after this method returns (present FPS, target FPS, draw calls,
frame/update()/render() timings, backend, demo title). Optional
{@link
overlayRows
}
adds stacked bars above
the footer.
Demos do not need to duplicate engine overlay text. Reserve about ~42 px at the top and space for the bottom palette
grid (or ~13 px when
{@link
HardwareSettings.isOverlayPaletteEnabled
}
is `false`) at the bottom (plus ~14 px per
custom overlay row) for overlay bars, or disable the overlay in `configure()` when using custom full-screen HUD
layouts.
This is a hot path. Batch draws by texture to reduce GPU state changes
and reuse Color32/Vector2i instances instead of allocating per frame.
Avoid mutating the simulation state here unless it is strictly visual.render() {
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) => 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(const C_BG: 1C_BG);
// Draw the high-contrast horizontal bars across the middle. They're static -
// the CRT scanlines and shadow mask interact strongly with bright horizontals.
for (let let i: numberi = 0; let i: numberi < const BAR_COLORS: {}BAR_COLORS.length; let i: numberi++) {
const const y: numbery = const BAR_TOP: 60BAR_TOP + let i: numberi * (const BAR_HEIGHT: 18BAR_HEIGHT + const BAR_GAP: 6BAR_GAP);
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) => 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(20, const y: numbery, const DISPLAY_W: 320DISPLAY_W - 40, const BAR_HEIGHT: 18BAR_HEIGHT), const BAR_COLORS: {}BAR_COLORS[let i: numberi]);
}
// Draw the bouncing squares on top of the bars. Vector2i.lerp() blends prevPos
// toward pos by BT.renderAlpha, so a square's drawn position matches this exact
// render moment instead of only its last-tick position - smoother motion when
// render() runs at a different rate than update() (see docs/api-game-loop.md
// in the engine repo for the full explanation).
for (const const sq: anysq of this.Demo.squares: {} | undefinedsquares) {
const const drawPos: Vector2idrawPos = 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.Vector2i.lerp(a: Vector2i, b: Vector2i, t: number): Vector2iLinearly interpolates between two vectors.
Result is truncated to integers. t is clamped to [0, 1].lerp(const sq: anysq.prevPos, const sq: anysq.pos, 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.renderAlpha: numberFractional progress between the last completed fixed update and the next.
Intended for interpolating render state between fixed-update steps.renderAlpha);
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) => 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 drawPos: Vector2idrawPos.Vector2i.x: numberHorizontal component (defaults to 0).x, const drawPos: Vector2idrawPos.Vector2i.y: numberVertical component (defaults to 0).y, const SQUARE_SIZE: 24SQUARE_SIZE, const SQUARE_SIZE: 24SQUARE_SIZE), const sq: anysq.color);
}
// Status readout: a small panel from the shared UI kit, drawn last so it sits on
// top of the moving squares. Like everything the demo draws, it lives on the
// logical buffer, so the CRT effects warp and glow the panel too - a nice way to
// read the toggle even when you cannot see the scanlines up close.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT);
import uiui.panel();
if (this.Demo.effectsAvailable: anyeffectsAvailable) {
// 'accent' (phosphor green) while the stack is live, 'dim' gray while it rests.
import uiui.label(this.Demo.enabled: anyenabled ? 'CRT: ON' : 'CRT: OFF', { color: stringcolor: this.Demo.enabled: anyenabled ? 'accent' : 'dim' });
import uiui.label('Auto-toggles every 2s', { color: stringcolor: 'dim' });
} else {
// Software renderer: no post-processing at all, so explain the reduced mode.
import uiui.label('CRT: N/A', { color: stringcolor: 'dim' });
import uiui.label(import SOFTWARE_FALLBACK_NOTESOFTWARE_FALLBACK_NOTE, { color: stringcolor: 'warm' });
}
import uiui.end();
}
}
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 DemoToggle demo: a small animated scene with the CRT effect stack flipping on and off
every two seconds. Demonstrates the dynamic add/remove path of the post-process chain.
The effect stack comes from `BT.preset.crtPipBoy()`, a one-line helper that returns a
fresh array of display-tier effects (BarrelDistortion + ChromaticAberration + ... +
Bloom). We hold onto the array so we can re-add the SAME instances on each toggle -
that way the GPU pipelines stay alive across toggles instead of being torn down and
rebuilt every two seconds.Demo);