/*
PipBoy CRT: a faux Fallout terminal with scanlines, glitches, and bloom.
@description A faux Fallout terminal built from decomposed CRT effects: barrel warp, scanlines, mask, and glitches.
@pageTitle BLIT386 Demo – PipBoy CRT
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
Guides:
- Post-Process Effects: https://blit386.dev/docs/guides/post-process-effects#architecture
- Bitmap Fonts: https://blit386.dev/docs/guides/bitmap-fonts#custom-bitmap-fonts-btfont
- Random: https://blit386.dev/docs/guides/random#deterministic-by-default
- Overlay: https://blit386.dev/docs/guides/overlay#colors-and-hud-palette-slots
Reference:
- Rendering: https://blit386.dev/docs/api/rendering#post-process-effects
- Core: https://blit386.dev/docs/api/core#requested-vs-active-backend
- Random: https://blit386.dev/docs/api/random#generators
- Overlay: https://blit386.dev/docs/api/overlay#event-tags
---
WHAT YOU WILL SEE
-----------------
A green-on-black terminal on a curved CRT. The boot sequence types itself out one
character at a time (two ticks per character, a short pause after each line); a line
switches from dim to full green the moment it completes. Once the last line is in, a "==
STATUS ==" block appears in the right half of the screen - HP, AP, RAD, CAPS, and WEIGHT,
with RAD dimmed and CAPS in amber - and a bright cursor blinks below the boot text, 0.5
seconds on and 0.5 seconds off.
The whole picture passes through a CRT stack: barrel curvature, scanlines aligned to the
240 logical rows, an RGB shadow mask, a vignette, film grain, a bright band rolling down
the screen, and bloom. Roughly every 2 to 6 seconds a glitch burst lasting 5 to 29 ticks
fires. There are five kinds: a horizontal band shift, a red/blue channel split, heavier
grain, a brightness dip, and per-row interference jitter.
WHAT YOU WILL LEARN
-------------------
- Post-processing: full-screen effects that run after the scene is drawn, registered with
BT.effectAdd(effect) and routed by each effect's `tier`.
- The two effect tiers and why they exist:
- Pixel tier: palette-native, runs on the logical index buffer (r8uint, one byte per
pixel) at 320x240. PixelGlitch lives here.
- Between the tiers the engine resolves each index through the palette and upscales to
RGBA at the drawing-buffer size (1280x960).
- Display tier: simulates the physical screen on that full-size RGBA image.
BarrelDistortion, ChromaticAberration, Interference, RollLine, Scanlines, RGBMask,
Vignette, Noise, Flicker, and Bloom live here.
- Composing individual effect classes instead of one monolithic shader.
BT.preset.crtPipBoy() builds a similar stack in one call (without Vignette); this demo
wires each piece by hand so every parameter is visible.
- Driving effect uniforms from JavaScript each tick: `time` for RollLine, Noise, and
Interference, and the glitch fields through the shared helpers in
src/shared/crt-glitch.js.
- A tick-driven state machine that uses BT.random for timing, type, and strength.
- Loading a .btfont with BitmapFont.load(), running indexize() on it, and recoloring
glyphs through the BT.printFont() palette offset.
HOW POST-PROCESSING WORKS
-------------------------
Without effects the engine draws the scene, resolves palette indices to RGBA, upscales,
and presents. Once an effect is registered with BT.effectAdd, the engine routes the frame
through one or two effect chains. Each effect reads one texture and writes another; the
last effect in the display chain writes to the swap chain.
Pixel-tier effects (PixelGlitch here) operate on the logical framebuffer, which stores
palette slot indices (GPU format r8uint) at 320x240 - not RGBA yet. They stay
palette-native: integer texture reads, no averaging into in-between colors.
The engine then runs the palette resolve plus upscale: each index becomes an RGBA color
and the image grows to the drawing-buffer size (1280x960). Display-tier effects run on
that RGBA output. BarrelDistortion therefore computes its curve at 1280x960, not on the
320x240 index grid, so lines stay smooth instead of breaking into stair-steps.
HOW THE GLITCH STATE MACHINE WORKS
----------------------------------
Two counters drive it:
- glitchCooldown: ticks remaining until the next burst starts.
- glitchTicksLeft: ticks remaining in the current burst (0 means idle).
Every tick, update() decrements one of them. While glitchTicksLeft is above zero the burst
is running: an envelope sin(t * PI) rises from 0 to 1 and back over the burst's lifetime,
and applyGlitchUniforms() scales the chosen effect's parameter by it. When the burst ends,
resetGlitchUniforms() restores the resting values and a new cooldown is rolled. When the
cooldown reaches zero, a new burst is rolled: a type from GLITCH_TYPES_CHROMA, a duration
from GLITCH_ACTIVE_MIN..MAX ticks, a peak from GLITCH_INTENSITY_MIN..MAX, and a fresh
PixelGlitch seed. Each burst type drives a different uniform:
- hshift: PixelGlitch.intensity (the pixel-tier band shift).
- chromasplit: ChromaticAberration.aberration.
- noise: Noise.amount above its resting grain.
- flicker: Flicker.amount dips toward FLICKER_DIP.
- interference: Interference.amount.
SOFTWARE FALLBACK
-----------------
If the browser uses the Canvas 2D software renderer (WebGPU missing or ?backend=software
in the URL), post-process effects are unavailable. The terminal scene, boot animation, and
status block still run; only the CRT stack is skipped, and the glitch machine never
starts. A caption drawn with the shared UI kit in the bottom-left corner explains the
reduced mode.
HOW THE BITMAP FONT COLORS WORK
-------------------------------
The font loads as a sprite sheet of white glyph pixels. indexize() against the palette
replaces each white pixel with the index of the white slot (C_WHITE). BT.printFont(font,
pos, text, offset) adds `offset` to that index, so passing `C_GREEN - C_WHITE` moves every
glyph pixel from the white slot to the green slot. The print() helper below does that
arithmetic. The Sprite Effects demo uses the same trick for tints.
TRY THIS
--------
- Press the backquote key (`) to open the engine overlay. The bottom-left toggle hint is
hidden so it does not break the terminal illusion, but the key still works. The overlay
draws in this demo's green palette slots, with the timing chart in rich diagnostics mode
and the renderer diagnostics bar enabled.
- Watch the timing chart tags: "Boot done" once the boot text completes, then "Glitch:
hshift", "Glitch: chromasplit", "Glitch: noise", "Glitch: flicker", or "Glitch:
interference" at the start of each burst.
- Add ?backend=software to the URL to see the fallback caption.
*/
// Each effect is its own class, so the stack can be composed freely.
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;
... 124 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. 4x 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. This is the size the frame is presented at, not
// the pixel-art size: the demo still draws palette indices into a 320x240 logical buffer.
// Pixel-tier effects touch that index buffer; resolve + upscale then 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;
// Fixed update 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 that index is shifted 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 font's pixels land here; 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 nod to the alternative PipBoy palette)
// Layout for the terminal text, pre-computed 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;
// Boot typing speed. Each character is revealed every BOOT_TICKS_PER_CHAR ticks, and
// every line (including an empty one) adds a BOOT_LINE_SPACER_TICKS pause - 6 ticks at 60
// ticks per second is 100 ms.
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 character by character. Keep it
// short - with too many lines the demo takes a long time to finish booting.
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 effects have something to work on. The third column
// is a color name resolved by colorSlot().
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 ticks
// per second is 0.5 seconds. The cursor is a bright rectangle.
const const CURSOR_BLINK_TICKS: 30CURSOR_BLINK_TICKS = 30;
// Maps a status-line color name (kept as a string in STATUS_LINES so the table stays
// readable) to the matching palette slot.
function function colorSlot(name: any): 3 | 4 | 6colorSlot(name: anyname) {
if (name: anyname === 'amber') {
return const C_AMBER: 6C_AMBER;
}
if (name: anyname === 'dim') {
return const C_GREEN_DIM: 3C_GREEN_DIM;
}
return const C_GREEN: 4C_GREEN;
}
// PipBoy-style terminal showcase: a boot sequence and status block in green bitmap text,
// plus a JavaScript-side glitch state machine that rewrites post-process effect uniforms
// each tick. The effect stack is built explicitly so each piece is visible:
// - PixelGlitch (pixel tier) on the logical r8uint index buffer for chunky band shifts.
// - Palette resolve + upscale to RGBA at the drawing-buffer size (done by the engine).
// - BarrelDistortion, ChromaticAberration, Interference, RollLine, Scanlines, RGBMask,
// Vignette, Noise, Flicker, and Bloom (display tier) on that RGBA image.
// For a similar look in one line, use BT.preset.crtPipBoy() instead.
class class DemoDemo {
// True once WebGPU post-process effects were installed in init().
Demo.effectsAvailable: booleaneffectsAvailable = false;
// Tick count captured when the boot sequence started.
Demo.bootStartTick: numberbootStartTick = 0;
// Ticks elapsed since bootStartTick, refreshed every update().
Demo.ticksSinceBoot: numberticksSinceBoot = 0;
// True after the first frame where the boot sequence is fully visible.
Demo.bootTagged: booleanbootTagged = false;
// Ticks remaining until the next glitch burst starts.
Demo.glitchCooldown: numberglitchCooldown = 0;
// Ticks remaining in the current glitch burst (0 means idle).
Demo.glitchTicksLeft: numberglitchTicksLeft = 0;
// Full duration of the current burst, used to build the envelope.
Demo.glitchDuration: numberglitchDuration = 0;
// Active glitch type key, or 'none' when idle.
Demo.glitchType: stringglitchType = 'none';
// Peak intensity of the current burst (0..1), scaled by the envelope each tick.
Demo.glitchPeak: numberglitchPeak = 0;
// Pixel-art logical size, a 4x drawing buffer for the display-tier CRT stack, and an
// overlay tuned to the terminal look.
Demo.configure(): {
displaySize: Vector2i;
drawingBufferSize: Vector2i;
maxCanvasSize: Vector2i;
outputUpscaleFilter: string;
targetFPS: number;
isOverlayToggleHintVisible: boolean;
overlayStyle: {
barPaletteIndex: number;
textPaletteIndex: number;
gapPaletteIndex: number;
};
isOverlayTimingChartEnabled: boolean;
overlayTimingChartDiagnostics: string;
isOverlayRendererDiagnosticsBarEnabled: boolean;
overlayTimingChartStyle: {
updateBarPaletteIndex: number;
renderBarPaletteIndex: number;
warningPaletteIndex: number;
errorPaletteIndex: number;
tagPaletteIndex: number;
};
}
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.
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 throws for those effects. A clean 4x
// integer scale maps each logical pixel 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 (the 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. Change this to 'linear' to compare.
outputUpscaleFilter: stringoutputUpscaleFilter: 'nearest',
targetFPS: numbertargetFPS: const TARGET_FPS: 60TARGET_FPS,
// Hide the small tilde 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
// overlay itself still works: press the backquote key (`) to toggle it. The
// hint is hidden, not disabled.
isOverlayToggleHintVisible: booleanisOverlayToggleHintVisible: false,
// Overlay colors from this demo's own slots, plus the timing chart in rich
// diagnostics mode and the renderer diagnostics bar.
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,
},
};
}
async Demo.init(): unknowninit() {
// 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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteCreate: (size?: number) => PaletteCreates a standalone palette instance.paletteCreate(256);
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_BG: 1C_BG, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(8, 14, 8, 255)); // almost-black with a 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
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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteSet: (palette: Palette) => voidStores the active engine palette.
Use this to swap the **entire palette** (e.g. switch between a day and night
theme). After this call the renderer uploads the new palette uniform on the
next frame.
**Palette-value swap (change what a slot looks like):** mutate the live
{@link
BT.palette
}
in place with `palette.set(slot, newColor)`. The renderer
uploads dirty slots on the next frame; no `paletteSet()` or
{@link
BT.spritesRefresh
}
needed.
**Palette-layout swap (same colors, different slot positions):** build a new
palette with the same colors at new indices, call `paletteSet()`, then call
{@link
BT.spritesRefresh
}
so every sprite sheet re-maps its original RGBA
pixels against the new slot layout.paletteSet(const palette: Palettepalette);
// Step 2: load the bitmap font. PragmataPro is a monospaced programming font - a
// good fit for a fictional terminal. A .btfont is a BLIT386 bitmap font: a JSON
// glyph table (bounds, offsets, and advance per character) plus a PNG glyph
// atlas.
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 loads 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 and display tiers) needs WebGPU. Software mode skips the
// rest.
this.Demo.effectsAvailable: booleaneffectsAvailable = import isAvailableisAvailable();
if (!this.Demo.effectsAvailable: booleaneffectsAvailable) {
this.Demo.bootStartTick: numberbootStartTick = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.ticks: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks;
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.assignTag: (label?: string) => voidPlaces a labeled marker on the overlay timing chart at the current tick.
Requires `isOverlayTimingChartEnabled: true` in `configure()`. Tags scroll with the chart
history and are pruned when they leave the visible window. Empty labels become
`"Untitled"`. Chart width resets add an automatic `"Start"` tag.assignTag('Software renderer');
return true;
}
// Step 4: pixel-tier effect (chunky glitch). PixelGlitch reads and 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. bandHeight is the height of each
// band in source pixels.
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;
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; the state machine spikes 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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.effectAdd: (effect: Effect) => voidAppends a fullscreen post-processing effect to whichever chain matches
its declared
{@link
Effect.tier
}
.
- `tier='pixel'` -> pixel chain (logical resolution).
- `tier='display'` -> display chain (output resolution); requires
`drawingBufferSize` in effective hardware settings (`configure()` or
`defaultConfig()`).
Effects run in registration order within each tier. The pixel chain runs
first, followed by the upscale pass, followed by the display chain. Each
{@link
Effect
}
instance owns its own GPU resources and may be mutated
each frame from demo code.effectAdd(this.Demo.pixelGlitch: PixelGlitchpixelGlitch); // the effect's tier 'pixel' routes it
// Step 5: display-tier stack. Order matters: barrel first (it warps the UVs the
// rest of the chain inherits), then color and signal artifacts, then scanlines
// and mask, then vignette, noise, and flicker, and finally bloom on top of the
// modulated image.
// Barrel distortion: simulates the curved glass of a CRT tube. Because this runs
// after palette resolve + upscale, the curve is computed at 1280x960 and 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. 0 at rest here; the
// 'chromasplit' burst spikes it.
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. 0
// at rest so the screen is calm between bursts; the 'interference' burst spikes
// it.
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 is not quite synced.
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 and 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: a 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;
// Noise: subtle film grain that changes every 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 (1.0 means
// no change).
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 phosphor halo of an old monitor. Last
// in the chain so it 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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.effectAdd: (effect: Effect) => voidAppends a fullscreen post-processing effect to whichever chain matches
its declared
{@link
Effect.tier
}
.
- `tier='pixel'` -> pixel chain (logical resolution).
- `tier='display'` -> display chain (output resolution); requires
`drawingBufferSize` in effective hardware settings (`configure()` or
`defaultConfig()`).
Effects run in registration order within each tier. The pixel chain runs
first, followed by the upscale pass, followed by the display chain. Each
{@link
Effect
}
instance owns its own GPU resources and may be mutated
each frame from demo code.effectAdd(const fx: anyfx);
}
// Step 6: boot animation timer. Ticks instead of wall-clock time keep the boot
// animation deterministic even if the browser frame rate hiccups.
this.Demo.bootStartTick: numberbootStartTick = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.ticks: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks;
// Step 7: glitch state machine state. See the file header for what each field
// means. BT.random is the engine's shared random number generator; int(min, max)
// returns a whole number from min up to but not including max, so the first burst
// waits a random number of ticks instead of firing on frame 1.
this.Demo.glitchCooldown: numberglitchCooldown = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.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, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
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: numberglitchTicksLeft = 0; // ticks remaining in the current burst; 0 means idle
this.Demo.glitchDuration: numberglitchDuration = 0;
this.Demo.glitchType: stringglitchType = 'none';
this.Demo.glitchPeak: numberglitchPeak = 0;
return true;
}
Demo.update(): voidupdate() {
if (!this.Demo.effectsAvailable: booleaneffectsAvailable) {
this.Demo.ticksSinceBoot: numberticksSinceBoot = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.ticks: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks - this.Demo.bootStartTick: numberbootStartTick;
return;
}
// 1. Drive the boot animation timer. Nothing is drawn here - render() reads
// ticksSinceBoot and computes how many characters to show. update() provides
// time.
this.Demo.ticksSinceBoot: numberticksSinceBoot = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.ticks: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks - this.Demo.bootStartTick: numberbootStartTick;
// 2. Drive the time-based effects. RollLine, Noise, and Interference animate from
// a seconds value. Converting ticks to seconds keeps the animation speed
// 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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.ticks: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks / const TARGET_FPS: 60TARGET_FPS;
this.Demo.rollLine: RollLine | undefinedrollLine.RollLine.time: numberSeconds; demos drive this each frame. Use BT.timeSeconds so BT.renderAt can seek it.time = const seconds: numberseconds;
this.Demo.noise: Noise | undefinednoise.Noise.time: numberSeconds; reseeds the noise each frame. Drive it from BT.timeSeconds so BT.renderAt can seek it.time = const seconds: numberseconds;
this.Demo.interference: Interference | undefinedinterference.Interference.time: numberSeconds; reseeds the row offsets each frame. Drive it from BT.timeSeconds so BT.renderAt can seek it.time = const seconds: numberseconds;
// 3. Drive the glitch state machine.
this.Demo.stepGlitchMachine(): voidstepGlitchMachine();
}
Demo.render(): voidrender() {
// Fill the background. Even with the CRT stack on top, this is 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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.clear: (paletteIndex: number) => voidSets the frame clear color using a palette index.
The renderer uses this color when clearing the full display at the start
of the next frame.clear(const C_BG: 1C_BG);
// Draw the boot sequence one character at a time, line by line.
this.Demo.renderBootSequence(): voidrenderBootSequence();
// Once every boot line is visible, draw the status block on the right.
if (this.Demo.bootFullyDone(): booleanbootFullyDone()) {
if (!this.Demo.bootTagged: booleanbootTagged) {
this.Demo.bootTagged: booleanbootTagged = 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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.assignTag: (label?: string) => voidPlaces a labeled marker on the overlay timing chart at the current tick.
Requires `isOverlayTimingChartEnabled: true` in `configure()`. Tags scroll with the chart
history and are pruned when they leave the visible window. Empty labels become
`"Untitled"`. Chart width resets add an automatic `"Start"` tag.assignTag('Boot done');
}
this.Demo.renderStatusBlock(): voidrenderStatusBlock();
this.Demo.renderBlinkingCursor(): voidrenderBlinkingCursor();
}
// 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: booleaneffectsAvailable) {
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(). Either a burst
// is 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(): voidstepGlitchMachine() {
if (this.Demo.glitchTicksLeft: numberglitchTicksLeft > 0) {
// Inside a burst. Build an envelope that ramps up to glitchPeak and back
// down: sin over 0..PI is a 0 -> 1 -> 0 hump across the lifetime of the
// burst. t is 0 at the start and 1 at the end.
const const t: numbert = 1 - this.Demo.glitchTicksLeft: numberglitchTicksLeft / this.Demo.glitchDuration: numberglitchDuration;
const const envelope: anyenvelope = Math.sin(const t: numbert * Math.PI); // 0 -> 1 -> 0
import applyGlitchUniformsapplyGlitchUniforms(this, const envelope: anyenvelope);
this.Demo.glitchTicksLeft: numberglitchTicksLeft--;
// When the burst ends, reset the uniforms so the screen calms down and roll
// the next cooldown.
if (this.Demo.glitchTicksLeft: numberglitchTicksLeft === 0) {
import resetGlitchUniformsresetGlitchUniforms(this);
this.Demo.glitchCooldown: numberglitchCooldown = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.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, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
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: numberglitchCooldown--;
if (this.Demo.glitchCooldown: numberglitchCooldown <= 0) {
// Roll a new burst: pick() returns one item of the list, int() a whole
// number in [min, max), float() a decimal in [min, max).
this.Demo.glitchType: stringglitchType = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.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, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.assignTag: (label?: string) => voidPlaces a labeled marker on the overlay timing chart at the current tick.
Requires `isOverlayTimingChartEnabled: true` in `configure()`. Tags scroll with the chart
history and are pruned when they leave the visible window. Empty labels become
`"Untitled"`. Chart width resets add an automatic `"Start"` tag.assignTag(`Glitch: ${this.Demo.glitchType: stringglitchType}`);
this.Demo.glitchDuration: numberglitchDuration = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.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, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
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: numberglitchTicksLeft = this.Demo.glitchDuration: numberglitchDuration;
this.Demo.glitchPeak: numberglitchPeak = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.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, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
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);
// Reseed 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;
... 124 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, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
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`.
Demo.print(pos: any, text: any, slot: any): voidprint(pos: anypos, text: anytext, slot: anyslot) {
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.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: anypos, text: anytext, slot: anyslot - const C_WHITE: 2C_WHITE);
}
// Reveals BOOT_LINES one character at a time, slicing each string by how many ticks
// have passed.
Demo.renderBootSequence(): voidrenderBootSequence() {
let let ticksLeft: numberticksLeft = this.Demo.ticksSinceBoot: numberticksSinceBoot;
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 only consume the spacer ticks (so the pause between sections
// feels right) and draw nothing.
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 one 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) {
// A future line, not yet started. Stop - everything below is invisible
// too.
return;
}
const const visible: anyvisible = const fullLine: anyfullLine.slice(0, const charsToShow: anycharsToShow);
// Finished lines get the brighter green; a line still typing stays dim until
// it completes.
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: any, text: any, slot: any): voidprint(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(): booleanbootFullyDone() {
// Every line costs length * BOOT_TICKS_PER_CHAR plus the spacer; empty lines cost
// only the 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: numberticksSinceBoot >= let totalTicks: numbertotalTicks;
}
// Draws the static status block on the right half of the screen.
Demo.renderStatusBlock(): voidrenderStatusBlock() {
const const x: numberx = const DISPLAY_W: 320DISPLAY_W / 2 + 6;
const const y0: 18y0 = const TEXT_TOP: 18TEXT_TOP;
this.Demo.print(pos: any, text: any, slot: any): voidprint(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: any, text: any, slot: any): voidprint(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);
// The value column starts 70 px right of the label column. Hand-tuned for
// this font size; fine because both label and value are short.
this.Demo.print(pos: any, text: any, slot: any): voidprint(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: any): 3 | 4 | 6colorSlot(const colorName: anycolorName));
}
}
// Draws a blinking cursor below the boot text: a bright rectangle visible for half a
// second, then off for half a second, the way old terminals did it.
Demo.renderBlinkingCursor(): voidrenderBlinkingCursor() {
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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.ticks: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks / const CURSOR_BLINK_TICKS: 30CURSOR_BLINK_TICKS) % 2;
if (const phase: numberphase === 0) {
// 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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(const 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 DemoDemo);