// Basics Enhanced.
// @description The bouncing sprite from Basics again, with optional visual effects over the same PipBoy palette.
//
// Same bouncing-sprite behavior as the Basics demo (https://demos.blit386.dev/basics),
// with the same PipBoy palette and overlay rows for position and bounces. Every
// frame is also routed through a hand-built CRT stack on WebGPU. If Basics was "the engine
// works", this demo is "the engine works, and here is the kind of finish you can layer
// on top once you understand the post-process pipeline".
//
// Prerequisites: Basics (https://demos.blit386.dev/basics),
// PipBoy CRT (https://demos.blit386.dev/crt-pipboy),
// CRT Toggle (https://demos.blit386.dev/crt-toggle).
//
// The pipeline has two tiers. Both come from the engine's post-process system we
// explored in crt-pipboy and crt-toggle:
//
//   1. Pixel tier - runs ON the logical index buffer (320x240, palette indices, BEFORE
//      the palette is resolved into RGB). Effects here distort the indexed image itself.
//      Only PixelGlitch sits here. See:
//      https://blit386.dev/docs/guides/post-process-effects
//
//   2. Display tier - runs AFTER the palette is resolved and the image is upscaled to
//      the canvas. Effects here work in full-color RGB and can blur, warp, tint, and
//      bloom the final image. The other ten effects in this demo live here.
//
// Why ten separate display-tier effects instead of one ready-made preset (like
// BT.preset.crtPipBoy used in crt-toggle)? Because hand-composing the chain makes it possible
// to drive individual uniforms from a state machine - the glitch state machine below
// picks ONE of five glitch styles, ramps it up for a few frames, and ramps it down again.
//
// SOFTWARE FALLBACK: when the engine uses the software renderer, the bouncing sprite
// demo still runs but the CRT stack is not registered. A warm on-canvas note (drawn with
// the shared UI kit in src/shared/ui.js) and overlay rows explain the reduced mode.
//
// Live version: https://demos.blit386.dev/basics-enhanced

import {
    
type BarrelDistortion = BarrelDistortion
class BarrelDistortion
Barrel distortion that warps UVs outward from the screen center. Display-tier: operates on the upscaled output. Applying this in the pixel tier (logical 320x240) discretizes the curve onto the source texel grid, which CSS upscale then magnifies into visible step artifacts. At output resolution the curve has enough resolution to express smoothly. The math comes from Timothy Lottes's public-domain `crt-lottes.glsl`: `warp(uv) = uv + delta * d2 * curvature` where `delta = uv - 0.5` and `d2 = dot(delta, delta)`.
@since1.0.3
BarrelDistortion
,
type Bloom = Bloom
class Bloom
Single-pass box-blur bloom. Samples a 5x5 neighborhood (25 taps) around each fragment, averages, then mixes with the original color by {@link glow } . {@link spread } scales the texel offset so the bloom radius can be tuned independently of the source resolution. Display-tier: bloom mixes neighboring pixels into intermediate hues that are not in the active palette. Running it in pixel space would violate the palette-pixel aesthetic; running it on the upscaled output reads as the warm phosphor glow of an old monitor instead. The implementation matches the original PipBoy bloom shader. A future optimization would be a two-pass separable Gaussian (5 + 5 = 10 taps); add it once a GPU perf test demands it.
@since1.0.3
Bloom
,
function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
,
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
,
type ChromaticAberration = ChromaticAberration
class ChromaticAberration
RGB channel offset that simulates lens chromatic aberration: red samples left of the fragment, blue samples right, green stays centered. Display-tier: spreads color along the lens axis. At logical resolution the single-pixel offset is too coarse and reads as a glitch instead of a soft fringe.
@since1.0.3
ChromaticAberration
,
class Color32
Mutable 32-bit RGBA color value with 8-bit channels.
@since0.1.0
Color32
,
type Flicker = Flicker
class Flicker
Brightness multiplier - the simplest CRT animation knob. Demos drive {@link amount } per frame to simulate flicker (e.g. with `0.95 + sin(t) * 0.05`). The effect is intentionally trivial so the demo controls the pattern; for procedural noise-driven flicker, combine with the {@link Noise } effect. Display-tier.
@since1.0.3
Flicker
,
type Interference = Interference
class Interference
Per-row horizontal jitter that simulates analog signal interference. Each output row gets a deterministic random horizontal offset seeded by row index and time. Row offsets are stable for one frame and re-seed every frame, producing a buzzing-noise feel. Display-tier. Drives jitter from {@link time } ; demos typically pass `BT.ticks / BT.targetFPS`.
@since1.0.3
Interference
,
type Noise = Noise
class Noise
Additive per-pixel pseudo-random noise. Reseeds each frame from {@link time } so the noise pattern animates. Display-tier.
@since1.0.3
Noise
,
type PixelGlitch = PixelGlitch
class PixelGlitch
Chunky pixel-aligned horizontal glitch: every Nth row of source pixels gets a random horizontal shift. Shifts snap to integer source-pixel offsets so palette indices move whole-texel (no RGB resampling). Pixel-tier: runs on the logical `r8uint` framebuffer (palette indices).
@since1.0.3
PixelGlitch
,
type RGBMask = RGBMask
class RGBMask
CRT shadow mask: per-pixel R/G/B vertical-stripe pattern with darkened cell borders, simulating the phosphor grille of an aperture-grille CRT. Display-tier: at output resolution there are enough output pixels per mask cell to read as colored stripes. The cell pitch (in output pixels) is the {@link size } parameter. Math is a direct WGSL port of the libretro `crt-lottes.glsl` mask code.
@since1.0.3
RGBMask
,
type RollLine = RollLine
class RollLine
Slowly scrolling vertical interference band that brightens a horizontal stripe of the image. Combination of three cosines + smoothstep gives the stripe a soft top/bottom edge. Display-tier. Demo drives {@link time } (typically `BT.ticks / BT.targetFPS`).
@since1.0.3
RollLine
,
type Scanlines = Scanlines
class Scanlines
CRT scanlines: alternating bright/dark horizontal bands aligned to the source vertical resolution. Display-tier: at output resolution there is enough vertical pixels for scanlines to read as alternating bright/dark bands. At logical 320x240 the Gaussian weight quantizes to one of two values per source row and you lose the smooth fade.
@since1.0.3
Scanlines
,
type SpriteSheet = SpriteSheet
class SpriteSheet
Sprite-sheet wrapper around a loaded image asset. The class keeps the original image available for CPU-side inspection while lazily creating and caching a GPU texture for rendering. When possible, `load()` also pre-decodes the source into an `ImageBitmap` so texture uploads preserve pixel-art alpha and color values more reliably. After calling `indexize()`, the sheet stores palette indices rather than RGBA data. The GPU texture becomes an `r8uint` format uploaded via `writeTexture`. The original RGBA bytes are retained so `reindexize()` can re-convert without reloading the image.
@since0.1.0
SpriteSheet
,
class Vector2i
Integer 2D vector for pixel-perfect positioning. Used for points, sizes, directions, and camera offsets throughout the engine. The API includes both allocation-free `*To()` / `*InPlace()` variants and convenience methods that return new vectors.
@since0.1.0
Vector2i
,
type Vignette = Vignette
class Vignette
Edge-darkening vignette: smooth radial fade from full brightness at the center to black at the corners. Display-tier: applies to the whole simulated screen, not the underlying pixel art.
@since1.0.3
Vignette
,
} from 'blit386'; import { import ABERRATION_BASEABERRATION_BASE, import applyGlitchUniformsapplyGlitchUniforms, import FLICKER_BASEFLICKER_BASE, import GLITCH_ACTIVE_MAXGLITCH_ACTIVE_MAX, import GLITCH_ACTIVE_MINGLITCH_ACTIVE_MIN, import GLITCH_COOLDOWN_MAXGLITCH_COOLDOWN_MAX, import GLITCH_COOLDOWN_MINGLITCH_COOLDOWN_MIN, import GLITCH_INTENSITY_MAXGLITCH_INTENSITY_MAX, import GLITCH_INTENSITY_MINGLITCH_INTENSITY_MIN, import GLITCH_LABELSGLITCH_LABELS, 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'; /** @typedef {import('blit386').IBTDemo} IBTDemo */ /** @typedef {import('blit386').HardwareSettings} HardwareSettings */ /** @typedef {import('blit386').Palette} Palette */ /** @typedef {import('blit386').SpriteSheet} SpriteSheet */ /** @typedef {import('blit386').Rect2i} Rect2i */ /** @typedef {import('blit386').PixelGlitch} PixelGlitch */ /** @typedef {import('blit386').BarrelDistortion} BarrelDistortion */ /** @typedef {import('blit386').ChromaticAberration} ChromaticAberration */ /** @typedef {import('blit386').Interference} Interference */ /** @typedef {import('blit386').RollLine} RollLine */ /** @typedef {import('blit386').Scanlines} Scanlines */ /** @typedef {import('blit386').RGBMask} RGBMask */ /** @typedef {import('blit386').Vignette} Vignette */ /** @typedef {import('blit386').Noise} Noise */ /** @typedef {import('blit386').Flicker} Flicker */ /** @typedef {import('blit386').Bloom} Bloom */ // Palette slots match the Basics demo so the two demos feel like the same scene. const const C_BG: 1C_BG = 1; // Almost-black with a faint green tint. const const C_OVERLAY_BAR: 2C_OVERLAY_BAR = 2; // Bar behind overlay custom rows. const const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN = 3; // PipBoy green (position, CRT status). const const C_OVERLAY_AMBER: 4C_OVERLAY_AMBER = 4; // Amber accent (bounces, glitch readout). const const C_OVERLAY_ERROR: 5C_OVERLAY_ERROR = 5; // Red tint reserved for future timing-chart error markers. const const SPRITE_BASE: 10SPRITE_BASE = 10; const const SPRITE_URL: "/sprites/logo-1.png"SPRITE_URL = '/sprites/logo-1.png'; const const TARGET_FPS: 30TARGET_FPS = 30; // Run the display-tier post-process at a larger output buffer than the logical screen. const const OUTPUT_W: 960OUTPUT_W = 960; const const OUTPUT_H: 720OUTPUT_H = 720; // The shared fallback note is one long sentence - too wide for this 320-pixel screen in // the 6-pixel-wide system font. split('. ') cuts the string at the sentence break, giving // us an array of two shorter lines the UI kit can draw one under the other. const const FALLBACK_LINES: anyFALLBACK_LINES = import SOFTWARE_FALLBACK_NOTESOFTWARE_FALLBACK_NOTE.split('. '); /** * Basics demo plus a hand-built CRT post-process chain and periodic glitch bursts. * * @implements {IBTDemo} */ class class Demo
Basics demo plus a hand-built CRT post-process chain and periodic glitch bursts.
@implementsIBTDemo
Demo
{
// --- Bouncing sprite (same roles as the Basics demo) --- // Top-left corner of the logo on screen (whole pixels only). Demo.pos: Vector2ipos = new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(160, 120);
// How many pixels the logo moves each update() tick (x and y separately). Demo.speed: Vector2ispeed = new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(1, 1);
// Logo position at the START of the most recent update() tick, before that tick // moved it. render() blends between this and pos using BT.renderAlpha so the logo // glides smoothly between ticks instead of jumping - same fix and same reason as // Basics demo (see the big comment above its render() for the full // explanation, including why targetFPS 30 makes the stutter especially visible). Demo.prevPos: Vector2iprevPos = new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(160, 120);
// Logo width and height in pixels; filled from the loaded PNG in init(). Demo.size: Vector2isize = new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(16, 16);
// Counts wall hits so overlayRows() can show a running total. Demo.bounces: numberbounces = 0; // Numbered paint cans for every draw call; built in init(). /** @type {Palette | null} */ Demo.palette: Palette | null
@type{Palette | null}
palette
= null;
// Loaded indexed sprite sheet (GPU texture + palette mapping). /** @type {SpriteSheet | null} */ Demo.spriteSheet: SpriteSheet | null
@type{SpriteSheet | null}
spriteSheet
= null;
// Which rectangle inside the PNG to draw (full image for our logo). /** @type {Rect2i | null} */ Demo.spriteRect: Rect2i | null
@type{Rect2i | null}
spriteRect
= null;
// --- Post-process effect handles (WebGPU only) --- // Pixel-tier glitch: shifts horizontal bands in the index buffer before palette resolve. /** @type {PixelGlitch | null} */ Demo.pixelGlitch: PixelGlitch | null
@type{PixelGlitch | null}
pixelGlitch
= null;
// Display-tier CRT stack (runs on upscaled RGBA after palette resolve). /** @type {BarrelDistortion | null} */ Demo.barrel: BarrelDistortion | null
@type{BarrelDistortion | null}
barrel
= null;
/** @type {ChromaticAberration | null} */ Demo.aberration: ChromaticAberration | null
@type{ChromaticAberration | null}
aberration
= null;
/** @type {Interference | null} */ Demo.interference: Interference | null
@type{Interference | null}
interference
= null;
/** @type {RollLine | null} */ Demo.rollLine: RollLine | null
@type{RollLine | null}
rollLine
= null;
/** @type {Scanlines | null} */ Demo.scanlines: Scanlines | null
@type{Scanlines | null}
scanlines
= null;
/** @type {RGBMask | null} */ Demo.mask: RGBMask | null
@type{RGBMask | null}
mask
= null;
/** @type {Vignette | null} */ Demo.vignette: Vignette | null
@type{Vignette | null}
vignette
= null;
/** @type {Noise | null} */ Demo.noise: Noise | null
@type{Noise | null}
noise
= null;
/** @type {Flicker | null} */ Demo.flicker: Flicker | null
@type{Flicker | null}
flicker
= null;
/** @type {Bloom | null} */ Demo.bloom: Bloom | null
@type{Bloom | null}
bloom
= null;
// --- Glitch state machine (same idea as the PipBoy CRT demo) --- // Ticks until the next random glitch burst starts (counts down while idle). Demo.glitchCooldown: numberglitchCooldown = 0; // Ticks remaining in the current burst (0 = calm screen). Demo.glitchTicksLeft: numberglitchTicksLeft = 0; // How long this burst was scheduled to last (used for the fade envelope). Demo.glitchDuration: numberglitchDuration = 0; // Which glitch personality is active ('none', 'hshift', 'noise', ...). Demo.glitchType: stringglitchType = 'none'; // Peak strength rolled for this burst (0..1 scale before envelope). Demo.glitchPeak: numberglitchPeak = 0; // True when WebGPU post-process is available; false in software fallback. Demo.effectsAvailable: booleaneffectsAvailable = false; // Reused every frame for overlayRows() - position, bounces, CRT status, glitch readout. Demo.overlayRowData: {}overlayRowData = [ { leftText: stringleftText: 'Position (0, 0)', textPaletteIndex: numbertextPaletteIndex: const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN }, { leftText: stringleftText: 'Bounces 0', textPaletteIndex: numbertextPaletteIndex: const C_OVERLAY_AMBER: 4C_OVERLAY_AMBER }, { leftText: stringleftText: 'CRT stack OFF', textPaletteIndex: numbertextPaletteIndex: const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN }, { leftText: stringleftText: 'Glitch NONE', textPaletteIndex: numbertextPaletteIndex: const C_OVERLAY_AMBER: 4C_OVERLAY_AMBER }, ]; /** * @returns {Partial<HardwareSettings>} */ Demo.configure(): Partial<HardwareSettings>
Optional hook to declare display size, optional output drawing-buffer size, upscale filter, target fixed-update rate, rendering backend, and overlay. When omitted, the engine uses {@link defaultConfig } (`320x240` logical, `640x480` drawing buffer, `60` FPS, overlay enabled). When present, you may return only the fields you want to change; the engine merges them with {@link defaultConfig } via {@link mergeHardwareSettings } . Omit `displaySize` to inherit the full default resolution and output buffer. Include `displaySize` when you want a custom logical size; optional fields you omit then stay unset (for example no `drawingBufferSize` means a 1:1 drawing buffer).
@returns
configure
() {
return { displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(320, 240),
// Display-tier CRT runs on the upscaled RGBA buffer (3x logical here). drawingBufferSize: Vector2idrawingBufferSize: new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const OUTPUT_W: 960OUTPUT_W, const OUTPUT_H: 720OUTPUT_H),
// Demos layout may scale the canvas up to 4x logical on screen (default cap is 960x720). maxCanvasSize: Vector2imaxCanvasSize: new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(320 * 4, 240 * 4),
outputUpscaleFilter: stringoutputUpscaleFilter: 'nearest', targetFPS: numbertargetFPS: const TARGET_FPS: 30TARGET_FPS, isDetectingDroppedFrames: booleanisDetectingDroppedFrames: true, // Opt in to the engine timing chart band (update vs render CPU bars above the FPS row). // Bar colors default to overlayStyle; we set explicit indices so they match this palette. isOverlayTimingChartEnabled: booleanisOverlayTimingChartEnabled: true,
overlayStyle: {
    barPaletteIndex: number;
    textPaletteIndex: number;
    gapPaletteIndex: number;
}
overlayStyle
: {
barPaletteIndex: numberbarPaletteIndex: const C_OVERLAY_BAR: 2C_OVERLAY_BAR, textPaletteIndex: numbertextPaletteIndex: const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN, gapPaletteIndex: numbergapPaletteIndex: const C_OVERLAY_BAR: 2C_OVERLAY_BAR, },
overlayTimingChartStyle: {
    updateBarPaletteIndex: number;
    renderBarPaletteIndex: number;
    warningPaletteIndex: number;
    errorPaletteIndex: number;
    tagPaletteIndex: number;
}
overlayTimingChartStyle
: {
updateBarPaletteIndex: numberupdateBarPaletteIndex: const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN, renderBarPaletteIndex: numberrenderBarPaletteIndex: const C_OVERLAY_AMBER: 4C_OVERLAY_AMBER, warningPaletteIndex: numberwarningPaletteIndex: const C_OVERLAY_AMBER: 4C_OVERLAY_AMBER, errorPaletteIndex: numbererrorPaletteIndex: const C_OVERLAY_ERROR: 5C_OVERLAY_ERROR, tagPaletteIndex: numbertagPaletteIndex: const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN, }, }; } /** * @returns {Promise<boolean>} */ async Demo.init(): Promise<boolean>
Called once after the selected rendering backend has been initialized. Load assets and prepare a demo state here.
@returns
init
() {
// --- Palette (matches the Basics demo PipBoy green scene) --- this.Demo.palette: Palette | null
@type{Palette | null}
palette
=
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.paletteCreate: (size?: number) => Palette
Creates a standalone palette instance.
@since1.0.3@paramsize - Palette size. Defaults to 256 colors.@returnsNew mutable palette.
paletteCreate
(256);
this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_BG: 1C_BG, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(16, 28, 16));
this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_OVERLAY_BAR: 2C_OVERLAY_BAR, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(24, 44, 28));
this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(80, 200, 110));
this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_OVERLAY_AMBER: 4C_OVERLAY_AMBER, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(220, 180, 60));
this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_OVERLAY_ERROR: 5C_OVERLAY_ERROR, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(200, 70, 70));
// --- Sprite load (same two-step path as the Basics demo) --- // Step 1: scan the PNG and copy every unique color into palette slots // starting at SPRITE_BASE so indexed drawing knows which slot each pixel uses. await class SpriteSheet
Sprite-sheet wrapper around a loaded image asset. The class keeps the original image available for CPU-side inspection while lazily creating and caching a GPU texture for rendering. When possible, `load()` also pre-decodes the source into an `ImageBitmap` so texture uploads preserve pixel-art alpha and color values more reliably. After calling `indexize()`, the sheet stores palette indices rather than RGBA data. The GPU texture becomes an `r8uint` format uploaded via `writeTexture`. The original RGBA bytes are retained so `reindexize()` can re-convert without reloading the image.
@since0.1.0
SpriteSheet
.
SpriteSheet.loadColorsIntoPalette(url: string, palette: Palette, startSlot: number, options?: {
    sort?: "luminance" | "none";
}): Promise<Color32[]>
Walks a PNG's pixels and registers every unique opaque color into the supplied palette starting at `startSlot`. Pixels with alpha 0 are skipped - they map to the engine's transparent sentinel slot 0 at draw time. Opaque pixels are deduplicated on RGB and stored with alpha forced to 255, matching the lookup performed by `indexize()` so a subsequent `sheet.indexize(palette)` call resolves without throwing on missing colors. By default colors are sorted darkest-first by perceived luminance ( {@link Color32.luminance } ); pass `{ sort: 'none' }` to keep the row-major scan order of the source image. Image loading goes through {@link AssetLoader.loadImage } , so the call shares cache and in-flight deduplication with {@link SpriteSheet.load } . The destination range is validated before any write, so the palette is never left partially mutated: if the collected colors would not fit (`startSlot < 1` or `startSlot + count > palette.size`), the method throws without touching any slot.
@paramurl - Path or URL to the PNG file.@parampalette - Target palette to populate.@paramstartSlot - First palette slot to write into.@paramoptions - Optional configuration.@paramoptions.sort - Color ordering. Defaults to `'luminance'`.@returnsRegistered colors in palette-write order.@throwsError if the image cannot be loaded.@throwsRangeError if the discovered colors do not fit in the palette starting at `startSlot`.
loadColorsIntoPalette
(const SPRITE_URL: "/sprites/logo-1.png"SPRITE_URL, this.Demo.palette: Palette
@type{Palette | null}
palette
, const SPRITE_BASE: 10SPRITE_BASE);
// Step 2: loadIndexed builds the GPU sheet + a full-image source rectangle. const const indexed: Promise<IndexedSpriteLoadResult>indexed = await class SpriteSheet
Sprite-sheet wrapper around a loaded image asset. The class keeps the original image available for CPU-side inspection while lazily creating and caching a GPU texture for rendering. When possible, `load()` also pre-decodes the source into an `ImageBitmap` so texture uploads preserve pixel-art alpha and color values more reliably. After calling `indexize()`, the sheet stores palette indices rather than RGBA data. The GPU texture becomes an `r8uint` format uploaded via `writeTexture`. The original RGBA bytes are retained so `reindexize()` can re-convert without reloading the image.
@since0.1.0
SpriteSheet
.
SpriteSheet.loadIndexed(url: string, palette: Palette, startSlot: number, options?: {
    sort?: "luminance" | "none";
}): Promise<IndexedSpriteLoadResult>
Convenience one-call path for palette-indexed sprite setup. This combines: 1) {@link SpriteSheet.loadColorsIntoPalette } 2) {@link SpriteSheet.load } 3) {@link SpriteSheet.indexize } It returns the indexized sheet plus a full-frame source rectangle and the colors that were written into the palette. Callers still control when to activate the palette via `BT.paletteSet(palette)`.
@paramurl - Path or URL to the PNG file.@parampalette - Target palette used for both registration and indexization.@paramstartSlot - First palette slot to write discovered colors into.@paramoptions - Optional color-sort behavior for registration.@paramoptions.sort - Color ordering for palette registration.@returnsObject with `sheet`, `srcRect`, and registered `colors`.
loadIndexed
(const SPRITE_URL: "/sprites/logo-1.png"SPRITE_URL, this.Demo.palette: Palette
@type{Palette | null}
palette
, const SPRITE_BASE: 10SPRITE_BASE, { sort?: "none" | "luminance" | undefinedsort: 'none' });
this.Demo.spriteSheet: SpriteSheet | null
@type{SpriteSheet | null}
spriteSheet
= const indexed: Promise<IndexedSpriteLoadResult>indexed.sheet;
this.Demo.spriteRect: Rect2i | null
@type{Rect2i | null}
spriteRect
= const indexed: Promise<IndexedSpriteLoadResult>indexed.srcRect;
// Install the shared UI kit colors. The scene owns slots 1-5 and the logo owns // slots 10-12, so the kit's default range (240-251, at the top of the palette) // is provably free. The scene keeps its own PipBoy colors - the kit colors are // only for the on-canvas hint and fallback note drawn in render(). import applyThemeapplyTheme(this.Demo.palette: Palette
@type{Palette | null}
palette
);
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.paletteSet: (palette: Palette) => void
Stores the active engine palette. Use this to swap the **entire palette** (e.g. switch between a day and night theme). After this call the renderer uploads the new palette uniform on the next frame. **Palette-value swap (change what a slot looks like):** mutate the live {@link BT.palette } in place with `palette.set(slot, newColor)`. The renderer uploads dirty slots on the next frame; no `paletteSet()` or {@link BT.spritesRefresh } needed. **Palette-layout swap (same colors, different slot positions):** build a new palette with the same colors at new indices, call `paletteSet()`, then call {@link BT.spritesRefresh } so every sprite sheet re-maps its original RGBA pixels against the new slot layout.
@since1.0.3@parampalette - Palette to make active.
paletteSet
(this.Demo.palette: Palette
@type{Palette | null}
palette
);
this.Demo.size: Vector2isize = new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(this.Demo.spriteSheet: SpriteSheet | null
@type{SpriteSheet | null}
spriteSheet
.SpriteSheet.size: Vector2i
Gets the sprite-sheet dimensions in pixels.
@returnsSheet dimensions. Changes after a hot-replace image swap with different dimensions - any `srcRect` a demo holds onto is the demo's own responsibility to reconcile.
size
.Vector2i.x: number
Horizontal component (defaults to 0).
x
, this.Demo.spriteSheet: SpriteSheet | null
@type{SpriteSheet | null}
spriteSheet
.SpriteSheet.size: Vector2i
Gets the sprite-sheet dimensions in pixels.
@returnsSheet dimensions. Changes after a hot-replace image swap with different dimensions - any `srcRect` a demo holds onto is the demo's own responsibility to reconcile.
size
.Vector2i.y: number
Vertical component (defaults to 0).
y
);
this.Demo.pos: Vector2ipos = new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(
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
.displaySize: Vector2i
Active logical render resolution in pixels. This is the game/simulation coordinate space configured by the demo, not the canvas element's CSS size. Each read returns a clone.
@since1.0.4@returnsConfigured logical size, or `Vector2i.zero()` before initialization.
displaySize
.Vector2i.x: number
Horizontal component (defaults to 0).
x
/ 2 - this.Demo.size: Vector2isize.Vector2i.x: number
Horizontal component (defaults to 0).
x
/ 2),
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
.displaySize: Vector2i
Active logical render resolution in pixels. This is the game/simulation coordinate space configured by the demo, not the canvas element's CSS size. Each read returns a clone.
@since1.0.4@returnsConfigured logical size, or `Vector2i.zero()` before initialization.
displaySize
.Vector2i.y: number
Vertical component (defaults to 0).
y
/ 2 - this.Demo.size: Vector2isize.Vector2i.y: number
Vertical component (defaults to 0).
y
/ 2),
); // Keep prevPos in sync with the real starting position so the very first // render() does not try to smoothly slide in from the (160, 120) placeholder. this.Demo.prevPos: Vector2iprevPos = this.Demo.pos: Vector2ipos; // Post-process requires WebGPU; software renderer skips the whole CRT stack. this.Demo.effectsAvailable: booleaneffectsAvailable = import isAvailableisAvailable(); if (!this.Demo.effectsAvailable: booleaneffectsAvailable) { // Software renderer: update() never runs the glitch machine, so there is nothing to seed. return true; } // --- Pixel tier: chunky band glitch on the index buffer --- this.Demo.pixelGlitch: PixelGlitch | null
@type{PixelGlitch | null}
pixelGlitch
= new new PixelGlitch(): PixelGlitch
Chunky pixel-aligned horizontal glitch: every Nth row of source pixels gets a random horizontal shift. Shifts snap to integer source-pixel offsets so palette indices move whole-texel (no RGB resampling). Pixel-tier: runs on the logical `r8uint` framebuffer (palette indices).
@since1.0.3
PixelGlitch
();
this.Demo.pixelGlitch: PixelGlitch
@type{PixelGlitch | null}
pixelGlitch
.PixelGlitch.bandHeight: number
Height of each glitch band in source pixels. Each band gets a single shift value, so larger bands produce chunkier glitches.
bandHeight
= import PIXEL_GLITCH_BAND_HEIGHTPIXEL_GLITCH_BAND_HEIGHT;
this.Demo.pixelGlitch: PixelGlitch
@type{PixelGlitch | null}
pixelGlitch
.PixelGlitch.intensity: number
Glitch strength in `[0, 1]`. Scales the per-band horizontal shift magnitude. Bands are shifted when their hash exceeds ~0.85 (~15% of bands). `0` disables.
intensity
= 0; // state machine raises this during hshift bursts
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.effectAdd: (effect: Effect) => void
Appends a fullscreen post-processing effect to whichever chain matches its declared {@link Effect.tier } . - `tier='pixel'` -> pixel chain (logical resolution). - `tier='display'` -> display chain (output resolution); requires `drawingBufferSize` in effective hardware settings (`configure()` or `defaultConfig()`). Effects run in registration order within each tier. The pixel chain runs first, followed by the upscale pass, followed by the display chain. Each {@link Effect } instance owns its own GPU resources and may be mutated each frame from demo code.
@since1.0.3@parameffect - Effect instance to append. When the engine is not ready, shows a canvas error instead of throwing.
effectAdd
(this.Demo.pixelGlitch: PixelGlitch
@type{PixelGlitch | null}
pixelGlitch
);
// --- Display tier: hand-built CRT chain (resting values; glitch machine mutates some) --- this.Demo.barrel: BarrelDistortion | null
@type{BarrelDistortion | null}
barrel
= new new BarrelDistortion(): BarrelDistortion
Barrel distortion that warps UVs outward from the screen center. Display-tier: operates on the upscaled output. Applying this in the pixel tier (logical 320x240) discretizes the curve onto the source texel grid, which CSS upscale then magnifies into visible step artifacts. At output resolution the curve has enough resolution to express smoothly. The math comes from Timothy Lottes's public-domain `crt-lottes.glsl`: `warp(uv) = uv + delta * d2 * curvature` where `delta = uv - 0.5` and `d2 = dot(delta, delta)`.
@since1.0.3
BarrelDistortion
();
this.Demo.barrel: BarrelDistortion
@type{BarrelDistortion | null}
barrel
.BarrelDistortion.curvature: number
Curvature strength. Typical values: - `0.02` - subtle, like a flat-screen monitor. - `0.05` - moderate desktop CRT. - `0.10` - heavy small CRT or pocket TV.
curvature
= 0.05;
this.Demo.aberration: ChromaticAberration | null
@type{ChromaticAberration | null}
aberration
= new new ChromaticAberration(): ChromaticAberration
RGB channel offset that simulates lens chromatic aberration: red samples left of the fragment, blue samples right, green stays centered. Display-tier: spreads color along the lens axis. At logical resolution the single-pixel offset is too coarse and reads as a glitch instead of a soft fringe.
@since1.0.3
ChromaticAberration
();
this.Demo.aberration: ChromaticAberration
@type{ChromaticAberration | null}
aberration
.ChromaticAberration.aberration: number
Channel offset in display-chain (output) pixels. Reasonable values are `0.5` to `3.0`. Set to `0` to disable.
aberration
= import ABERRATION_BASEABERRATION_BASE;
this.Demo.interference: Interference | null
@type{Interference | null}
interference
= new new Interference(): Interference
Per-row horizontal jitter that simulates analog signal interference. Each output row gets a deterministic random horizontal offset seeded by row index and time. Row offsets are stable for one frame and re-seed every frame, producing a buzzing-noise feel. Display-tier. Drives jitter from {@link time } ; demos typically pass `BT.ticks / BT.targetFPS`.
@since1.0.3
Interference
();
this.Demo.interference: Interference
@type{Interference | null}
interference
.Interference.amount: number
Maximum horizontal offset as a UV fraction (e.g. `0.06` shifts the row by up to ~6% of the image width). Set to `0` to disable.
amount
= 0;
this.Demo.rollLine: RollLine | null
@type{RollLine | null}
rollLine
= new new RollLine(): RollLine
Slowly scrolling vertical interference band that brightens a horizontal stripe of the image. Combination of three cosines + smoothstep gives the stripe a soft top/bottom edge. Display-tier. Demo drives {@link time } (typically `BT.ticks / BT.targetFPS`).
@since1.0.3
RollLine
();
this.Demo.rollLine: RollLine
@type{RollLine | null}
rollLine
.RollLine.amount: number
Roll line amplitude (mix factor onto a brightness boost).
amount
= 0.1;
this.Demo.rollLine: RollLine
@type{RollLine | null}
rollLine
.RollLine.speed: number
Scroll speed multiplier; final scroll velocity = `time * speed`.
speed
= 1.0;
this.Demo.scanlines: Scanlines | null
@type{Scanlines | null}
scanlines
= new new Scanlines(): Scanlines
CRT scanlines: alternating bright/dark horizontal bands aligned to the source vertical resolution. Display-tier: at output resolution there is enough vertical pixels for scanlines to read as alternating bright/dark bands. At logical 320x240 the Gaussian weight quantizes to one of two values per source row and you lose the smooth fade.
@since1.0.3
Scanlines
();
this.Demo.scanlines: Scanlines
@type{Scanlines | null}
scanlines
.Scanlines.amount: number
Scanline mix amount in `[0, 1]`. 0 disables.
amount
= 0.55;
this.Demo.scanlines: Scanlines
@type{Scanlines | null}
scanlines
.Scanlines.strength: number
Negative gaussian falloff parameter for scanline brightness. More negative values produce sharper dark bands. PipBoy reference: `-8.0`.
strength
= -8;
this.Demo.scanlines: Scanlines
@type{Scanlines | null}
scanlines
.Scanlines.density: number
Number of scanline cycles vertically. Should match the demo's logical source vertical resolution so each "source pixel row" maps to one scanline cycle. Defaults to `240`, the most common pixel-art height. Set to e.g. `200` for VGA-style 320x200 games or `144` for Game Boy resolution.
density
= 240;
this.Demo.mask: RGBMask | null
@type{RGBMask | null}
mask
= new new RGBMask(): RGBMask
CRT shadow mask: per-pixel R/G/B vertical-stripe pattern with darkened cell borders, simulating the phosphor grille of an aperture-grille CRT. Display-tier: at output resolution there are enough output pixels per mask cell to read as colored stripes. The cell pitch (in output pixels) is the {@link size } parameter. Math is a direct WGSL port of the libretro `crt-lottes.glsl` mask code.
@since1.0.3
RGBMask
();
this.Demo.mask: RGBMask
@type{RGBMask | null}
mask
.RGBMask.intensity: number
Mask brightness mix amount in `[0, 1]`. 0 hides the mask.
intensity
= 0.18;
this.Demo.mask: RGBMask
@type{RGBMask | null}
mask
.RGBMask.size: number
Mask cell pitch in output (display-chain) pixels. Smaller = denser mask.
size
= 6;
this.Demo.mask: RGBMask
@type{RGBMask | null}
mask
.RGBMask.border: number
Border darkening within each mask cell. 0 disables, 1 strong.
border
= 0.5;
this.Demo.vignette: Vignette | null
@type{Vignette | null}
vignette
= new new Vignette(): Vignette
Edge-darkening vignette: smooth radial fade from full brightness at the center to black at the corners. Display-tier: applies to the whole simulated screen, not the underlying pixel art.
@since1.0.3
Vignette
();
this.Demo.vignette: Vignette
@type{Vignette | null}
vignette
.Vignette.amount: number
Vignette darkening exponent. Higher values produce a stronger vignette with a sharper falloff. PipBoy reference: `0.2`. Set to `0` to disable.
amount
= 0.35;
this.Demo.noise: Noise | null
@type{Noise | null}
noise
= new new Noise(): Noise
Additive per-pixel pseudo-random noise. Reseeds each frame from {@link time } so the noise pattern animates. Display-tier.
@since1.0.3
Noise
();
this.Demo.noise: Noise
@type{Noise | null}
noise
.Noise.amount: number
Noise amplitude as a `[-amount, +amount]` additive perturbation on each channel. Reasonable values are `0.005` to `0.05`. Set to `0` to disable.
amount
= import NOISE_BASENOISE_BASE;
this.Demo.flicker: Flicker | null
@type{Flicker | null}
flicker
= new new Flicker(): Flicker
Brightness multiplier - the simplest CRT animation knob. Demos drive {@link amount } per frame to simulate flicker (e.g. with `0.95 + sin(t) * 0.05`). The effect is intentionally trivial so the demo controls the pattern; for procedural noise-driven flicker, combine with the {@link Noise } effect. Display-tier.
@since1.0.3
Flicker
();
this.Demo.flicker: Flicker
@type{Flicker | null}
flicker
.Flicker.amount: number
Brightness multiplier. `1` is unmodulated; values below `1` darken the frame. The demo typically updates this each frame from a sin wave or random source.
amount
= import FLICKER_BASEFLICKER_BASE;
this.Demo.bloom: Bloom | null
@type{Bloom | null}
bloom
= new new Bloom(): Bloom
Single-pass box-blur bloom. Samples a 5x5 neighborhood (25 taps) around each fragment, averages, then mixes with the original color by {@link glow } . {@link spread } scales the texel offset so the bloom radius can be tuned independently of the source resolution. Display-tier: bloom mixes neighboring pixels into intermediate hues that are not in the active palette. Running it in pixel space would violate the palette-pixel aesthetic; running it on the upscaled output reads as the warm phosphor glow of an old monitor instead. The implementation matches the original PipBoy bloom shader. A future optimization would be a two-pass separable Gaussian (5 + 5 = 10 taps); add it once a GPU perf test demands it.
@since1.0.3
Bloom
();
this.Demo.bloom: Bloom
@type{Bloom | null}
bloom
.Bloom.spread: number
Texel offset multiplier for the box-blur kernel.
spread
= 3.0;
this.Demo.bloom: Bloom
@type{Bloom | null}
bloom
.Bloom.glow: number
Mix factor between the original sample and the blurred neighborhood.
glow
= 0.18;
// Register every display-tier effect in draw order (first added runs first). for (const const fx: anyfx of [ this.Demo.barrel: BarrelDistortion
@type{BarrelDistortion | null}
barrel
,
this.Demo.aberration: ChromaticAberration
@type{ChromaticAberration | null}
aberration
,
this.Demo.interference: Interference
@type{Interference | null}
interference
,
this.Demo.rollLine: RollLine
@type{RollLine | null}
rollLine
,
this.Demo.scanlines: Scanlines
@type{Scanlines | null}
scanlines
,
this.Demo.mask: RGBMask
@type{RGBMask | null}
mask
,
this.Demo.vignette: Vignette
@type{Vignette | null}
vignette
,
this.Demo.noise: Noise
@type{Noise | null}
noise
,
this.Demo.flicker: Flicker
@type{Flicker | null}
flicker
,
this.Demo.bloom: Bloom
@type{Bloom | null}
bloom
,
]) {
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.effectAdd: (effect: Effect) => void
Appends a fullscreen post-processing effect to whichever chain matches its declared {@link Effect.tier } . - `tier='pixel'` -> pixel chain (logical resolution). - `tier='display'` -> display chain (output resolution); requires `drawingBufferSize` in effective hardware settings (`configure()` or `defaultConfig()`). Effects run in registration order within each tier. The pixel chain runs first, followed by the upscale pass, followed by the display chain. Each {@link Effect } instance owns its own GPU resources and may be mutated each frame from demo code.
@since1.0.3@parameffect - Effect instance to append. When the engine is not ready, shows a canvas error instead of throwing.
effectAdd
(const fx: anyfx);
} // 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: 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;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.random: Random
Default engine PRNG (live reference - not a copy). Time-seeded when the engine singleton is created. Call {@link BT.randomSeed } for a reproducible run. Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.
@since1.5.0@returnsThe shared {@link Random} instance.@exampleBT.randomSeed(42); BT.random.int(150, 420); BT.random.pick(['a', 'b', 'c']);
random
.Random.int(minOrMaxExclusive: number, maxExclusive?: number): number
Returns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).
@paramminOrMaxExclusive - When alone, exclusive upper bound from 0; otherwise inclusive min.@parammaxExclusive - Exclusive upper bound when two arguments are passed.@returnsWhole number in the half-open range.@since1.5.0
int
(import GLITCH_COOLDOWN_MINGLITCH_COOLDOWN_MIN, import GLITCH_COOLDOWN_MAXGLITCH_COOLDOWN_MAX);
this.Demo.glitchTicksLeft: numberglitchTicksLeft = 0; this.Demo.glitchDuration: numberglitchDuration = 0; this.Demo.glitchType: stringglitchType = 'none'; this.Demo.glitchPeak: numberglitchPeak = 0; return true; } Demo.update(): void
Called zero or more times per frame at the fixed timestep declared by `targetFPS`. The accumulator pattern ensures the target rate is met on average, but a single frame may invoke this multiple times (catch-up) or not at all. Update simulation, timers, and input-driven state here. This is a hot path. Minimize allocations, reuse objects, and prefer in-place vector operations where possible. Avoid rendering work here; draw in `render()` instead.
update
() {
// --- Bounce logic (same rules as the Basics demo; game logic lives only in update()) --- // Remember where the logo was BEFORE this tick moves it, so render() can // draw a smooth in-between position instead of a pop. this.Demo.prevPos: Vector2iprevPos = this.Demo.pos: Vector2ipos; // Move the logo by adding speed to position - one step per tick. this.Demo.pos: Vector2ipos = this.Demo.pos: Vector2ipos.Vector2i.add(other: Vector2i): Vector2i
Adds another vector and returns the result as a new vector.
@paramother - Vector to add.@returnsNew vector with summed components.
add
(this.Demo.speed: Vector2ispeed);
// Left/right wall test. pos is the sprite's top-left corner, so the right // edge is at pos.x + size.x. We compare against displaySize.x - size.x. if (this.Demo.pos: Vector2ipos.Vector2i.x: number
Horizontal component (defaults to 0).
x
<= 0 || this.Demo.pos: Vector2ipos.Vector2i.x: number
Horizontal component (defaults to 0).
x
>=
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
.displaySize: Vector2i
Active logical render resolution in pixels. This is the game/simulation coordinate space configured by the demo, not the canvas element's CSS size. Each read returns a clone.
@since1.0.4@returnsConfigured logical size, or `Vector2i.zero()` before initialization.
displaySize
.Vector2i.x: number
Horizontal component (defaults to 0).
x
- this.Demo.size: Vector2isize.Vector2i.x: number
Horizontal component (defaults to 0).
x
) {
// Flip horizontal direction (multiply speed.x by -1). this.Demo.speed: Vector2ispeed.Vector2i.x: number
Horizontal component (defaults to 0).
x
= -this.Demo.speed: Vector2ispeed.Vector2i.x: number
Horizontal component (defaults to 0).
x
;
this.Demo.bounces: numberbounces++;
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.assignTag: (label?: string) => void
Places a labeled marker on the overlay timing chart at the current tick. Requires `isOverlayTimingChartEnabled: true` in `configure()`. Tags scroll with the chart history and are pruned when they leave the visible window. Empty labels become `"Untitled"`. Chart width resets add an automatic `"Start"` tag.
@since1.1.0@paramlabel - Short event name (for example `'Round start'`).
assignTag
('H');
} // Top/bottom wall test uses the same idea on the y axis. if (this.Demo.pos: Vector2ipos.Vector2i.y: number
Vertical component (defaults to 0).
y
<= 0 || this.Demo.pos: Vector2ipos.Vector2i.y: number
Vertical component (defaults to 0).
y
>=
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
.displaySize: Vector2i
Active logical render resolution in pixels. This is the game/simulation coordinate space configured by the demo, not the canvas element's CSS size. Each read returns a clone.
@since1.0.4@returnsConfigured logical size, or `Vector2i.zero()` before initialization.
displaySize
.Vector2i.y: number
Vertical component (defaults to 0).
y
- this.Demo.size: Vector2isize.Vector2i.y: number
Vertical component (defaults to 0).
y
) {
this.Demo.speed: Vector2ispeed.Vector2i.y: number
Vertical component (defaults to 0).
y
= -this.Demo.speed: Vector2ispeed.Vector2i.y: number
Vertical component (defaults to 0).
y
;
this.Demo.bounces: numberbounces++;
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.assignTag: (label?: string) => void
Places a labeled marker on the overlay timing chart at the current tick. Requires `isOverlayTimingChartEnabled: true` in `configure()`. Tags scroll with the chart history and are pruned when they leave the visible window. Empty labels become `"Untitled"`. Chart width resets add an automatic `"Start"` tag.
@since1.1.0@paramlabel - Short event name (for example `'Round start'`).
assignTag
('V');
} // Animated CRT uniforms need elapsed time; skip when effects are unavailable. if (this.Demo.effectsAvailable: booleaneffectsAvailable) { 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
.timeSeconds: number
Fixed-step elapsed time in seconds (`BT.ticks * BT.deltaSeconds`).
@since1.0.4@returnsElapsed fixed-step time in seconds since initialization.
timeSeconds
;
this.Demo.rollLine: RollLine | null
@type{RollLine | null}
rollLine
.RollLine.time: number
Wall-clock seconds; demos typically drive this each frame.
time
= const seconds: numberseconds;
this.Demo.noise: Noise | null
@type{Noise | null}
noise
.Noise.time: number
Wall-clock seconds; reseeds the noise each frame.
time
= const seconds: numberseconds;
this.Demo.interference: Interference | null
@type{Interference | null}
interference
.Interference.time: number
Wall-clock seconds; reseeds the row offsets each frame.
time
= const seconds: numberseconds;
} else { return; } // --- Glitch state machine (PipBoy CRT demo pattern) --- if (this.Demo.glitchTicksLeft: numberglitchTicksLeft > 0) { // Inside a burst: build a 0 -> 1 -> 0 envelope so the effect ramps in and out. // t goes from 0 at burst start to 1 on the last tick; sin(t * PI) is a smooth hump. const const t: numbert = 1 - (this.Demo.glitchTicksLeft: numberglitchTicksLeft - 1) / this.Demo.glitchDuration: numberglitchDuration; const const envelope: anyenvelope = Math.sin(const t: numbert * Math.PI); import applyGlitchUniformsapplyGlitchUniforms(this, const envelope: anyenvelope); this.Demo.glitchTicksLeft: numberglitchTicksLeft--; if (this.Demo.glitchTicksLeft: numberglitchTicksLeft <= 0) { // Burst finished - return effect uniforms to calm resting values. 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;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.random: Random
Default engine PRNG (live reference - not a copy). Time-seeded when the engine singleton is created. Call {@link BT.randomSeed } for a reproducible run. Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.
@since1.5.0@returnsThe shared {@link Random} instance.@exampleBT.randomSeed(42); BT.random.int(150, 420); BT.random.pick(['a', 'b', 'c']);
random
.Random.int(minOrMaxExclusive: number, maxExclusive?: number): number
Returns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).
@paramminOrMaxExclusive - When alone, exclusive upper bound from 0; otherwise inclusive min.@parammaxExclusive - Exclusive upper bound when two arguments are passed.@returnsWhole number in the half-open range.@since1.5.0
int
(import GLITCH_COOLDOWN_MINGLITCH_COOLDOWN_MIN, import GLITCH_COOLDOWN_MAXGLITCH_COOLDOWN_MAX);
} return; } // Idle between bursts: count down cooldown ticks. this.Demo.glitchCooldown: numberglitchCooldown--; if (this.Demo.glitchCooldown: numberglitchCooldown <= 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: 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;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.random: Random
Default engine PRNG (live reference - not a copy). Time-seeded when the engine singleton is created. Call {@link BT.randomSeed } for a reproducible run. Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.
@since1.5.0@returnsThe shared {@link Random} instance.@exampleBT.randomSeed(42); BT.random.int(150, 420); BT.random.pick(['a', 'b', 'c']);
random
.Random.pick<string>(arr: readonly string[]): string
Returns one element chosen uniformly from a non-empty array.
@paramarr - Array to pick from; must contain at least one element.@returnsChosen element.@since1.5.0
pick
(import GLITCH_TYPES_CHROMAGLITCH_TYPES_CHROMA);
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;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.random: Random
Default engine PRNG (live reference - not a copy). Time-seeded when the engine singleton is created. Call {@link BT.randomSeed } for a reproducible run. Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.
@since1.5.0@returnsThe shared {@link Random} instance.@exampleBT.randomSeed(42); BT.random.int(150, 420); BT.random.pick(['a', 'b', 'c']);
random
.Random.int(minOrMaxExclusive: number, maxExclusive?: number): number
Returns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).
@paramminOrMaxExclusive - When alone, exclusive upper bound from 0; otherwise inclusive min.@parammaxExclusive - Exclusive upper bound when two arguments are passed.@returnsWhole number in the half-open range.@since1.5.0
int
(import GLITCH_ACTIVE_MINGLITCH_ACTIVE_MIN, import GLITCH_ACTIVE_MAXGLITCH_ACTIVE_MAX);
this.Demo.glitchTicksLeft: 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;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.random: Random
Default engine PRNG (live reference - not a copy). Time-seeded when the engine singleton is created. Call {@link BT.randomSeed } for a reproducible run. Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.
@since1.5.0@returnsThe shared {@link Random} instance.@exampleBT.randomSeed(42); BT.random.int(150, 420); BT.random.pick(['a', 'b', 'c']);
random
.Random.float(min: number, max: number): number
Returns the next pseudo-random float in [min, max).
@parammin - Inclusive lower bound.@parammax - Exclusive upper bound.@returnsFloat in [min, max).@since1.5.0
float
(import GLITCH_INTENSITY_MINGLITCH_INTENSITY_MIN, import GLITCH_INTENSITY_MAXGLITCH_INTENSITY_MAX);
// Fresh seed so PixelGlitch band noise looks different each burst. this.Demo.pixelGlitch: PixelGlitch | null
@type{PixelGlitch | null}
pixelGlitch
.PixelGlitch.seed: number
Per-glitch random seed. Change between glitches to vary the band noise pattern.
seed
=
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.random: Random
Default engine PRNG (live reference - not a copy). Time-seeded when the engine singleton is created. Call {@link BT.randomSeed } for a reproducible run. Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.
@since1.5.0@returnsThe shared {@link Random} instance.@exampleBT.randomSeed(42); BT.random.int(150, 420); BT.random.pick(['a', 'b', 'c']);
random
.Random.float(min: number, max: number): number
Returns the next pseudo-random float in [min, max).
@parammin - Inclusive lower bound.@parammax - Exclusive upper bound.@returnsFloat in [min, max).@since1.5.0
float
(0, 1000);
} } Demo.render(): void
Called once per `requestAnimationFrame` tick (browser refresh rate). Issue all draw calls for the current frame here. When {@link HardwareSettings.isOverlayEnabled } is `true` (default), the engine draws a screen-space overlay HUD after this method returns (present FPS, target FPS, draw calls, frame/update()/render() timings, backend, demo title). Optional {@link overlayRows } adds stacked bars above the footer. Demos do not need to duplicate engine overlay text. Reserve about ~42 px at the top and space for the bottom palette grid (or ~13 px when {@link HardwareSettings.isOverlayPaletteEnabled } is `false`) at the bottom (plus ~14 px per custom overlay row) for overlay bars, or disable the overlay in `configure()` when using custom full-screen HUD layouts. This is a hot path. Batch draws by texture to reduce GPU state changes and reuse Color32/Vector2i instances instead of allocating per frame. Avoid mutating the simulation state here unless it is strictly visual.
render
() {
// Clear the logical framebuffer to the PipBoy background color. // C_BG is palette index 1 set in init() - almost black with a faint green tint.
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.clear: (paletteIndex: number) => void
Sets the frame clear color using a palette index. The renderer uses this color when clearing the full display at the start of the next frame.
@since0.1.0@parampaletteIndex - Palette index for the full-screen clear pass.
clear
(const C_BG: 1C_BG);
// Blend prevPos toward pos by BT.renderAlpha to get the logo's true position at // this exact render moment, instead of only its last-tick position. Same fix, // same reason, as the Basics demo - see the big comment above its render(). const const drawPos: Vector2idrawPos = class Vector2i
Integer 2D vector for pixel-perfect positioning. Used for points, sizes, directions, and camera offsets throughout the engine. The API includes both allocation-free `*To()` / `*InPlace()` variants and convenience methods that return new vectors.
@since0.1.0
Vector2i
.Vector2i.lerp(a: Vector2i, b: Vector2i, t: number): Vector2i
Linearly interpolates between two vectors. Result is truncated to integers. t is clamped to [0, 1].
@parama - Start vector.@paramb - End vector.@paramt - Interpolation factor, clamped to [0, 1] (0 = a, 1 = b).@returnsNew interpolated vector.
lerp
(this.Demo.prevPos: Vector2iprevPos, this.Demo.pos: Vector2ipos,
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.renderAlpha: number
Fractional progress between the last completed fixed update and the next. Intended for interpolating render state between fixed-update steps.
@since1.3.0@returnsInterpolation alpha in `[0, 1)`.
renderAlpha
);
// Draw the bouncing logo at its smoothed position (updated in update(), not here). // paletteOffset 0 keeps the sprite's original indexed colors from SPRITE_BASE.
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
.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => void
Draws a sprite region from an indexed sprite sheet. Sprite draws are batched internally. Grouping draws from the same {@link SpriteSheet } minimizes batch flushes and reduces GPU state changes. The sprite sheet must have been converted to palette indices via `spriteSheet.indexize(palette)` before the first draw call. Prefer `SpriteSheet.loadIndexed(...)` for one-call setup. **Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1. Index 0 is always transparent and is discarded by the fragment shader. The final palette lookup is `storedIndex + paletteOffset`, so: - `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`. `palette[0]` is never reachable because stored indices start at 1. - `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`, and so on. Use this for palette-swap effects such as team colors or damage flashes. **Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's robust buffer access returns 0 for every component; because the fragment shader forces alpha to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also produces out-of-bounds black pixels.
@since0.1.0@paramspriteSheet - Indexed sprite sheet.@paramsrcRect - Source rectangle within the sprite sheet, in pixels.@paramdestPos - Destination top-left position in display coordinates.@parampaletteOffset - Shift added to every stored pixel index before palette lookup (default 0).@exampleBT.drawSprite(sheet, new Rect2i(0, 0, 16, 16), new Vector2i(10, 10)); BT.drawSprite(sheet, new Rect2i(0, 0, 16, 16), new Vector2i(10, 10), 16); // blue team
drawSprite
(this.Demo.spriteSheet: SpriteSheet | null
@type{SpriteSheet | null}
spriteSheet
, this.Demo.spriteRect: Rect2i | null
@type{Rect2i | null}
spriteRect
, const drawPos: Vector2idrawPos, 0);
// On-canvas text drawn with the shared UI kit: a borderless label group (no // ui.panel() call, so no box) pinned to the top-left corner. Small margin and // padding keep it tucked near the edge, like the old hand-drawn hint. import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { margin: numbermargin: 2, pad: numberpad: 2 }); // Hint: the engine overlay (FPS, position, CRT status) toggles with Backquote // or the small symbol in the bottom-left corner of the upscaled canvas. import uiui.label('Press ~ or click/tap the symbol below', { color: stringcolor: 'dim' }); // Software renderer: warn that the CRT look is missing. The shared note was // split into two lines up top (FALLBACK_LINES) so it fits the screen width. if (!this.Demo.effectsAvailable: booleaneffectsAvailable) { for (const const line: anyline of const FALLBACK_LINES: anyFALLBACK_LINES) { import uiui.label(const line: anyline, { color: stringcolor: 'warm' }); } } import uiui.end(); // Position, bounces, CRT stack, and glitch readout live in overlayRows(), not here. // After this pass finishes, WebGPU runs the CRT post-process chain on the result. } /** * Position, bounces, CRT status, and glitch readout (same rows as Basics plus enhanced extras). * * @returns {readonly { leftText: string }[]} */
Demo.overlayRows(): readonly {
    leftText: string;
}[]
Position, bounces, CRT status, and glitch readout (same rows as Basics plus enhanced extras).
@returns
overlayRows
() {
this.Demo.overlayRowData: {}overlayRowData[0].leftText = `Position (${this.Demo.pos: Vector2ipos.Vector2i.x: number
Horizontal component (defaults to 0).
x
}, ${this.Demo.pos: Vector2ipos.Vector2i.y: number
Vertical component (defaults to 0).
y
})`;
this.Demo.overlayRowData: {}overlayRowData[1].leftText = `Bounces ${this.Demo.bounces: numberbounces}`; if (this.Demo.effectsAvailable: booleaneffectsAvailable) { this.Demo.overlayRowData: {}overlayRowData[2].leftText = 'CRT stack ON'; const const glitchLabel: anyglitchLabel = import GLITCH_LABELSGLITCH_LABELS[this.Demo.glitchType: stringglitchType] ?? 'NONE'; const const glitchValue: anyglitchValue = this.Demo.glitchTicksLeft: numberglitchTicksLeft > 0 ? Math.round(this.Demo.glitchPeak: numberglitchPeak * 100) : 0; this.Demo.overlayRowData: {}overlayRowData[3].leftText = `Glitch ${const glitchLabel: anyglitchLabel} ${String(const glitchValue: anyglitchValue).padStart(2, '0')}%`; } else { // Software renderer: no CRT stack, so the glitch machine never fires. The full // explanation lives on the canvas itself (see render()), not in the overlay. this.Demo.overlayRowData: {}overlayRowData[2].leftText = 'CRT stack OFF (software)'; this.Demo.overlayRowData: {}overlayRowData[3].leftText = 'Glitch NONE'; } return this.Demo.overlayRowData: {}overlayRowData; } } function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
(class Demo
Basics demo plus a hand-built CRT post-process chain and periodic glitch bursts.
@implementsIBTDemo
Demo
);