// @pageTitle BLIT386 Demo – PipBoy CRT
// @description A faux Fallout terminal built from decomposed CRT effects: barrel warp, scanlines, mask, and glitches.
//
// PipBoy CRT: a faux Fallout terminal with scanlines, glitches, and bloom.
//
// Part of the BLIT386 demo series.
// Prerequisites:
//   Basics      https://demos.blit386.dev/basics
//   Bitmap Font https://demos.blit386.dev/bitmap-font
//
// Live version: https://demos.blit386.dev/crt-pipboy
//
// Guide: https://blit386.dev/docs/guides/post-process-effects
//
// WHAT YOU WILL SEE
// A green-on-black terminal that looks like an old curved CRT screen. Scanlines, a soft
// glow (called "bloom"), and tiny noise speckles make the picture look like it is coming
// from a real cathode-ray tube. Every few seconds the picture glitches: a band of pixels
// jumps sideways, the color channels split apart, the whole screen flickers darker, or
// static noise rolls.
//
// WHAT YOU WILL LEARN
//   - "Post-processing": running effects on the WHOLE screen after we are done drawing it.
//   - The two effect TIERS BLIT386 offers, and why they exist:
//       * pixel-tier: chunky, palette-native, runs on the logical index buffer at 320x240 (one byte per pixel).
//       * between tiers: the engine looks up each index in your palette and upscales to RGBA at canvas size.
//       * display-tier: smooth, simulates the physical screen, runs on that full-size RGBA image.
//   - How to compose individual effects (BarrelDistortion, Scanlines, RGBMask, ...) instead
//     of relying on a single big shader. The preset BT.preset.crtPipBoy() does this for you
//     in one line; here we build the stack explicitly so each piece is visible.
//   - A "state machine": a tiny set of rules that decides when to start a glitch, what kind,
//     and how long it lasts.
//
// HOW POST-PROCESSING WORKS
// Normally the engine draws straight to the screen. When you add an effect with BT.effectAdd
// the engine routes the scene through one or two effect chains. Each effect reads a texture,
// writes a new one, and the last effect in the display chain writes to the swap chain.
//
// Pixel-tier effects (e.g. PixelGlitch) operate on the logical framebuffer, which stores
// palette slot indices (GPU format r8uint) at 320x240 - not full RGBA yet. They stay
// palette-native: integer texture reads, no averaging into fake in-between colors.
//
// Next the engine runs palette LUT resolve plus upscale: each index becomes a real RGBA
// color and the image grows to the canvas size (here 1280x960). Display-tier effects
// (e.g. BarrelDistortion, Scanlines) run on that RGBA output. Crucially, BarrelDistortion
// does NOT bend the curve on the 320x240 index grid - lines stay smooth instead of
// breaking into stair-steps.
//
// HOW THE GLITCH STATE MACHINE WORKS
// We keep two counters:
//   - glitchCooldown:  ticks remaining until the NEXT glitch starts.
//   - glitchTicksLeft: ticks remaining in the CURRENT glitch (0 means "no glitch right now").
// Every frame we count one down. When `glitchTicksLeft` runs out we decrement `glitchCooldown`.
// When `glitchCooldown` runs out we roll a new glitch (random type, random duration, random
// strength) and reset `glitchTicksLeft` to that duration. Each burst type drives different
// effect uniforms.
//
// SOFTWARE FALLBACK
// If the browser uses the Canvas 2D software renderer (WebGPU missing or
// ?backend=software), post-process effects are not available. The terminal
// scene, boot animation, and status block still run; only the CRT stack is skipped.
// An on-screen note drawn with the shared UI kit explains the reduced mode.
//
// HOW THE BITMAP FONT COLORS WORK
// The font is loaded as a sprite sheet of WHITE glyph pixels. We "indexize" the sheet
// against our palette, which replaces each white pixel with the index of the white slot
// (C_WHITE). When we draw with BT.printFont(font, pos, text, offset), the engine adds
// `offset` to that index. So passing `C_GREEN - C_WHITE` shifts every glyph pixel from
// the white slot to the green slot. Same trick the Sprite Effects demo uses for tints.

// Pull in everything we need from the engine. The new two-tier post-process API exposes
// each individual effect as its own class so we can compose them however we like.
import {
    class BarrelDistortion
Barrel distortion that warps UVs outward from the screen center. Display-tier: operates on the upscaled output. Applying this in the pixel tier (logical 320x240) discretizes the curve onto the source texel grid, which CSS upscale then magnifies into visible step artifacts. At output resolution the curve has enough resolution to express smoothly. The math comes from Timothy Lottes's public-domain `crt-lottes.glsl`: `warp(uv) = uv + delta * d2 * curvature` where `delta = uv - 0.5` and `d2 = dot(delta, delta)`.
@since1.0.3
BarrelDistortion
,
class BitmapFont
Bitmap font backed by a sprite-sheet texture atlas. The class is responsible for: - loading `.btfont` metadata and its referenced texture - exposing glyph lookup by character or character code - measuring string widths with a small reusable cache - providing the underlying {@link SpriteSheet } used for rendering glyph quads
@since0.1.0@changed1.5.0 `getGlyph` / `getGlyphByCode` (and `measureText`, which now shares their lookup) substitute a font-defined fallback glyph for a missing character instead of returning `null`, when the font's glyph map has an entry keyed by `U+FFFD`.@changed1.7.0 Added the `codePoints` getter, returning every Unicode code point the font defines a glyph for (ascending, derived live from the same glyph map `getGlyph()` and `hasGlyph()` read).
BitmapFont
,
class Bloom
Single-pass box-blur bloom. Samples a 5x5 neighborhood (25 taps) around each fragment, averages, then mixes with the original color by {@link glow } . {@link spread } scales the texel offset so the bloom radius can be tuned independently of the source resolution. Display-tier: bloom mixes neighboring pixels into intermediate hues that are not in the active palette. Running it in pixel space would violate the palette-pixel aesthetic; running it on the upscaled output reads as the warm phosphor glow of an old monitor instead. The implementation matches the original PipBoy bloom shader. A future optimization would be a two-pass separable Gaussian (5 + 5 = 10 taps); add it once a GPU perf test demands it.
@since1.0.3
Bloom
,
function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
,
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
,
class ChromaticAberration
RGB channel offset that simulates lens chromatic aberration: red samples left of the fragment, blue samples right, green stays centered. Display-tier: spreads color along the lens axis. At logical resolution the single-pixel offset is too coarse and reads as a glitch instead of a soft fringe.
@since1.0.3
ChromaticAberration
,
class Color32
Mutable 32-bit RGBA color value with 8-bit channels.
@since0.1.0
Color32
,
class Flicker
Brightness multiplier - the simplest CRT animation knob. Demos drive {@link amount } per frame to simulate flicker (e.g. with `0.95 + sin(t) * 0.05`). The effect is intentionally trivial so the demo controls the pattern; for procedural noise-driven flicker, combine with the {@link Noise } effect. Display-tier.
@since1.0.3
Flicker
,
class Interference
Per-row horizontal jitter that simulates analog signal interference. Each output row gets a deterministic random horizontal offset seeded by row index and time. Row offsets are stable for one frame and re-seed every frame, producing a buzzing-noise feel. Display-tier. Drives jitter from {@link time } ; demos typically pass `BT.ticks / BT.targetFPS`.
@since1.0.3
Interference
,
class Noise
Additive per-pixel pseudo-random noise. Reseeds each frame from {@link time } so the noise pattern animates. Display-tier.
@since1.0.3
Noise
,
class PixelGlitch
Chunky pixel-aligned horizontal glitch: every Nth row of source pixels gets a random horizontal shift. Shifts snap to integer source-pixel offsets so palette indices move whole-texel (no RGB resampling). Pixel-tier: runs on the logical `r8uint` framebuffer (palette indices).
@since1.0.3
PixelGlitch
,
class Rect2i
Integer rectangle for pixel-perfect bounds and regions. Used throughout the engine for sprite regions, display-space bounds, and zero-allocation geometry helpers. Both convenience getters and allocation-free `*To()` helpers are provided so callers can choose between readability and hot-path efficiency.
@since0.1.0
Rect2i
,
class RGBMask
CRT shadow mask: per-pixel R/G/B vertical-stripe pattern with darkened cell borders, simulating the phosphor grille of an aperture-grille CRT. Display-tier: at output resolution there are enough output pixels per mask cell to read as colored stripes. The cell pitch (in output pixels) is the {@link size } parameter. Math is a direct WGSL port of the libretro `crt-lottes.glsl` mask code.
@since1.0.3
RGBMask
,
class RollLine
Slowly scrolling vertical interference band that brightens a horizontal stripe of the image. Combination of three cosines + smoothstep gives the stripe a soft top/bottom edge. Display-tier. Demo drives {@link time } (typically `BT.ticks / BT.targetFPS`).
@since1.0.3
RollLine
,
class Scanlines
CRT scanlines: alternating bright/dark horizontal bands aligned to the source vertical resolution. Display-tier: at output resolution there is enough vertical pixels for scanlines to read as alternating bright/dark bands. At logical 320x240 the Gaussian weight quantizes to one of two values per source row and you lose the smooth fade.
@since1.0.3
Scanlines
,
class Vector2i
Integer 2D vector for pixel-perfect positioning. Used for points, sizes, directions, and camera offsets throughout the engine. The API includes both allocation-free `*To()` / `*InPlace()` variants and convenience methods that return new vectors.
@since0.1.0
Vector2i
,
class Vignette
Edge-darkening vignette: smooth radial fade from full brightness at the center to black at the corners. Display-tier: applies to the whole simulated screen, not the underlying pixel art.
@since1.0.3
Vignette
,
} from 'blit386'; import { import ABERRATION_BASEABERRATION_BASE, import applyGlitchUniformsapplyGlitchUniforms, import FLICKER_BASEFLICKER_BASE, import GLITCH_ACTIVE_MAXGLITCH_ACTIVE_MAX, import GLITCH_ACTIVE_MINGLITCH_ACTIVE_MIN, import GLITCH_COOLDOWN_MAXGLITCH_COOLDOWN_MAX, import GLITCH_COOLDOWN_MINGLITCH_COOLDOWN_MIN, import GLITCH_INTENSITY_MAXGLITCH_INTENSITY_MAX, import GLITCH_INTENSITY_MINGLITCH_INTENSITY_MIN, import GLITCH_TYPES_CHROMAGLITCH_TYPES_CHROMA, import NOISE_BASENOISE_BASE, import PIXEL_GLITCH_BAND_HEIGHTPIXEL_GLITCH_BAND_HEIGHT, import resetGlitchUniformsresetGlitchUniforms, } from './shared/crt-glitch.js'; 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'; // The internal pixel resolution of the demo. Small numbers keep the pixel art look. const const DISPLAY_W: 320DISPLAY_W = 320; const const DISPLAY_H: 240DISPLAY_H = 240; // Output resolution. Setting this 4x larger than the logical size gives the display-tier // effects (barrel curve, scanlines, RGB mask) enough output pixels to render smoothly, // and turns each logical pixel into a clean 4x4 block on screen. // // IMPORTANT: this is the SCREEN size we present at, NOT the pixel-art size. The game still // draws palette indices into a 320x240 logical buffer. Pixel-tier effects touch that index // buffer; then resolve + upscale turns it into RGBA at this size; display-tier effects run // on the RGBA image. const const OUTPUT_W: 1280OUTPUT_W = 1280; const const OUTPUT_H: 960OUTPUT_H = 960; // We update at this rate (60 ticks per second). The glitch state machine measures // time in ticks, so changing this also changes how often glitches trigger. const const TARGET_FPS: 60TARGET_FPS = 60; // Palette indices. Index 0 is always transparent. Slot order matters: the bitmap font // is indexized against C_WHITE, and we shift that index up to reach the colored slots. const const C_BG: 1C_BG = 1; // Almost-black: the inside of the screen. Used by BT.clear. const const C_WHITE: 2C_WHITE = 2; // Pure white: the slot the font's pixels indexize to. Never drawn directly. const const C_GREEN_DIM: 3C_GREEN_DIM = 3; // Faded green: low-priority text and chrome. const const C_GREEN: 4C_GREEN = 4; // PipBoy green: the main text color. const const C_GREEN_BRIGHT: 5C_GREEN_BRIGHT = 5; // Hot green: highlights and the cursor. const const C_AMBER: 6C_AMBER = 6; // Amber: warning numbers (a wink at the alternative PipBoy palette). // Layout for the terminal text. We pre-compute everything in pixels so render() // stays a list of draw calls rather than a math exercise. const const TEXT_LEFT: 14TEXT_LEFT = 14; const const TEXT_TOP: 18TEXT_TOP = 18; const const LINE_HEIGHT: 14LINE_HEIGHT = 14; // How many ticks each "boot line" takes to type out. 6 ticks at 60 FPS = 100ms per line spacer. // Each character within a line is revealed every `BOOT_TICKS_PER_CHAR` ticks. const const BOOT_LINE_SPACER_TICKS: 6BOOT_LINE_SPACER_TICKS = 6; const const BOOT_TICKS_PER_CHAR: 2BOOT_TICKS_PER_CHAR = 2; // The boot sequence. Each entry is one line that appears letter-by-letter. // Keep this short - with too many lines the demo never finishes booting. // Tip: read this top-to-bottom to imagine how a real PipBoy might wake up. const const BOOT_LINES: {}BOOT_LINES = [ 'ROBCO INDUSTRIES (TM) PIP-BOY 3000', 'COPYRIGHT 2075 ROBCO IND.', '', 'INITIATING BOOT SEQUENCE...', 'LOADING FIRMWARE..............[ OK ]', 'CHECKING RAD SENSORS..........[ OK ]', 'GEIGER COUNTER................[ OK ]', 'VAULT-TEC LINK................[FAIL]', 'FALLBACK: LOCAL CACHE.........[ OK ]', '', '> WELCOME, RESIDENT 101', ]; // Status block contents, drawn AFTER the boot sequence finishes. These never animate; // they sit on the screen so the CRT effect has something colorful to chew on. const const STATUS_LINES: {}STATUS_LINES = [ ['HP', '125 / 125', 'green'], ['AP', ' 75 / 75', 'green'], ['RAD', ' 3 / 1000', 'dim'], ['CAPS', ' 1248', 'amber'], ['WEIGHT', ' 84 / 200', 'green'], ]; // The cursor blinks: ON for half a second, OFF for half a second. // 30 ticks at 60 FPS = 0.5 seconds. The cursor is just a bright square. const const CURSOR_BLINK_TICKS: 30CURSOR_BLINK_TICKS = 30; /** @typedef {import('blit386').IBTDemo} IBTDemo */ /** @typedef {import('blit386').HardwareSettings} HardwareSettings */ /** * Maps a status-line "color name" (kept as a string in STATUS_LINES so the table * is human-readable) to the matching palette slot. * * @param {string} name * @returns {number} */ function function colorSlot(name: string): number
Maps a status-line "color name" (kept as a string in STATUS_LINES so the table is human-readable) to the matching palette slot.
@paramname@returns
colorSlot
(name: string
@paramname
name
) {
if (name: string
@paramname
name
=== 'amber') {
return const C_AMBER: 6C_AMBER; } if (name: string
@paramname
name
=== 'dim') {
return const C_GREEN_DIM: 3C_GREEN_DIM; } return const C_GREEN: 4C_GREEN; } /** * PipBoy-style terminal showcase. Renders a tiny boot sequence + status block in green * bitmap text, then drives a JS-side glitch state machine that mutates the post-process * effect uniforms each frame to produce occasional glitches. * * The effect stack is built explicitly here so each piece is visible: * - PixelGlitch (pixel tier) on the logical r8uint index buffer for chunky band shifts. * - Palette resolve + upscale to RGBA at canvas size (handled by the engine). * - BarrelDistortion + ChromaticAberration + Interference + RollLine + Scanlines + * RGBMask + Vignette + Noise + Flicker + Bloom (display tier) on that RGBA for the * physical CRT simulation. * * If you want the same look in one line of code, use `BT.preset.crtPipBoy()` instead. * * @implements {IBTDemo} */ class class Demo
PipBoy-style terminal showcase. Renders a tiny boot sequence + status block in green bitmap text, then drives a JS-side glitch state machine that mutates the post-process effect uniforms each frame to produce occasional glitches. The effect stack is built explicitly here so each piece is visible: - PixelGlitch (pixel tier) on the logical r8uint index buffer for chunky band shifts. - Palette resolve + upscale to RGBA at canvas size (handled by the engine). - BarrelDistortion + ChromaticAberration + Interference + RollLine + Scanlines + RGBMask + Vignette + Noise + Flicker + Bloom (display tier) on that RGBA for the physical CRT simulation. If you want the same look in one line of code, use `BT.preset.crtPipBoy()` instead.
@implementsIBTDemo
Demo
{
/** True once WebGPU post-process effects were installed in init(). */ Demo.effectsAvailable: boolean
True once WebGPU post-process effects were installed in init().
effectsAvailable
= false;
/** Tick count captured when the boot sequence started. */ Demo.bootStartTick: number
Tick count captured when the boot sequence started.
bootStartTick
= 0;
/** Ticks elapsed since bootStartTick - refreshed every update(). */ Demo.ticksSinceBoot: number
Ticks elapsed since bootStartTick - refreshed every update().
ticksSinceBoot
= 0;
/** True after the first frame where the boot sequence is fully visible. */ Demo.bootTagged: boolean
True after the first frame where the boot sequence is fully visible.
bootTagged
= false;
/** Ticks remaining until the next glitch burst starts. */ Demo.glitchCooldown: number
Ticks remaining until the next glitch burst starts.
glitchCooldown
= 0;
/** Ticks remaining in the current glitch burst (0 means idle). */ Demo.glitchTicksLeft: number
Ticks remaining in the current glitch burst (0 means idle).
glitchTicksLeft
= 0;
/** Full duration of the current burst, used to build the envelope. */ Demo.glitchDuration: number
Full duration of the current burst, used to build the envelope.
glitchDuration
= 0;
/** Active glitch personality key, or 'none' when idle. */ Demo.glitchType: string
Active glitch personality key, or 'none' when idle.
glitchType
= 'none';
/** Peak intensity of the current burst (0..1), scaled by the envelope each tick. */ Demo.glitchPeak: number
Peak intensity of the current burst (0..1), scaled by the envelope each tick.
glitchPeak
= 0;
/** * Pixel-art logical size, 4x drawing buffer for display-tier CRT, overlay tuned for terminal look. * * @returns {Partial<HardwareSettings>} */ Demo.configure(): Partial<HardwareSettings>
Pixel-art logical size, 4x drawing buffer for display-tier CRT, overlay tuned for terminal look.
@returns
configure
() {
return { // The internal canvas is pixel-art sized. Game logic and draws write palette // indices into an r8uint buffer at this resolution. PixelGlitch sees that buffer. // Display-tier CRT effects run later on RGBA after resolve + upscale below. displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const DISPLAY_W: 320DISPLAY_W, const DISPLAY_H: 240DISPLAY_H),
// drawingBufferSize is REQUIRED to enable the display tier of the post-process // chain. Without it, there is no canvas-sized RGBA surface for display-tier // shaders, so BT.effectAdd will throw for those effects. We pick a clean // 4x integer scale so each logical pixel maps to a 4x4 output block. drawingBufferSize: Vector2idrawingBufferSize: new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const OUTPUT_W: 1280OUTPUT_W, const OUTPUT_H: 960OUTPUT_H),
// Let the demos layout show the full drawing buffer (default CSS cap is 960x720). maxCanvasSize: Vector2imaxCanvasSize: new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const OUTPUT_W: 1280OUTPUT_W, const OUTPUT_H: 960OUTPUT_H),
// 'nearest' keeps the pixel-art crispness through the upscale; 'linear' would // soften it like an old TV signal. Try changing this to 'linear' to see the // softer look. outputUpscaleFilter: stringoutputUpscaleFilter: 'nearest', targetFPS: numbertargetFPS: const TARGET_FPS: 60TARGET_FPS, // Hide the little "~" toggle hint that normally sits in the bottom-left // corner. This is a full-screen CRT terminal, so a stray hint icon would // break the illusion (and show up in the curved-glass post-process). The // stats overlay still opens: press the Backquote key (`) to toggle the // full dev HUD, press ` again to hide it. The hint is only hidden, not // disabled. isOverlayToggleHintVisible: booleanisOverlayToggleHintVisible: false,
overlayStyle: {
    barPaletteIndex: number;
    textPaletteIndex: number;
    gapPaletteIndex: number;
}
overlayStyle
: {
barPaletteIndex: numberbarPaletteIndex: const C_BG: 1C_BG, textPaletteIndex: numbertextPaletteIndex: const C_GREEN: 4C_GREEN, gapPaletteIndex: numbergapPaletteIndex: const C_BG: 1C_BG, }, isOverlayTimingChartEnabled: booleanisOverlayTimingChartEnabled: true, overlayTimingChartDiagnostics: stringoverlayTimingChartDiagnostics: 'rich', isOverlayRendererDiagnosticsBarEnabled: booleanisOverlayRendererDiagnosticsBarEnabled: true,
overlayTimingChartStyle: {
    updateBarPaletteIndex: number;
    renderBarPaletteIndex: number;
    warningPaletteIndex: number;
    errorPaletteIndex: number;
    tagPaletteIndex: number;
}
overlayTimingChartStyle
: {
updateBarPaletteIndex: numberupdateBarPaletteIndex: const C_GREEN: 4C_GREEN, renderBarPaletteIndex: numberrenderBarPaletteIndex: const C_AMBER: 6C_AMBER, warningPaletteIndex: numberwarningPaletteIndex: const C_AMBER: 6C_AMBER, errorPaletteIndex: numbererrorPaletteIndex: const C_GREEN_BRIGHT: 5C_GREEN_BRIGHT, tagPaletteIndex: numbertagPaletteIndex: const C_GREEN_DIM: 3C_GREEN_DIM, }, }; } /** * @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.
@returns
init
() {
// Step 1: build the palette // Six scene colors, all in the low slots. The palette is 256 entries long so the // shared UI theme can live in the high slots (240-251), far away from the greens. 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) => Palette
Creates a standalone palette instance.
@since1.0.3@paramsize - Palette size. Defaults to 256 colors.@returnsNew mutable palette.
paletteCreate
(256);
const palette: Palettepalette.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_BG: 1C_BG, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(8, 14, 8, 255)); // Almost-black with green tint
const palette: Palettepalette.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_WHITE: 2C_WHITE, class Color32
Mutable 32-bit RGBA color value with 8-bit channels.
@since0.1.0
Color32
.Color32.white: Color32
Pure white color (255, 255, 255, 255). Cached frozen singleton - do not modify.
@returnsThe shared white Color32 instance.
white
); // Slot the font glyph pixels resolve to
const palette: Palettepalette.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_GREEN_DIM: 3C_GREEN_DIM, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(40, 100, 60, 255)); // Faded green
const palette: Palettepalette.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_GREEN: 4C_GREEN, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(80, 200, 110, 255)); // PipBoy green
const palette: Palettepalette.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_GREEN_BRIGHT: 5C_GREEN_BRIGHT, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(170, 255, 190, 255)); // Hot green highlights
const palette: Palettepalette.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_AMBER: 6C_AMBER, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(220, 180, 60, 255)); // Vault-Tec amber accent
// Install the shared UI kit colors into slots 240-251. In this demo the kit only // draws the software-fallback note; the terminal itself stays hand-rolled so the // phosphor-green PipBoy look is untouched. 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) => void
Stores the active engine palette. Use this to swap the **entire palette** (e.g. switch between a day and night theme). After this call the renderer uploads the new palette uniform on the next frame. **Palette-value swap (change what a slot looks like):** mutate the live {@link BT.palette } in place with `palette.set(slot, newColor)`. The renderer uploads dirty slots on the next frame; no `paletteSet()` or {@link BT.spritesRefresh } needed. **Palette-layout swap (same colors, different slot positions):** build a new palette with the same colors at new indices, call `paletteSet()`, then call {@link BT.spritesRefresh } so every sprite sheet re-maps its original RGBA pixels against the new slot layout.
@since1.0.3@parampalette - Palette to make active.
paletteSet
(const palette: Palettepalette);
// Step 2: load the bitmap font // PragmataPro is a monospaced programming font - a perfect fit for a fictional // terminal. The .btfont is a BLIT386 bitmap font: a PNG glyph atlas plus a // small JSON describing each character's bounds. this.Demo.font: anyfont = await class BitmapFont
Bitmap font backed by a sprite-sheet texture atlas. The class is responsible for: - loading `.btfont` metadata and its referenced texture - exposing glyph lookup by character or character code - measuring string widths with a small reusable cache - providing the underlying {@link SpriteSheet } used for rendering glyph quads
@since0.1.0@changed1.5.0 `getGlyph` / `getGlyphByCode` (and `measureText`, which now shares their lookup) substitute a font-defined fallback glyph for a missing character instead of returning `null`, when the font's glyph map has an entry keyed by `U+FFFD`.@changed1.7.0 Added the `codePoints` getter, returning every Unicode code point the font defines a glyph for (ascending, derived live from the same glyph map `getGlyph()` and `hasGlyph()` read).
BitmapFont
.BitmapFont.load(url: string): Promise<BitmapFont>
Loads a bitmap font from a `.btfont` JSON file. The font descriptor can reference either an embedded PNG data URI (`data:image/png;base64,...`) or a texture file path relative to the font JSON file.
@paramurl - Path to the .btfont file.@returnsLoaded bitmap font instance.@throwsError if the font descriptor or texture cannot be loaded.
load
('/fonts/PragmataPro14.btfont');
// Step 3: indexize the font // The font is loaded as a sprite sheet of white pixels. "Indexize" walks every // pixel, looks up its color in the palette, and replaces the pixel with the // matching slot index. After this, the font's pixels carry index = C_WHITE, and // BT.printFont can shift that index by an offset to recolor the glyphs at draw time. this.Demo.font: anyfont.getSpriteSheet().indexize(const palette: Palettepalette); // Post-process (pixel + display tiers) needs WebGPU. Software mode skips this block. this.Demo.effectsAvailable: boolean
True once WebGPU post-process effects were installed in init().
effectsAvailable
= import isAvailableisAvailable();
if (!this.Demo.effectsAvailable: boolean
True once WebGPU post-process effects were installed in init().
effectsAvailable
) {
this.Demo.bootStartTick: number
Tick count captured when the boot sequence started.
bootStartTick
=
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
;
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) => void
Places 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.
@since1.1.0@paramlabel - Short event name (for example `'Round start'`).
assignTag
('Software renderer');
return true; } // Step 4: pixel-tier effect (chunky glitch) // PixelGlitch reads/writes the logical index buffer (320x240 r8uint) so band shifts // stay palette-native. If the same shift ran after resolve + upscale, each band // would span multiple output pixels and lose the chunky retro look. this.Demo.pixelGlitch: PixelGlitch | undefinedpixelGlitch = new new PixelGlitch(): PixelGlitch
Chunky pixel-aligned horizontal glitch: every Nth row of source pixels gets a random horizontal shift. Shifts snap to integer source-pixel offsets so palette indices move whole-texel (no RGB resampling). Pixel-tier: runs on the logical `r8uint` framebuffer (palette indices).
@since1.0.3
PixelGlitch
();
this.Demo.pixelGlitch: PixelGlitchpixelGlitch.PixelGlitch.bandHeight: number
Height of each glitch band in source pixels. Each band gets a single shift value, so larger bands produce chunkier glitches.
bandHeight
= import PIXEL_GLITCH_BAND_HEIGHTPIXEL_GLITCH_BAND_HEIGHT; // height of each glitch band in source pixels
this.Demo.pixelGlitch: PixelGlitchpixelGlitch.PixelGlitch.intensity: number
Glitch strength in `[0, 1]`. Scales the per-band horizontal shift magnitude. Bands are shifted when their hash exceeds ~0.85 (~15% of bands). `0` disables.
intensity
= 0; // 0 = no glitch right now (state machine will spike it)
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) => void
Appends 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.
@since1.0.3@parameffect - Effect instance to append. When the engine is not ready, shows a canvas error instead of throwing.
effectAdd
(this.Demo.pixelGlitch: PixelGlitchpixelGlitch); // tier='pixel' on the effect routes this automatically
// Step 5: display-tier stack // Order matters: barrel first (warps the UVs the rest of the chain inherits), // then color/signal artifacts, then scanlines and mask, then noise, then flicker, // and finally bloom on top of the modulated image. // Pincushion barrel distortion: simulates the curved glass of a CRT tube. Because // this runs AFTER palette resolve + upscale, the curve is computed at 1280x960 // lines stay smooth. (Bending earlier on the 320x240 grid would quantize the curve // and produce visible step artifacts on diagonals.) this.Demo.barrel: BarrelDistortion | undefinedbarrel = new new BarrelDistortion(): BarrelDistortion
Barrel distortion that warps UVs outward from the screen center. Display-tier: operates on the upscaled output. Applying this in the pixel tier (logical 320x240) discretizes the curve onto the source texel grid, which CSS upscale then magnifies into visible step artifacts. At output resolution the curve has enough resolution to express smoothly. The math comes from Timothy Lottes's public-domain `crt-lottes.glsl`: `warp(uv) = uv + delta * d2 * curvature` where `delta = uv - 0.5` and `d2 = dot(delta, delta)`.
@since1.0.3
BarrelDistortion
();
this.Demo.barrel: BarrelDistortionbarrel.BarrelDistortion.curvature: number
Curvature strength. Typical values: - `0.02` - subtle, like a flat-screen monitor. - `0.05` - moderate desktop CRT. - `0.10` - heavy small CRT or pocket TV.
curvature
= 0.05; // small tube; 0.10 would be a tiny pocket TV
// Chromatic aberration: shifts the red and blue channels horizontally. Cheap CRT // optics produce a tiny version of this naturally. this.Demo.aberration: ChromaticAberration | undefinedaberration = new new ChromaticAberration(): ChromaticAberration
RGB channel offset that simulates lens chromatic aberration: red samples left of the fragment, blue samples right, green stays centered. Display-tier: spreads color along the lens axis. At logical resolution the single-pixel offset is too coarse and reads as a glitch instead of a soft fringe.
@since1.0.3
ChromaticAberration
();
this.Demo.aberration: ChromaticAberrationaberration.ChromaticAberration.aberration: number
Channel offset in display-chain (output) pixels. Reasonable values are `0.5` to `3.0`. Set to `0` to disable.
aberration
= import ABERRATION_BASEABERRATION_BASE;
// Interference: per-row horizontal jitter that simulates analog signal noise. // Set to 0 at rest so the screen is calm between glitch bursts; the state // machine spikes this during an 'interference' burst. this.Demo.interference: Interference | undefinedinterference = new new Interference(): Interference
Per-row horizontal jitter that simulates analog signal interference. Each output row gets a deterministic random horizontal offset seeded by row index and time. Row offsets are stable for one frame and re-seed every frame, producing a buzzing-noise feel. Display-tier. Drives jitter from {@link time } ; demos typically pass `BT.ticks / BT.targetFPS`.
@since1.0.3
Interference
();
this.Demo.interference: Interferenceinterference.Interference.amount: number
Maximum horizontal offset as a UV fraction (e.g. `0.06` shifts the row by up to ~6% of the image width). Set to `0` to disable.
amount
= 0;
// Roll line: a horizontal bright band slowly scrolls down the screen, like an // old TV that isn't quite sync'd. this.Demo.rollLine: RollLine | undefinedrollLine = new new RollLine(): RollLine
Slowly scrolling vertical interference band that brightens a horizontal stripe of the image. Combination of three cosines + smoothstep gives the stripe a soft top/bottom edge. Display-tier. Demo drives {@link time } (typically `BT.ticks / BT.targetFPS`).
@since1.0.3
RollLine
();
this.Demo.rollLine: RollLinerollLine.RollLine.amount: number
Roll line amplitude (mix factor onto a brightness boost).
amount
= 0.1; // strength of the bright band
this.Demo.rollLine: RollLinerollLine.RollLine.speed: number
Scroll speed multiplier; final scroll velocity = `time * speed`.
speed
= 1.0; // how fast it scrolls
// Scanlines: alternating bright/dark horizontal bands aligned to source pixel rows. this.Demo.scanlines: Scanlines | undefinedscanlines = new new Scanlines(): Scanlines
CRT scanlines: alternating bright/dark horizontal bands aligned to the source vertical resolution. Display-tier: at output resolution there is enough vertical pixels for scanlines to read as alternating bright/dark bands. At logical 320x240 the Gaussian weight quantizes to one of two values per source row and you lose the smooth fade.
@since1.0.3
Scanlines
();
this.Demo.scanlines: Scanlinesscanlines.Scanlines.amount: number
Scanline mix amount in `[0, 1]`. 0 disables.
amount
= 0.55; // mix factor: 0 disables, 1 full effect
this.Demo.scanlines: Scanlinesscanlines.Scanlines.strength: number
Negative gaussian falloff parameter for scanline brightness. More negative values produce sharper dark bands. PipBoy reference: `-8.0`.
strength
= -8; // sharper bands at more negative values
// Match scanline density to logical rows so each source pixel row gets one // bright/dark cycle. Without this, the scanlines would map to OUTPUT rows and // be 4x denser than the underlying pixel art. this.Demo.scanlines: Scanlinesscanlines.Scanlines.density: number
Number of scanline cycles vertically. Should match the demo's logical source vertical resolution so each "source pixel row" maps to one scanline cycle. Defaults to `240`, the most common pixel-art height. Set to e.g. `200` for VGA-style 320x200 games or `144` for Game Boy resolution.
density
= const DISPLAY_H: 240DISPLAY_H;
// RGB shadow mask: per-pixel R/G/B vertical-stripe pattern with darkened cell // borders, simulating the phosphor grille of an aperture-grille CRT. this.Demo.mask: RGBMask | undefinedmask = new new RGBMask(): RGBMask
CRT shadow mask: per-pixel R/G/B vertical-stripe pattern with darkened cell borders, simulating the phosphor grille of an aperture-grille CRT. Display-tier: at output resolution there are enough output pixels per mask cell to read as colored stripes. The cell pitch (in output pixels) is the {@link size } parameter. Math is a direct WGSL port of the libretro `crt-lottes.glsl` mask code.
@since1.0.3
RGBMask
();
this.Demo.mask: RGBMaskmask.RGBMask.intensity: number
Mask brightness mix amount in `[0, 1]`. 0 hides the mask.
intensity
= 0.18; // 0 hides the mask, 1 = max influence
this.Demo.mask: RGBMaskmask.RGBMask.size: number
Mask cell pitch in output (display-chain) pixels. Smaller = denser mask.
size
= 6; // mask cell pitch in source pixels
this.Demo.mask: RGBMaskmask.RGBMask.border: number
Border darkening within each mask cell. 0 disables, 1 strong.
border
= 0.5; // border darkening within each cell
// Vignette: edge darkening to sell the curved-glass illusion. this.Demo.vignette: Vignette | undefinedvignette = new new Vignette(): Vignette
Edge-darkening vignette: smooth radial fade from full brightness at the center to black at the corners. Display-tier: applies to the whole simulated screen, not the underlying pixel art.
@since1.0.3
Vignette
();
this.Demo.vignette: Vignettevignette.Vignette.amount: number
Vignette darkening exponent. Higher values produce a stronger vignette with a sharper falloff. PipBoy reference: `0.2`. Set to `0` to disable.
amount
= 0.35;
// Per-frame noise: subtle film grain that animates each frame. this.Demo.noise: Noise | undefinednoise = new new Noise(): Noise
Additive per-pixel pseudo-random noise. Reseeds each frame from {@link time } so the noise pattern animates. Display-tier.
@since1.0.3
Noise
();
this.Demo.noise: Noisenoise.Noise.amount: number
Noise amplitude as a `[-amount, +amount]` additive perturbation on each channel. Reasonable values are `0.005` to `0.05`. Set to `0` to disable.
amount
= import NOISE_BASENOISE_BASE;
// Flicker: a brightness multiplier driven by the glitch state machine. this.Demo.flicker: Flicker | undefinedflicker = new new Flicker(): Flicker
Brightness multiplier - the simplest CRT animation knob. Demos drive {@link amount } per frame to simulate flicker (e.g. with `0.95 + sin(t) * 0.05`). The effect is intentionally trivial so the demo controls the pattern; for procedural noise-driven flicker, combine with the {@link Noise } effect. Display-tier.
@since1.0.3
Flicker
();
this.Demo.flicker: Flickerflicker.Flicker.amount: number
Brightness multiplier. `1` is unmodulated; values below `1` darken the frame. The demo typically updates this each frame from a sin wave or random source.
amount
= import FLICKER_BASEFLICKER_BASE;
// Bloom: a soft glow on bright pixels - the warm phosphor halo of an old monitor. // Stacked LAST so the bloom sees the final post-CRT image. this.Demo.bloom: Bloom | undefinedbloom = new new Bloom(): Bloom
Single-pass box-blur bloom. Samples a 5x5 neighborhood (25 taps) around each fragment, averages, then mixes with the original color by {@link glow } . {@link spread } scales the texel offset so the bloom radius can be tuned independently of the source resolution. Display-tier: bloom mixes neighboring pixels into intermediate hues that are not in the active palette. Running it in pixel space would violate the palette-pixel aesthetic; running it on the upscaled output reads as the warm phosphor glow of an old monitor instead. The implementation matches the original PipBoy bloom shader. A future optimization would be a two-pass separable Gaussian (5 + 5 = 10 taps); add it once a GPU perf test demands it.
@since1.0.3
Bloom
();
this.Demo.bloom: Bloombloom.Bloom.spread: number
Texel offset multiplier for the box-blur kernel.
spread
= 3.0; // size of the bloom kernel
this.Demo.bloom: Bloombloom.Bloom.glow: number
Mix factor between the original sample and the blurred neighborhood.
glow
= 0.18; // mix factor onto the original pixel
// Register all display-tier effects in order. for (const const fx: anyfx of [ this.Demo.barrel: BarrelDistortionbarrel, this.Demo.aberration: ChromaticAberrationaberration, this.Demo.interference: Interferenceinterference, this.Demo.rollLine: RollLinerollLine, this.Demo.scanlines: Scanlinesscanlines, this.Demo.mask: RGBMaskmask, this.Demo.vignette: Vignettevignette, this.Demo.noise: Noisenoise, this.Demo.flicker: Flickerflicker, this.Demo.bloom: Bloombloom, ]) {
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) => void
Appends 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.
@since1.0.3@parameffect - Effect instance to append. When the engine is not ready, shows a canvas error instead of throwing.
effectAdd
(const fx: anyfx);
} // Step 6: boot animation timer // We use ticks instead of wall-clock so the boot animation stays deterministic // even if the browser frame rate hiccups. this.Demo.bootStartTick: number
Tick count captured when the boot sequence started.
bootStartTick
=
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
;
// Step 7: glitch state machine state // See the file header for what each field means. We start in a long cooldown so the first burst doesn't fire on // frame 1. // BT.random is the engine's shared random number generator. Its int() method returns a whole number from the // first value up to (but not including) the second, so this waits a random number of ticks before the // first burst. this.Demo.glitchCooldown: number
Ticks remaining until the next glitch burst starts.
glitchCooldown
=
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
.random: Random
Default engine PRNG (live reference - not a copy). Time-seeded when the engine singleton is created. Call {@link BT.randomSeed } for a reproducible run. Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.
@since1.5.0@returnsThe shared {@link Random} instance.@exampleBT.randomSeed(42); BT.random.int(150, 420); BT.random.pick(['a', 'b', 'c']);
random
.Random.int(minOrMaxExclusive: number, maxExclusive?: number): number
Returns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).
@paramminOrMaxExclusive - When alone, exclusive upper bound from 0; otherwise inclusive min.@parammaxExclusive - Exclusive upper bound when two arguments are passed.@returnsWhole number in the half-open range.@since1.5.0
int
(import GLITCH_COOLDOWN_MINGLITCH_COOLDOWN_MIN, import GLITCH_COOLDOWN_MAXGLITCH_COOLDOWN_MAX);
this.Demo.glitchTicksLeft: number
Ticks remaining in the current glitch burst (0 means idle).
glitchTicksLeft
= 0; // ticks remaining in the current burst; 0 means "no glitch right now"
this.Demo.glitchDuration: number
Full duration of the current burst, used to build the envelope.
glitchDuration
= 0;
this.Demo.glitchType: string
Active glitch personality key, or 'none' when idle.
glitchType
= 'none';
this.Demo.glitchPeak: number
Peak intensity of the current burst (0..1), scaled by the envelope each tick.
glitchPeak
= 0;
return true; } Demo.update(): void
Called 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
() {
if (!this.Demo.effectsAvailable: boolean
True once WebGPU post-process effects were installed in init().
effectsAvailable
) {
this.Demo.ticksSinceBoot: number
Ticks elapsed since bootStartTick - refreshed every update().
ticksSinceBoot
=
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
- this.Demo.bootStartTick: number
Tick count captured when the boot sequence started.
bootStartTick
;
return; } // 1. Drive the boot animation timer // We don't draw here - render() reads `this.ticksSinceBoot` and computes how many // characters to show. update() just provides time. this.Demo.ticksSinceBoot: number
Ticks elapsed since bootStartTick - refreshed every update().
ticksSinceBoot
=
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
- this.Demo.bootStartTick: number
Tick count captured when the boot sequence started.
bootStartTick
;
// 2. Drive the time-based effects every frame // RollLine, Noise, and Interference all need a wall-clock seconds value to drive // their animations. Convert ticks to seconds so the animation speed is independent // of TARGET_FPS. 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: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
/ const TARGET_FPS: 60TARGET_FPS;
this.Demo.rollLine: RollLine | undefinedrollLine.RollLine.time: number
Wall-clock seconds; demos typically drive this each frame.
time
= const seconds: numberseconds;
this.Demo.noise: Noise | undefinednoise.Noise.time: number
Wall-clock seconds; reseeds the noise each frame.
time
= const seconds: numberseconds;
this.Demo.interference: Interference | undefinedinterference.Interference.time: number
Wall-clock seconds; reseeds the row offsets each frame.
time
= const seconds: numberseconds;
// 3. Drive the glitch state machine this.Demo.stepGlitchMachine(): void
Advances the glitch state machine by one tick. Called from update() once per frame. Either a burst is currently running (count it down and drive the effect uniforms), or the screen is calm (count down the cooldown and roll a fresh burst when it reaches zero).
stepGlitchMachine
();
} Demo.render(): void
Called 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
() {
// Fill the background. Even with the CRT effect on top, this becomes the // "phosphor off" color of every empty cell.
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.clear: (paletteIndex: number) => void
Sets the frame clear color using a palette index. The renderer uses this color when clearing the full display at the start of the next frame.
@since0.1.0@parampaletteIndex - Palette index for the full-screen clear pass.
clear
(const C_BG: 1C_BG);
// Draw the boot sequence one character at a time, line by line. this.Demo.renderBootSequence(): void
Reveals the BOOT_LINES one character at a time. We compute how many ticks have passed and use that to slice the strings in place.
renderBootSequence
();
// Once the boot lines are all visible, draw the status block on the right. if (this.Demo.bootFullyDone(): boolean
Returns true once every boot line has been fully typed out.
bootFullyDone
()) {
if (!this.Demo.bootTagged: boolean
True after the first frame where the boot sequence is fully visible.
bootTagged
) {
this.Demo.bootTagged: boolean
True after the first frame where the boot sequence is fully visible.
bootTagged
= true;
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) => void
Places 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.
@since1.1.0@paramlabel - Short event name (for example `'Round start'`).
assignTag
('Boot done');
} this.Demo.renderStatusBlock(): void
Draws the static "stats" block on the right half of the screen.
renderStatusBlock
();
this.Demo.renderBlinkingCursor(): void
Draws a blinking cursor below the boot text. A bright square that's visible for half a second and then off for half a second. (Old terminals worked exactly like this.)
renderBlinkingCursor
();
} // In software mode the CRT stack is skipped - say so with a kit label. Omitting // ui.panel() keeps the group borderless, so it reads as a single caption line. if (!this.Demo.effectsAvailable: boolean
True once WebGPU post-process effects were installed in init().
effectsAvailable
) {
import uiui.begin(import UI_ANCHORSUI_ANCHORS.BOTTOM_LEFT); import uiui.label(import SOFTWARE_FALLBACK_NOTESOFTWARE_FALLBACK_NOTE, { color: stringcolor: 'warm' }); import uiui.end(); } } /** * Advances the glitch state machine by one tick. Called from update() once * per frame. Either a burst is currently running (count it down and drive * the effect uniforms), or the screen is calm (count down the cooldown and * roll a fresh burst when it reaches zero). */ Demo.stepGlitchMachine(): void
Advances the glitch state machine by one tick. Called from update() once per frame. Either a burst is currently running (count it down and drive the effect uniforms), or the screen is calm (count down the cooldown and roll a fresh burst when it reaches zero).
stepGlitchMachine
() {
if (this.Demo.glitchTicksLeft: number
Ticks remaining in the current glitch burst (0 means idle).
glitchTicksLeft
> 0) {
// We are inside a glitch burst. Build an "envelope": ramps up to glitchPeak, // holds, then ramps down. Sounds fancy - in practice it just makes a sin curve // over the lifetime of the burst (sin from 0 to PI is a nice 0 -> 1 -> 0 hump). const const t: numbert = 1 - this.Demo.glitchTicksLeft: number
Ticks remaining in the current glitch burst (0 means idle).
glitchTicksLeft
/ this.Demo.glitchDuration: number
Full duration of the current burst, used to build the envelope.
glitchDuration
; // 0 at start, 1 at end
const const envelope: anyenvelope = Math.sin(const t: numbert * Math.PI); // 0 -> 1 -> 0 import applyGlitchUniformsapplyGlitchUniforms(this, const envelope: anyenvelope); this.Demo.glitchTicksLeft: number
Ticks remaining in the current glitch burst (0 means idle).
glitchTicksLeft
--;
// When the burst ends, reset the uniforms so the screen calms down. if (this.Demo.glitchTicksLeft: number
Ticks remaining in the current glitch burst (0 means idle).
glitchTicksLeft
=== 0) {
import resetGlitchUniformsresetGlitchUniforms(this); this.Demo.glitchCooldown: number
Ticks remaining until the next glitch burst starts.
glitchCooldown
=
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
.random: Random
Default engine PRNG (live reference - not a copy). Time-seeded when the engine singleton is created. Call {@link BT.randomSeed } for a reproducible run. Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.
@since1.5.0@returnsThe shared {@link Random} instance.@exampleBT.randomSeed(42); BT.random.int(150, 420); BT.random.pick(['a', 'b', 'c']);
random
.Random.int(minOrMaxExclusive: number, maxExclusive?: number): number
Returns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).
@paramminOrMaxExclusive - When alone, exclusive upper bound from 0; otherwise inclusive min.@parammaxExclusive - Exclusive upper bound when two arguments are passed.@returnsWhole number in the half-open range.@since1.5.0
int
(import GLITCH_COOLDOWN_MINGLITCH_COOLDOWN_MIN, import GLITCH_COOLDOWN_MAXGLITCH_COOLDOWN_MAX);
} } else { // No active glitch - count down to the next one. this.Demo.glitchCooldown: number
Ticks remaining until the next glitch burst starts.
glitchCooldown
--;
if (this.Demo.glitchCooldown: number
Ticks remaining until the next glitch burst starts.
glitchCooldown
<= 0) {
// Roll a new burst. pick() draws one item out of a list, like taking a // card off the top of a shuffled deck. float() is the decimal cousin of // int(), for values that are not whole numbers. this.Demo.glitchType: string
Active glitch personality key, or 'none' when idle.
glitchType
=
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
.random: Random
Default engine PRNG (live reference - not a copy). Time-seeded when the engine singleton is created. Call {@link BT.randomSeed } for a reproducible run. Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.
@since1.5.0@returnsThe shared {@link Random} instance.@exampleBT.randomSeed(42); BT.random.int(150, 420); BT.random.pick(['a', 'b', 'c']);
random
.Random.pick<string>(arr: readonly string[]): string
Returns one element chosen uniformly from a non-empty array.
@paramarr - Array to pick from; must contain at least one element.@returnsChosen element.@since1.5.0
pick
(import GLITCH_TYPES_CHROMAGLITCH_TYPES_CHROMA);
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) => void
Places 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.
@since1.1.0@paramlabel - Short event name (for example `'Round start'`).
assignTag
(`Glitch: ${this.Demo.glitchType: string
Active glitch personality key, or 'none' when idle.
glitchType
}`);
this.Demo.glitchDuration: number
Full duration of the current burst, used to build the envelope.
glitchDuration
=
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
.random: Random
Default engine PRNG (live reference - not a copy). Time-seeded when the engine singleton is created. Call {@link BT.randomSeed } for a reproducible run. Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.
@since1.5.0@returnsThe shared {@link Random} instance.@exampleBT.randomSeed(42); BT.random.int(150, 420); BT.random.pick(['a', 'b', 'c']);
random
.Random.int(minOrMaxExclusive: number, maxExclusive?: number): number
Returns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).
@paramminOrMaxExclusive - When alone, exclusive upper bound from 0; otherwise inclusive min.@parammaxExclusive - Exclusive upper bound when two arguments are passed.@returnsWhole number in the half-open range.@since1.5.0
int
(import GLITCH_ACTIVE_MINGLITCH_ACTIVE_MIN, import GLITCH_ACTIVE_MAXGLITCH_ACTIVE_MAX);
this.Demo.glitchTicksLeft: number
Ticks remaining in the current glitch burst (0 means idle).
glitchTicksLeft
= this.Demo.glitchDuration: number
Full duration of the current burst, used to build the envelope.
glitchDuration
;
this.Demo.glitchPeak: number
Peak intensity of the current burst (0..1), scaled by the envelope each tick.
glitchPeak
=
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
.random: Random
Default engine PRNG (live reference - not a copy). Time-seeded when the engine singleton is created. Call {@link BT.randomSeed } for a reproducible run. Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.
@since1.5.0@returnsThe shared {@link Random} instance.@exampleBT.randomSeed(42); BT.random.int(150, 420); BT.random.pick(['a', 'b', 'c']);
random
.Random.float(min: number, max: number): number
Returns the next pseudo-random float in [min, max).
@parammin - Inclusive lower bound.@parammax - Exclusive upper bound.@returnsFloat in [min, max).@since1.5.0
float
(import GLITCH_INTENSITY_MINGLITCH_INTENSITY_MIN, import GLITCH_INTENSITY_MAXGLITCH_INTENSITY_MAX);
// Reset the seed so the shader uses a new band-noise pattern this burst. this.Demo.pixelGlitch: PixelGlitch | undefinedpixelGlitch.PixelGlitch.seed: number
Per-glitch random seed. Change between glitches to vary the band noise pattern.
seed
=
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
.random: Random
Default engine PRNG (live reference - not a copy). Time-seeded when the engine singleton is created. Call {@link BT.randomSeed } for a reproducible run. Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.
@since1.5.0@returnsThe shared {@link Random} instance.@exampleBT.randomSeed(42); BT.random.int(150, 420); BT.random.pick(['a', 'b', 'c']);
random
.Random.float(min: number, max: number): number
Returns the next pseudo-random float in [min, max).
@parammin - Inclusive lower bound.@parammax - Exclusive upper bound.@returnsFloat in [min, max).@since1.5.0
float
(0, 1000);
} } } /** * Wraps BT.printFont so callers pass a target palette slot rather than the * raw offset the engine wants. C_WHITE is where the font's pixels live after * indexize, so the offset to reach `slot` is `slot - C_WHITE`. * * @param {Vector2i} pos * @param {string} text * @param {number} slot - target palette slot index (e.g. C_GREEN) */ Demo.print(pos: Vector2i, text: string, slot: number): void
Wraps BT.printFont so callers pass a target palette slot rather than the raw offset the engine wants. C_WHITE is where the font's pixels live after indexize, so the offset to reach `slot` is `slot - C_WHITE`.
@parampos@paramtext@paramslot - target palette slot index (e.g. C_GREEN)
print
(pos: Vector2i
@parampos
pos
, text: string
@paramtext
text
, slot: number
- target palette slot index (e.g. C_GREEN)
@paramslot - target palette slot index (e.g. C_GREEN)
slot
) {
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.printFont: (font: BitmapFont, pos: Vector2i, text: string, paletteOffset?: number) => void
Draws text with a bitmap font through the indexed sprite pipeline. Supports proportional glyph widths and glyph-level offsets defined by the supplied {@link BitmapFont } . The font's underlying sprite sheet must have been indexized before calling this. Palette offset semantics and out-of-range behavior are identical to {@link BT.drawSprite } .
@since0.1.0@paramfont - Font asset used for rendering.@parampos - Text origin in display coordinates.@paramtext - String to render.@parampaletteOffset - Shift added to every stored glyph index before palette lookup (default 0).
printFont
(this.Demo.font: anyfont, pos: Vector2i
@parampos
pos
, text: string
@paramtext
text
, slot: number
- target palette slot index (e.g. C_GREEN)
@paramslot - target palette slot index (e.g. C_GREEN)
slot
- const C_WHITE: 2C_WHITE);
} /** * Reveals the BOOT_LINES one character at a time. We compute how many ticks have * passed and use that to slice the strings in place. */ Demo.renderBootSequence(): void
Reveals the BOOT_LINES one character at a time. We compute how many ticks have passed and use that to slice the strings in place.
renderBootSequence
() {
let let ticksLeft: numberticksLeft = this.Demo.ticksSinceBoot: number
Ticks elapsed since bootStartTick - refreshed every update().
ticksSinceBoot
;
for (let let i: numberi = 0; let i: numberi < const BOOT_LINES: {}BOOT_LINES.length; let i: numberi++) { const const fullLine: anyfullLine = const BOOT_LINES: {}BOOT_LINES[let i: numberi]; const const y: numbery = const TEXT_TOP: 18TEXT_TOP + let i: numberi * const LINE_HEIGHT: 14LINE_HEIGHT; // Empty lines just consume a small spacer of ticks (so the pause between sections // feels right) and don't draw anything. if (const fullLine: anyfullLine.length === 0) { let ticksLeft: numberticksLeft -= const BOOT_LINE_SPACER_TICKS: 6BOOT_LINE_SPACER_TICKS; continue; } // How many characters of THIS line should be visible? Each char takes // BOOT_TICKS_PER_CHAR ticks. Clamp to [0, length]. const const charsToShow: anycharsToShow = Math.max(0, Math.min(const fullLine: anyfullLine.length, Math.floor(let ticksLeft: numberticksLeft / const BOOT_TICKS_PER_CHAR: 2BOOT_TICKS_PER_CHAR))); if (const charsToShow: anycharsToShow === 0) { // Future line, not yet started. Stop - everything below is also invisible. return; } const const visible: anyvisible = const fullLine: anyfullLine.slice(0, const charsToShow: anycharsToShow); // Pick the color: lines that finished get the brighter green; lines mid-typing // stay dim until they complete. Subtle but adds life. const const slot: 3 | 4slot = const charsToShow: anycharsToShow >= const fullLine: anyfullLine.length ? const C_GREEN: 4C_GREEN : const C_GREEN_DIM: 3C_GREEN_DIM; this.Demo.print(pos: Vector2i, text: string, slot: number): void
Wraps BT.printFont so callers pass a target palette slot rather than the raw offset the engine wants. C_WHITE is where the font's pixels live after indexize, so the offset to reach `slot` is `slot - C_WHITE`.
@parampos@paramtext@paramslot - target palette slot index (e.g. C_GREEN)
print
(new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const TEXT_LEFT: 14TEXT_LEFT, const y: numbery), const visible: anyvisible, const slot: 3 | 4slot);
// Subtract this line's ticks from the running total before moving on. let ticksLeft: numberticksLeft -= const fullLine: anyfullLine.length * const BOOT_TICKS_PER_CHAR: 2BOOT_TICKS_PER_CHAR + const BOOT_LINE_SPACER_TICKS: 6BOOT_LINE_SPACER_TICKS; if (let ticksLeft: numberticksLeft <= 0) { return; } } } /** * Returns true once every boot line has been fully typed out. */ Demo.bootFullyDone(): boolean
Returns true once every boot line has been fully typed out.
bootFullyDone
() {
// Sum: every line costs (length * ticksPerChar + spacer); empty lines cost just spacer. let let totalTicks: numbertotalTicks = 0; for (const const line: anyline of const BOOT_LINES: {}BOOT_LINES) { let totalTicks: numbertotalTicks += (const line: anyline.length === 0 ? 0 : const line: anyline.length * const BOOT_TICKS_PER_CHAR: 2BOOT_TICKS_PER_CHAR) + const BOOT_LINE_SPACER_TICKS: 6BOOT_LINE_SPACER_TICKS; } return this.Demo.ticksSinceBoot: number
Ticks elapsed since bootStartTick - refreshed every update().
ticksSinceBoot
>= let totalTicks: numbertotalTicks;
} /** * Draws the static "stats" block on the right half of the screen. */ Demo.renderStatusBlock(): void
Draws the static "stats" block on the right half of the screen.
renderStatusBlock
() {
const const x: numberx = const DISPLAY_W: 320DISPLAY_W / 2 + 6; const const y0: 18y0 = const TEXT_TOP: 18TEXT_TOP; this.Demo.print(pos: Vector2i, text: string, slot: number): void
Wraps BT.printFont so callers pass a target palette slot rather than the raw offset the engine wants. C_WHITE is where the font's pixels live after indexize, so the offset to reach `slot` is `slot - C_WHITE`.
@parampos@paramtext@paramslot - target palette slot index (e.g. C_GREEN)
print
(new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const x: numberx, const y0: 18y0), '== STATUS ==', const C_GREEN_BRIGHT: 5C_GREEN_BRIGHT);
for (let let i: numberi = 0; let i: numberi < const STATUS_LINES: {}STATUS_LINES.length; let i: numberi++) { const [const label: anylabel, const value: anyvalue, const colorName: anycolorName] = const STATUS_LINES: {}STATUS_LINES[let i: numberi]; const const y: numbery = const y0: 18y0 + (let i: numberi + 2) * const LINE_HEIGHT: 14LINE_HEIGHT; this.Demo.print(pos: Vector2i, text: string, slot: number): void
Wraps BT.printFont so callers pass a target palette slot rather than the raw offset the engine wants. C_WHITE is where the font's pixels live after indexize, so the offset to reach `slot` is `slot - C_WHITE`.
@parampos@paramtext@paramslot - target palette slot index (e.g. C_GREEN)
print
(new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const x: numberx, const y: numbery), const label: anylabel, const C_GREEN_DIM: 3C_GREEN_DIM);
// Right-align the value. Label sits at column 0; value sits at column 70px. // Hand-tuned for this font size - fine because both label and value are short. this.Demo.print(pos: Vector2i, text: string, slot: number): void
Wraps BT.printFont so callers pass a target palette slot rather than the raw offset the engine wants. C_WHITE is where the font's pixels live after indexize, so the offset to reach `slot` is `slot - C_WHITE`.
@parampos@paramtext@paramslot - target palette slot index (e.g. C_GREEN)
print
(new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const x: numberx + 70, const y: numbery), const value: anyvalue, function colorSlot(name: string): number
Maps a status-line "color name" (kept as a string in STATUS_LINES so the table is human-readable) to the matching palette slot.
@paramname@returns
colorSlot
(const colorName: anycolorName));
} } /** * Draws a blinking cursor below the boot text. A bright square that's visible for half * a second and then off for half a second. (Old terminals worked exactly like this.) */ Demo.renderBlinkingCursor(): void
Draws a blinking cursor below the boot text. A bright square that's visible for half a second and then off for half a second. (Old terminals worked exactly like this.)
renderBlinkingCursor
() {
const const phase: numberphase = Math.floor(
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
/ const CURSOR_BLINK_TICKS: 30CURSOR_BLINK_TICKS) % 2;
if (const phase: numberphase === 0) { // The "I'm here" square. Sits one line below the last boot line. const const lastLineY: numberlastLineY = const TEXT_TOP: 18TEXT_TOP + const BOOT_LINES: {}BOOT_LINES.length * const LINE_HEIGHT: 14LINE_HEIGHT;
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.drawRectFill: (rect: Rect2i, paletteIndex: number) => void
Draws a filled rectangle.
@since0.1.0@paramrect - Rectangle bounds in display coordinates.@parampaletteIndex - Palette color index.
drawRectFill
(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(const TEXT_LEFT: 14TEXT_LEFT, const lastLineY: numberlastLineY + 4, 7, 12), const C_GREEN_BRIGHT: 5C_GREEN_BRIGHT);
} } } function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
(class Demo
PipBoy-style terminal showcase. Renders a tiny boot sequence + status block in green bitmap text, then drives a JS-side glitch state machine that mutates the post-process effect uniforms each frame to produce occasional glitches. The effect stack is built explicitly here so each piece is visible: - PixelGlitch (pixel tier) on the logical r8uint index buffer for chunky band shifts. - Palette resolve + upscale to RGBA at canvas size (handled by the engine). - BarrelDistortion + ChromaticAberration + Interference + RollLine + Scanlines + RGBMask + Vignette + Noise + Flicker + Bloom (display tier) on that RGBA for the physical CRT simulation. If you want the same look in one line of code, use `BT.preset.crtPipBoy()` instead.
@implementsIBTDemo
Demo
);