/*
CRT Toggle: turn the post-process effects on and off in flight.
@description Turn a whole CRT post-process stack on and off at runtime, switching every two seconds as it runs.
@pageTitle BLIT386 Demo – CRT Toggle
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
Guides:
- Post-Process Effects: https://blit386.dev/docs/guides/post-process-effects#presets
- Game Loop: https://blit386.dev/docs/guides/game-loop#smoothing-motion-between-updates
- Overlay: https://blit386.dev/docs/guides/overlay#optional-bands
Reference:
- Rendering: https://blit386.dev/docs/api/rendering#post-process-effects
- Game Loop: https://blit386.dev/docs/api/game-loop#interpolating-render-state-with-renderalpha
- Core: https://blit386.dev/docs/api/core#requested-vs-active-backend
- Overlay: https://blit386.dev/docs/api/overlay#event-tags
---
WHAT YOU WILL SEE
-----------------
A simple, colorful scene: six wide horizontal bars across the middle of a 320x240 screen
and five squares bouncing around the whole screen on top of them. Every two seconds the
CRT preset flips on or off by itself. While it is on you see scanlines, an RGB shadow
mask, a gentle barrel curve, a slight red/blue color fringe, a bright band slowly rolling
down, faint grain, and a soft phosphor glow; while it is off the picture is just the crisp
4x upscale of what the engine drew. The squares keep bouncing either way, so you can
compare the two looks back to back.
A small panel in the top-left corner says "CRT: ON" or "CRT: OFF" so you always know which
side you are looking at. Notice that the bars stay straight through the toggle: barrel
distortion is a display-tier effect, so it bends the 1280x960 RGBA picture after the
palette resolve and upscale - not the 320x240 index buffer - which keeps diagonals free of
stair-steps.
WHAT YOU WILL LEARN
-------------------
- How to add and remove a whole stack of post-process effects at runtime with
BT.effectAdd(effect) and BT.effectRemove(effect).
- That an empty chain costs nothing extra: the first BT.effectAdd allocates the display
chain's off-screen render targets, and removing the last effect (or BT.effectClear())
frees them again, so the engine goes back to drawing straight through the upscale pass.
- That the same effect instances are kept across toggles. Creating new ones on every
toggle would also work, but it would rebuild the GPU pipelines every two seconds for an
identical look.
- BT.preset.crtPipBoy(): one call that returns a fresh array of nine pre-configured
display-tier effects, so you do not have to wire them up by hand.
- BT.renderAlpha and Vector2i.lerp() for drawing the squares between two physics ticks.
- Checking BT.activeBackend (through the shared isAvailable() helper) before touching the
effect API, because the software renderer has no post-processing.
HOW THE TOGGLE WORKS
--------------------
Time is measured in ticks (60 per second). Every TOGGLE_PERIOD_TICKS (120, so two seconds)
update() flips a boolean and either adds or removes every effect in the preset array. The
effects that animate over time (RollLine, Noise, Interference) get their `time` field fed
every tick whether or not they are in the chain, so they are never stale when they come
back.
SOFTWARE FALLBACK
-----------------
In software renderer mode (no WebGPU, or ?backend=software in the URL) post-process
effects are unavailable. The squares and bars still animate; the toggle is disabled and
the panel says "CRT: N/A" with a one-line explanation.
Why auto-toggle instead of a button? This demo is about the effect API, not controls.
Auto-toggling keeps the ON/OFF comparison hands-free. Pointer and keyboard input have
their own demos later in the series.
TRY THIS
--------
- Press the backquote key (`) to open the engine overlay. Its timing chart gets a "CRT:
ON" or "CRT: OFF" tag at every toggle, so you can see whether the render bar changes
while the effects are live. The overlay draws its bar and text with this demo's own
palette slots.
- Add ?backend=software to the URL to see the fallback panel.
*/
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 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, ...) 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 engine overlay's text
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; // the engine overlay's bar and gap background
// The five colors of the bouncing squares, in order, so each square gets its own.
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, in pixels per tick. One value per square so the motion
// looks irregular - if all five moved at the same speed they would line up 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 ticks per second is 2 seconds per state
// - long enough to read the "CRT: ON / OFF" label and watch the scene switch.
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 work 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];
// A small animated scene with the CRT effect stack flipping on and off every two seconds,
// showing the dynamic add/remove path of the post-process chain. The stack comes from
// BT.preset.crtPipBoy(), which returns a fresh array of display-tier effects
// (BarrelDistortion, ChromaticAberration, Interference, RollLine, Scanlines, RGBMask,
// Noise, Flicker, Bloom). The array is kept so the same instances are re-added on each
// toggle; that way the GPU pipelines stay alive instead of being torn down and rebuilt
// every two seconds.
class class DemoDemo {
// Same 320x240 logical / 1280x960 output setup as the PipBoy CRT demo, which the
// display-tier CRT preset needs.
Demo.configure(): {
displaySize: Vector2i;
drawingBufferSize: Vector2i;
maxCanvasSize: Vector2i;
outputUpscaleFilter: string;
targetFPS: number;
overlayStyle: {
barPaletteIndex: number;
textPaletteIndex: number;
gapPaletteIndex: number;
};
isOverlayTimingChartEnabled: boolean;
overlayTimingChartStyle: {
updateBarPaletteIndex: number;
renderBarPaletteIndex: number;
warningPaletteIndex: number;
errorPaletteIndex: number;
tagPaletteIndex: number;
};
}
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, run any pixel-tier
// effects on that r8uint buffer, then the engine resolves indices through the
// palette and upscales to RGBA at this size, then the display chain runs on
// the RGBA image. Without this field BT.effectAdd throws 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 the output buffer size so the CRT picture is not capped at the
// default 960x720 CSS size.
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 a crisp 4x4 block. 'linear' would soften
// them into bilinear-blended squares - a different look, also valid.
outputUpscaleFilter: stringoutputUpscaleFilter: 'nearest',
targetFPS: numbertargetFPS: const TARGET_FPS: 60TARGET_FPS,
// The engine overlay draws with this demo's own palette slots, and its timing
// chart is on so the "CRT: ON / OFF" tags have somewhere to land.
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,
},
};
}
async Demo.init(): unknowninit() {
// Step 1: build the palette. A small, colorful set of scene colors in the low
// slots. Bright primaries make the CRT effect obvious - soft pastels would look
// the same with or without it. 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;
... 124 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 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;
... 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(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 nine pre-configured display-tier effects
// (BarrelDistortion, ChromaticAberration, Interference, RollLine, Scanlines,
// RGBMask, Noise, Flicker, Bloom). Keeping the array means the same instances
// are re-added 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;
... 124 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 from a `time` field; filter
// the array once and keep the references instead of walking the whole stack
// every tick.
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. The PipBoy CRT demo shows the CRT look straight
// away; here it is nicer to begin clean and let the effect arrive on 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;
... 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;
// Step 5: place the bouncing squares evenly across the bottom half so they do not
// all start in the same spot. Each square keeps its position (pos) and velocity
// (vel) as Vector2i instances - the engine convention for pixel 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 with
// BT.renderAlpha so each square glides between physics ticks instead of
// jumping - see the Game Loop guide linked in the header.
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(): voidupdate() {
// 1. Time-based toggle (WebGPU only - effectAdd throws in software mode). Every
// TOGGLE_PERIOD_TICKS flip the boolean and either add or remove the entire preset
// stack. The engine handles the GPU pipeline lifecycle.
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;
... 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.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;
... 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.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 it
// 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;
... 124 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;
... 124 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 goes, 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;
... 124 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;
... 124 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 the rolling line, noise, and interference.
// 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;
... 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 / 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 it off the screen edges. New Vector2i instances
// are assigned rather than components mutated; reassigning sq.pos and sq.vel is
// fine for per-frame demo state.
for (const const sq: anysq of this.Demo.squares: {} | undefinedsquares) {
// Snapshot where the square was 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 and right walls. The valid range is [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 it does not 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 and bottom walls. 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(): voidrender() {
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(const C_BG: 1C_BG);
// The high-contrast horizontal bars across the middle. They are 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;
... 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(20, const y: numbery, const DISPLAY_W: 320DISPLAY_W - 40, const BAR_HEIGHT: 18BAR_HEIGHT), const BAR_COLORS: {}BAR_COLORS[let i: numberi]);
}
// 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().
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;
... 124 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;
... 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 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 handy 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 DemoDemo);