// @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 BarrelDistortionBarrel 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)`.BarrelDistortion,
class BitmapFontBitmap 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 quadsBitmapFont,
class BloomSingle-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.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.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 ChromaticAberrationRGB 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.ChromaticAberration,
class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32,
class FlickerBrightness 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.Flicker,
class InterferencePer-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`.Interference,
class NoiseAdditive per-pixel pseudo-random noise. Reseeds each frame from
{@link
time
}
so the noise pattern animates.
Display-tier.Noise,
class PixelGlitchChunky 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).PixelGlitch,
class Rect2iInteger rectangle for pixel-perfect bounds and regions.
Used throughout the engine for sprite regions, display-space bounds, and
zero-allocation geometry helpers. Both convenience getters and allocation-free
`*To()` helpers are provided so callers can choose between readability and
hot-path efficiency.Rect2i,
class RGBMaskCRT 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.RGBMask,
class RollLineSlowly 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`).RollLine,
class ScanlinesCRT 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.Scanlines,
class Vector2iInteger 2D vector for pixel-perfect positioning.
Used for points, sizes, directions, and camera offsets throughout the engine.
The API includes both allocation-free `*To()` / `*InPlace()` variants and
convenience methods that return new vectors.Vector2i,
class VignetteEdge-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.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): numberMaps a status-line "color name" (kept as a string in STATUS_LINES so the table
is human-readable) to the matching palette slot.colorSlot(name: stringname) {
if (name: stringname === 'amber') {
return const C_AMBER: 6C_AMBER;
}
if (name: stringname === '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 DemoPipBoy-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.Demo {
/** True once WebGPU post-process effects were installed in init(). */
Demo.effectsAvailable: booleanTrue once WebGPU post-process effects were installed in init().effectsAvailable = false;
/** Tick count captured when the boot sequence started. */
Demo.bootStartTick: numberTick count captured when the boot sequence started.bootStartTick = 0;
/** Ticks elapsed since bootStartTick - refreshed every update(). */
Demo.ticksSinceBoot: numberTicks elapsed since bootStartTick - refreshed every update().ticksSinceBoot = 0;
/** True after the first frame where the boot sequence is fully visible. */
Demo.bootTagged: booleanTrue after the first frame where the boot sequence is fully visible.bootTagged = false;
/** Ticks remaining until the next glitch burst starts. */
Demo.glitchCooldown: numberTicks remaining until the next glitch burst starts.glitchCooldown = 0;
/** Ticks remaining in the current glitch burst (0 means idle). */
Demo.glitchTicksLeft: numberTicks remaining in the current glitch burst (0 means idle).glitchTicksLeft = 0;
/** Full duration of the current burst, used to build the envelope. */
Demo.glitchDuration: numberFull duration of the current burst, used to build the envelope.glitchDuration = 0;
/** Active glitch personality key, or 'none' when idle. */
Demo.glitchType: stringActive 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: numberPeak 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.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): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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.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) => PaletteCreates a standalone palette instance.paletteCreate(256);
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_BG: 1C_BG, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(8, 14, 8, 255)); // Almost-black with green tint
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_WHITE: 2C_WHITE, class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32.Color32.white: Color32Pure white color (255, 255, 255, 255).
Cached frozen singleton - do not modify.white); // Slot the font glyph pixels resolve to
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_GREEN_DIM: 3C_GREEN_DIM, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(40, 100, 60, 255)); // Faded green
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_GREEN: 4C_GREEN, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(80, 200, 110, 255)); // PipBoy green
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_GREEN_BRIGHT: 5C_GREEN_BRIGHT, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(170, 255, 190, 255)); // Hot green highlights
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_AMBER: 6C_AMBER, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.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) => voidStores the active engine palette.
Use this to swap the **entire palette** (e.g. switch between a day and night
theme). After this call the renderer uploads the new palette uniform on the
next frame.
**Palette-value swap (change what a slot looks like):** mutate the live
{@link
BT.palette
}
in place with `palette.set(slot, newColor)`. The renderer
uploads dirty slots on the next frame; no `paletteSet()` or
{@link
BT.spritesRefresh
}
needed.
**Palette-layout swap (same colors, different slot positions):** build a new
palette with the same colors at new indices, call `paletteSet()`, then call
{@link
BT.spritesRefresh
}
so every sprite sheet re-maps its original RGBA
pixels against the new slot layout.paletteSet(const palette: Palettepalette);
// 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 BitmapFontBitmap 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 quadsBitmapFont.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.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: booleanTrue once WebGPU post-process effects were installed in init().effectsAvailable = import isAvailableisAvailable();
if (!this.Demo.effectsAvailable: booleanTrue once WebGPU post-process effects were installed in init().effectsAvailable) {
this.Demo.bootStartTick: numberTick 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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.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) => voidPlaces a labeled marker on the overlay timing chart at the current tick.
Requires `isOverlayTimingChartEnabled: true` in `configure()`. Tags scroll with the chart
history and are pruned when they leave the visible window. Empty labels become
`"Untitled"`. Chart width resets add an automatic `"Start"` tag.assignTag('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(): PixelGlitchChunky 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).PixelGlitch();
this.Demo.pixelGlitch: PixelGlitchpixelGlitch.PixelGlitch.bandHeight: numberHeight 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: numberGlitch 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) => voidAppends a fullscreen post-processing effect to whichever chain matches
its declared
{@link
Effect.tier
}
.
- `tier='pixel'` -> pixel chain (logical resolution).
- `tier='display'` -> display chain (output resolution); requires
`drawingBufferSize` in effective hardware settings (`configure()` or
`defaultConfig()`).
Effects run in registration order within each tier. The pixel chain runs
first, followed by the upscale pass, followed by the display chain. Each
{@link
Effect
}
instance owns its own GPU resources and may be mutated
each frame from demo code.effectAdd(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(): BarrelDistortionBarrel 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)`.BarrelDistortion();
this.Demo.barrel: BarrelDistortionbarrel.BarrelDistortion.curvature: numberCurvature 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(): ChromaticAberrationRGB 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.ChromaticAberration();
this.Demo.aberration: ChromaticAberrationaberration.ChromaticAberration.aberration: numberChannel 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(): InterferencePer-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`.Interference();
this.Demo.interference: Interferenceinterference.Interference.amount: numberMaximum 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(): RollLineSlowly 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`).RollLine();
this.Demo.rollLine: RollLinerollLine.RollLine.amount: numberRoll line amplitude (mix factor onto a brightness boost).amount = 0.1; // strength of the bright band
this.Demo.rollLine: RollLinerollLine.RollLine.speed: numberScroll 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(): ScanlinesCRT 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.Scanlines();
this.Demo.scanlines: Scanlinesscanlines.Scanlines.amount: numberScanline mix amount in `[0, 1]`. 0 disables.amount = 0.55; // mix factor: 0 disables, 1 full effect
this.Demo.scanlines: Scanlinesscanlines.Scanlines.strength: numberNegative 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: numberNumber 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(): RGBMaskCRT 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.RGBMask();
this.Demo.mask: RGBMaskmask.RGBMask.intensity: numberMask 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: numberMask cell pitch in output (display-chain) pixels. Smaller = denser mask.size = 6; // mask cell pitch in source pixels
this.Demo.mask: RGBMaskmask.RGBMask.border: numberBorder 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(): VignetteEdge-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.Vignette();
this.Demo.vignette: Vignettevignette.Vignette.amount: numberVignette 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(): NoiseAdditive per-pixel pseudo-random noise. Reseeds each frame from
{@link
time
}
so the noise pattern animates.
Display-tier.Noise();
this.Demo.noise: Noisenoise.Noise.amount: numberNoise 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(): FlickerBrightness 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.Flicker();
this.Demo.flicker: Flickerflicker.Flicker.amount: numberBrightness 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(): BloomSingle-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.Bloom();
this.Demo.bloom: Bloombloom.Bloom.spread: numberTexel offset multiplier for the box-blur kernel.spread = 3.0; // size of the bloom kernel
this.Demo.bloom: Bloombloom.Bloom.glow: numberMix 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) => voidAppends a fullscreen post-processing effect to whichever chain matches
its declared
{@link
Effect.tier
}
.
- `tier='pixel'` -> pixel chain (logical resolution).
- `tier='display'` -> display chain (output resolution); requires
`drawingBufferSize` in effective hardware settings (`configure()` or
`defaultConfig()`).
Effects run in registration order within each tier. The pixel chain runs
first, followed by the upscale pass, followed by the display chain. Each
{@link
Effect
}
instance owns its own GPU resources and may be mutated
each frame from demo code.effectAdd(const fx: anyfx);
}
// 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: numberTick 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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.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: numberTicks 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: RandomDefault 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.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(import GLITCH_COOLDOWN_MINGLITCH_COOLDOWN_MIN, import GLITCH_COOLDOWN_MAXGLITCH_COOLDOWN_MAX);
this.Demo.glitchTicksLeft: numberTicks 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: numberFull duration of the current burst, used to build the envelope.glitchDuration = 0;
this.Demo.glitchType: stringActive glitch personality key, or 'none' when idle.glitchType = 'none';
this.Demo.glitchPeak: numberPeak intensity of the current burst (0..1), scaled by the envelope each tick.glitchPeak = 0;
return true;
}
Demo.update(): voidCalled zero or more times per frame at the fixed timestep declared by
`targetFPS`. The accumulator pattern ensures the target rate is met on
average, but a single frame may invoke this multiple times (catch-up) or
not at all. Update simulation, timers, and input-driven state here.
This is a hot path. Minimize allocations, reuse objects, and prefer
in-place vector operations where possible.
Avoid rendering work here; draw in `render()` instead.update() {
if (!this.Demo.effectsAvailable: booleanTrue once WebGPU post-process effects were installed in init().effectsAvailable) {
this.Demo.ticksSinceBoot: numberTicks 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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks - this.Demo.bootStartTick: numberTick 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: numberTicks 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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks - this.Demo.bootStartTick: numberTick 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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks / const TARGET_FPS: 60TARGET_FPS;
this.Demo.rollLine: RollLine | undefinedrollLine.RollLine.time: numberWall-clock seconds; demos typically drive this each frame.time = const seconds: numberseconds;
this.Demo.noise: Noise | undefinednoise.Noise.time: numberWall-clock seconds; reseeds the noise each frame.time = const seconds: numberseconds;
this.Demo.interference: Interference | undefinedinterference.Interference.time: numberWall-clock seconds; reseeds the row offsets each frame.time = const seconds: numberseconds;
// 3. Drive the glitch state machine
this.Demo.stepGlitchMachine(): voidAdvances 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(): voidCalled once per `requestAnimationFrame` tick (browser refresh rate).
Issue all draw calls for the current frame here.
When
{@link
HardwareSettings.isOverlayEnabled
}
is `true` (default), the engine
draws a screen-space overlay HUD after this method returns (present FPS, target FPS, draw calls,
frame/update()/render() timings, backend, demo title). Optional
{@link
overlayRows
}
adds stacked bars above
the footer.
Demos do not need to duplicate engine overlay text. Reserve about ~42 px at the top and space for the bottom palette
grid (or ~13 px when
{@link
HardwareSettings.isOverlayPaletteEnabled
}
is `false`) at the bottom (plus ~14 px per
custom overlay row) for overlay bars, or disable the overlay in `configure()` when using custom full-screen HUD
layouts.
This is a hot path. Batch draws by texture to reduce GPU state changes
and reuse Color32/Vector2i instances instead of allocating per frame.
Avoid mutating the simulation state here unless it is strictly visual.render() {
// 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) => voidSets the frame clear color using a palette index.
The renderer uses this color when clearing the full display at the start
of the next frame.clear(const C_BG: 1C_BG);
// Draw the boot sequence one character at a time, line by line.
this.Demo.renderBootSequence(): voidReveals 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(): booleanReturns true once every boot line has been fully typed out.bootFullyDone()) {
if (!this.Demo.bootTagged: booleanTrue after the first frame where the boot sequence is fully visible.bootTagged) {
this.Demo.bootTagged: booleanTrue 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) => voidPlaces a labeled marker on the overlay timing chart at the current tick.
Requires `isOverlayTimingChartEnabled: true` in `configure()`. Tags scroll with the chart
history and are pruned when they leave the visible window. Empty labels become
`"Untitled"`. Chart width resets add an automatic `"Start"` tag.assignTag('Boot done');
}
this.Demo.renderStatusBlock(): voidDraws the static "stats" block on the right half of the screen.renderStatusBlock();
this.Demo.renderBlinkingCursor(): voidDraws 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: booleanTrue 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(): voidAdvances 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: numberTicks 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: numberTicks remaining in the current glitch burst (0 means idle).glitchTicksLeft / this.Demo.glitchDuration: numberFull 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: numberTicks 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: numberTicks remaining in the current glitch burst (0 means idle).glitchTicksLeft === 0) {
import resetGlitchUniformsresetGlitchUniforms(this);
this.Demo.glitchCooldown: numberTicks 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: RandomDefault 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.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).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: numberTicks remaining until the next glitch burst starts.glitchCooldown--;
if (this.Demo.glitchCooldown: numberTicks 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: stringActive 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: RandomDefault 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.random.Random.pick<string>(arr: readonly string[]): stringReturns one element chosen uniformly from a non-empty array.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) => voidPlaces a labeled marker on the overlay timing chart at the current tick.
Requires `isOverlayTimingChartEnabled: true` in `configure()`. Tags scroll with the chart
history and are pruned when they leave the visible window. Empty labels become
`"Untitled"`. Chart width resets add an automatic `"Start"` tag.assignTag(`Glitch: ${this.Demo.glitchType: stringActive glitch personality key, or 'none' when idle.glitchType}`);
this.Demo.glitchDuration: numberFull 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: RandomDefault 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.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(import GLITCH_ACTIVE_MINGLITCH_ACTIVE_MIN, import GLITCH_ACTIVE_MAXGLITCH_ACTIVE_MAX);
this.Demo.glitchTicksLeft: numberTicks remaining in the current glitch burst (0 means idle).glitchTicksLeft = this.Demo.glitchDuration: numberFull duration of the current burst, used to build the envelope.glitchDuration;
this.Demo.glitchPeak: numberPeak 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: RandomDefault 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.random.Random.float(min: number, max: number): numberReturns the next pseudo-random float in [min, max).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: numberPer-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: RandomDefault 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.random.Random.float(min: number, max: number): numberReturns the next pseudo-random float in [min, max).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): voidWraps 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`.print(pos: Vector2ipos, text: stringtext, slot: number- 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) => voidDraws 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
}
.printFont(this.Demo.font: anyfont, pos: Vector2ipos, text: stringtext, slot: number- 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(): voidReveals 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: numberTicks 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): voidWraps 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`.print(new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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(): booleanReturns 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: numberTicks elapsed since bootStartTick - refreshed every update().ticksSinceBoot >= let totalTicks: numbertotalTicks;
}
/**
* Draws the static "stats" block on the right half of the screen.
*/
Demo.renderStatusBlock(): voidDraws 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): voidWraps 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`.print(new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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): voidWraps 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`.print(new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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): voidWraps 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`.print(new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const x: numberx + 70, const y: numbery), const value: anyvalue, function colorSlot(name: string): numberMaps a status-line "color name" (kept as a string in STATUS_LINES so the table
is human-readable) to the matching palette slot.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(): voidDraws 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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.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) => voidDraws a filled rectangle.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(const 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.bootstrap(class DemoPipBoy-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.Demo);