// @pageTitle BLIT386 Demo – Logo Low-Res
// @description The BLIT386 logo on a tiny 80x60 screen, upscaled nearest-neighbor inside a monochrome CRT stack.
//
// Logo Low-Res: the BLIT386 logo on a very chunky low-res screen, wrapped
// in the same Orava B/W CRT stack used in Sprite Effects demo.
//
// What you will see:
//   - The logo sprite from Basics demo centered on a tiny 80x60 pixel canvas.
//     That is one quarter of the usual 320x240 in each direction - even smaller
//     than an old Game Boy screen (160x144).
//   - The engine upscales that 80x60 picture 3x to 240x180 using nearest-neighbor
//     filtering, so each logical pixel becomes a big hard-edged 3x3 block.
//   - On top of that upscaled image the engine runs the Tesla Orava B/W CRT stack:
//     scanlines, a bright scrolling roll band (RollLine),
//     light noise, brightness waver (Flicker), soft RGB halation (RGBMask), a gentle
//     vignette, soft bloom, and occasional analog-TV fault bursts (horizontal hold,
//     snow, dimming, ghosting, vertical roll).
//   - Post-process needs WebGPU. The software renderer shows the logo without CRT.
//
// Why upscale first and then add CRT?
//   The engine renders the 80x60 palette-indexed picture into a 240x180 RGBA buffer
//   (the drawingBufferSize step). The CRT effects run AFTER that, on the RGBA buffer,
//   so they see 240x180 pixels and can paint convincing curved-tube scanlines across
//   the whole frame - even though the game itself only used 80x60 logical pixels.
//
// Prerequisites: Basics (https://demos.blit386.dev/basics),
//                Sprite Effects (https://demos.blit386.dev/sprite-effects).
//
// Live version: https://demos.blit386.dev/logo-lowres

import {
    
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 GLITCH_TYPES_VROLLGLITCH_TYPES_VROLL } from './shared/crt-glitch.js'; import { import isAvailableisAvailable } from './shared/post-process-backend.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').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 */ // --- Screen dimensions --- // The logical drawing area where BT.draw* calls happen. // 80x60 is our tiny retro resolution - even smaller than a Game Boy screen (160x144). const const DISPLAY_W: 80DISPLAY_W = 80; const const DISPLAY_H: 60DISPLAY_H = 60; // The intermediate buffer size. The engine stretches the 80x60 picture up to 240x180 // (exactly 3x) before the CRT effects run. Each logical pixel becomes a 3x3 block. const const OUTPUT_W: 240OUTPUT_W = 240; const const OUTPUT_H: 180OUTPUT_H = 180; // --- Palette slot numbers --- // Index 0 is always transparent. Our own colors start at index 1. // Think of these numbers as labels on paint jars laid out before painting. // The shared UI kit adds its own 12 colors at slots 240-251 in init(). const const C_BG: 1C_BG = 1; // Light gray background color. const const SPRITE_BASE: 3SPRITE_BASE = 3; // First palette slot reserved for the logo's own colors. // --- Sprite sources --- const const SPRITE_URL: "/sprites/logo-1.png"SPRITE_URL = '/sprites/logo-1.png'; // --- Orava CRT constants --- // These numbers control the base look of the old B/W CRT effect. const const FLICKER_BASE: 1FLICKER_BASE = 1.0; // Normal brightness (1.0 = full, lower = dimmer). const const FLICKER_DIP: 0.78FLICKER_DIP = 0.78; // How dark the screen gets during a "dim" TV fault. const const ABERRATION_BASE: 0ABERRATION_BASE = 0; // No chromatic offset when the set is working correctly. const const NOISE_BASE: 0.038NOISE_BASE = 0.038; // Slight film-grain noise: always present on a real CRT. // A bright band scrolls top-to-bottom on a real CRT (caused by the tube's electron beam). // ROLL_BASE is how bright the band appears; ROLL_SPEED is how fast it moves. const const ROLL_BASE: 0.26ROLL_BASE = 0.26; // Brightness of the scrolling highlight band. const const ROLL_SPEED: 0.92ROLL_SPEED = 0.92; // Scroll speed of the band (higher = faster). const const INTERFERENCE_BASE: 0INTERFERENCE_BASE = 0; // No ghost image when the set is tuned correctly. // --- Analog-TV fault burst settings --- // Every so often the virtual TV loses its signal and shows one of these faults. // Cooldown is how many ticks to wait between faults; active is how long the fault lasts. const const GLITCH_COOLDOWN_MIN: 150GLITCH_COOLDOWN_MIN = 150; // At least ~2.5 seconds between faults (at 60 FPS). const const GLITCH_COOLDOWN_MAX: 420GLITCH_COOLDOWN_MAX = 420; // Up to ~7 seconds between faults. const const GLITCH_ACTIVE_MIN: 4GLITCH_ACTIVE_MIN = 4; // Fault lasts at least 4 ticks (~0.07 s). const const GLITCH_ACTIVE_MAX: 24GLITCH_ACTIVE_MAX = 24; // Fault lasts up to 24 ticks (~0.4 s). const const GLITCH_INTENSITY_MIN: 0.3GLITCH_INTENSITY_MIN = 0.3; // Weakest fault (barely visible). const const GLITCH_INTENSITY_MAX: 0.95GLITCH_INTENSITY_MAX = 0.95; // Strongest fault (almost unwatchable). // --- Occasional subtle band-wobble (pixel-tier PixelGlitch) --- // This is separate from the bigger TV fault bursts. A real CRT sometimes has a // tiny horizontal jitter on random scan lines that is too mild to call a fault. const const BAND_WOBBLE_COOLDOWN_MIN: 100BAND_WOBBLE_COOLDOWN_MIN = 100; // At least ~1.7 seconds between wobbles (at 60 FPS). const const BAND_WOBBLE_COOLDOWN_MAX: 280BAND_WOBBLE_COOLDOWN_MAX = 280; // Up to ~4.7 seconds between wobbles. const const BAND_WOBBLE_ACTIVE_MIN: 3BAND_WOBBLE_ACTIVE_MIN = 3; // Wobble lasts at least 3 ticks (~0.05 s). const const BAND_WOBBLE_ACTIVE_MAX: 10BAND_WOBBLE_ACTIVE_MAX = 10; // Wobble lasts up to 10 ticks (~0.17 s). const const BAND_WOBBLE_INTENSITY: 0.11BAND_WOBBLE_INTENSITY = 0.11; // Peak strength - much milder than a full TV fault. /** * The BLIT386 logo centered on an 80x60 pixel canvas, upscaled 3x, * and wrapped in the Tesla Orava B/W CRT post-process stack. * * @implements {IBTDemo} */ class class Demo
The BLIT386 logo centered on an 80x60 pixel canvas, upscaled 3x, and wrapped in the Tesla Orava B/W CRT post-process stack.
@implementsIBTDemo
Demo
{
// The color palette - a numbered list of every color this demo will use. /** @type {Palette | null} */ Demo.palette: Palette | null
@type{Palette | null}
palette
= null;
// The loaded logo sprite image. null until init() finishes downloading it. /** @type {SpriteSheet | null} */ Demo.spriteSheet: SpriteSheet | null
@type{SpriteSheet | null}
spriteSheet
= null;
// Which part of the sprite sheet to draw (the full image in our case). /** @type {Rect2i | null} */ Demo.spriteRect: Rect2i | null
@type{Rect2i | null}
spriteRect
= null;
// Pixel position of the logo's top-left corner, set once in init(). 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
(0, 0);
// --- Post-process effect objects --- // Each one is created in init() if WebGPU is available. // They stay null when the software renderer is active. /** @type {PixelGlitch | null} */ Demo.pixelGlitch: PixelGlitch | null
@type{PixelGlitch | null}
pixelGlitch
= null; // Pixel-tier: horizontal band shift (V-hold style tear).
/** @type {ChromaticAberration | null} */ Demo.aberration: ChromaticAberration | null
@type{ChromaticAberration | null}
aberration
= null; // Red/blue fringe on bright edges (used during faults).
/** @type {Interference | null} */ Demo.interference: Interference | null
@type{Interference | null}
interference
= null; // Ghost image overlaid on the picture.
/** @type {RollLine | null} */ Demo.rollLine: RollLine | null
@type{RollLine | null}
rollLine
= null; // Bright band that scrolls top to bottom.
/** @type {Scanlines | null} */ Demo.scanlines: Scanlines | null
@type{Scanlines | null}
scanlines
= null; // Dark horizontal lines between pixel rows.
/** @type {RGBMask | null} */ Demo.mask: RGBMask | null
@type{RGBMask | null}
mask
= null; // Subtle phosphor-dot halation.
/** @type {Vignette | null} */ Demo.vignette: Vignette | null
@type{Vignette | null}
vignette
= null; // Darkened corners, like a real tube.
/** @type {Noise | null} */ Demo.noise: Noise | null
@type{Noise | null}
noise
= null; // Constant film-grain noise.
/** @type {Flicker | null} */ Demo.flicker: Flicker | null
@type{Flicker | null}
flicker
= null; // Brightness waver.
/** @type {Bloom | null} */ Demo.bloom: Bloom | null
@type{Bloom | null}
bloom
= null; // Soft glow on bright areas.
// true when WebGPU is active and post-process effects are registered. Demo.effectsAvailable: booleaneffectsAvailable = false; // --- TV fault state machine --- // glitchCooldown counts down ticks until the next fault fires. // glitchTicksLeft counts down ticks while a fault is running. // glitchDuration remembers how long the current fault was supposed to last // (used to compute a smooth 0..1 envelope so faults ease in and out). // glitchType is a string like 'hshift' saying which fault is active. // glitchPeak is how strong this particular fault burst is (0..1). Demo.glitchCooldown: numberglitchCooldown = 0; Demo.glitchTicksLeft: numberglitchTicksLeft = 0; Demo.glitchDuration: numberglitchDuration = 0; Demo.glitchType: stringglitchType = 'none'; Demo.glitchPeak: numberglitchPeak = 0; // --- Band-wobble state (mild pixel-tier jitter, separate from TV faults) --- Demo.bandWobbleCooldown: numberbandWobbleCooldown = 0; Demo.bandWobbleActive: numberbandWobbleActive = 0; Demo.bandWobbleDuration: numberbandWobbleDuration = 0; Demo.bandWobbleSeed: numberbandWobbleSeed = 0; /** * Called once at startup. Sets the tiny screen resolution, the 3x upscale * buffer, and the nearest-neighbor filter, and turns the engine overlay off. * * @returns {Partial<HardwareSettings>} */ Demo.configure(): Partial<HardwareSettings>
Called once at startup. Sets the tiny screen resolution, the 3x upscale buffer, and the nearest-neighbor filter, and turns the engine overlay off.
@returns
configure
() {
return { // Tiny 80x60 logical canvas. displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const DISPLAY_W: 80DISPLAY_W, const DISPLAY_H: 60DISPLAY_H),
// 3x upscale to 240x180. CRT effects run on this larger buffer. 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: 240OUTPUT_W, const OUTPUT_H: 180OUTPUT_H),
// Keep each logical pixel as a crisp hard-edged square. // The CRT effects layer on top and add the soft, curved-glass look. outputUpscaleFilter: stringoutputUpscaleFilter: 'nearest', // Engine overlay disabled so its HUD never covers the CRT image. // The small kit status chip drawn in render() takes over its job. isOverlayEnabled: booleanisOverlayEnabled: false, }; } /** * Runs once before the loop starts. Sets up the palette, loads the sprite, * centers it, and (when WebGPU is available) registers the Orava CRT effects. * * "async" and "await" pause this function while the PNG downloads from the * server - think of it like pressing Pause on a video and waiting for it to buffer. * * @returns {Promise<boolean>} true when everything is ready. */ async Demo.init(): Promise<boolean>
Runs once before the loop starts. Sets up the palette, loads the sprite, centers it, and (when WebGPU is available) registers the Orava CRT effects. "async" and "await" pause this function while the PNG downloads from the server - think of it like pressing Pause on a video and waiting for it to buffer.
@returnstrue when everything is ready.
init
() {
// --- Palette setup --- // BT.paletteCreate(256) makes a fresh numbered list with 256 empty color slots. 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);
// Color32(Red, Green, Blue) - each value is 0 (none) to 255 (maximum). 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
(160, 160, 160)); // Light gray background.
// Read every unique color in the logo PNG and store them starting at SPRITE_BASE. // The engine must know these colors before it can draw palette-indexed sprites. 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: 3SPRITE_BASE);
// Turn the full-color PNG into a palette-indexed sprite ready for BT.drawSprite. // loadIndexed() also returns srcRect, which covers the whole single-sprite sheet. 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: 3SPRITE_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;
// Activate our palette. Every draw call from here on uses these colors.
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
);
// --- Center the sprite --- // BT.displaySize is the logical 80x60 canvas. // Subtracting half the sprite size from half the screen size gives the top-left // corner position that puts the sprite's CENTER at the screen's CENTER. const const screen: Vector2iscreen =
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
;
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 screen: Vector2iscreen.Vector2i.x: number
Horizontal component (defaults to 0).
x
/ 2 - 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
/ 2),
Math.floor(const screen: Vector2iscreen.Vector2i.y: number
Vertical component (defaults to 0).
y
/ 2 - 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
/ 2),
); // --- Post-process effects (WebGPU only) --- // isAvailable() checks BT.activeBackend === 'webgpu'. If the browser falls back // to the Canvas 2D software renderer, we skip all effect setup and show a note. this.Demo.effectsAvailable: booleaneffectsAvailable = import isAvailableisAvailable(); if (!this.Demo.effectsAvailable: booleaneffectsAvailable) { // Software renderer: update() never runs the CRT state machine, so there is nothing to schedule. return true; } // Pixel-tier: horizontal band shift that mimics a TV losing its horizontal hold. // This runs BEFORE the palette-to-RGBA resolve step, so it shifts the raw // palette-index rows - each shifted row is then resolved to a different RGBA color. // intensity = 0 means no shift right now; it spikes up during H-HOLD faults. 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
= 2; // Two logical-pixel-tall bands (our screen is only 60 px tall).
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;
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: the effects below run on the full 240x180 RGBA buffer after upscaling. // Chromatic aberration splits red and blue channels slightly at bright edges. // On a B/W CRT this is very faint; we only raise it during fault bursts. 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
= const ABERRATION_BASE: 0ABERRATION_BASE;
// Interference layers a faint ghost copy of the picture over itself. // On a real TV this appears when a signal bounces off a wall before reaching the aerial. 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
= const INTERFERENCE_BASE: 0INTERFERENCE_BASE;
// RollLine simulates the bright horizontal scan band that sweeps top-to-bottom // on a CRT. You can see it clearly on old TV footage: a slightly bright line // drifting through the picture. 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
= const ROLL_BASE: 0.26ROLL_BASE;
this.Demo.rollLine: RollLine
@type{RollLine | null}
rollLine
.RollLine.speed: number
Scroll speed multiplier; final scroll velocity = `time * speed`.
speed
= const ROLL_SPEED: 0.92ROLL_SPEED;
// Scanlines darken alternate rows to recreate the gaps between phosphor lines. // density = DISPLAY_H aligns one scanline pair per logical pixel row (every 3 output pixels). // strength = negative values darken; -40 is a strong darkening (built-in CRT presets use -7 to -8). 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.05;
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
= -40;
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
= const DISPLAY_H: 60DISPLAY_H;
// RGBMask adds a subpixel pattern across the whole output image. // // How it works: // size = how many output pixels make up one full R-G-B cycle. // size = 3 → each subpixel is exactly 1 output pixel wide: R | G | B | R | G | B … // That is the same layout as a real LCD or OLED display (RGB stripe). // // With our 3x upscale each logical pixel is 3 output pixels wide, so one // full R-G-B cycle happens to line up exactly with one logical pixel here. // On a real screen the physical subpixel grid is independent of the game's // pixel grid, but the stripe look is the same either way. // // border = 0 disables the cell-edge darkening AND the vertical stagger that // was causing each logical pixel to look like a 2×2 block of subpixels. // With border = 0 you get clean horizontal-only RGB stripes. 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.05;
this.Demo.mask: RGBMask
@type{RGBMask | null}
mask
.RGBMask.size: number
Mask cell pitch in output (display-chain) pixels. Smaller = denser mask.
size
= 3;
this.Demo.mask: RGBMask
@type{RGBMask | null}
mask
.RGBMask.border: number
Border darkening within each mask cell. 0 disables, 1 strong.
border
= 0;
// Vignette darkens the corners slightly, like the shadow cast by a CRT bezel. 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.1;
// Noise adds a tiny random grain on every pixel, every frame. // On a real CRT this is thermal noise in the electron gun. amount = 0.038 is very faint. 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
= const NOISE_BASE: 0.038NOISE_BASE;
// Flicker multiplies the overall brightness each frame. // amount = 1.0 is fully bright; during a "dim" fault it dips toward FLICKER_DIP. 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
= const FLICKER_BASE: 1FLICKER_BASE;
// Bloom adds a soft glow around bright areas - the phosphor afterglow of a hot CRT. // spread controls the glow radius; glow controls how much it brightens the surroundings. 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
= 2.2;
this.Demo.bloom: Bloom
@type{Bloom | null}
bloom
.Bloom.glow: number
Mix factor between the original sample and the blurred neighborhood.
glow
= 0.09;
// Register all display-tier effects with the engine in the order they run. for (const const fx: anyfx of [ 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);
} // Pick a random delay before the first TV fault burst. // 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. 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
(const GLITCH_COOLDOWN_MIN: 150GLITCH_COOLDOWN_MIN, const GLITCH_COOLDOWN_MAX: 420GLITCH_COOLDOWN_MAX);
this.Demo.bandWobbleCooldown: numberbandWobbleCooldown =
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
(const BAND_WOBBLE_COOLDOWN_MIN: 100BAND_WOBBLE_COOLDOWN_MIN, const BAND_WOBBLE_COOLDOWN_MAX: 280BAND_WOBBLE_COOLDOWN_MAX);
return true; } /** * Called at 60 ticks per second. The logo never moves, so we only need * to drive the CRT effect state machine here. */ Demo.update(): void
Called at 60 ticks per second. The logo never moves, so we only need to drive the CRT effect state machine here.
update
() {
if (this.Demo.effectsAvailable: booleaneffectsAvailable) { this.Demo.updateCrtEffects(): void
Advances the TV fault state machine and the subtle band-wobble system. Called every tick from update() when WebGPU effects are active. The machine has two layers: 1. TV fault bursts - dramatic, infrequent, random type (H-HOLD, snow, etc.). 2. Band wobble - mild pixel-tier jitter, always separate from fault bursts.
updateCrtEffects
();
} } /** * Called once per screen refresh. The logo does not move so this is simple: * clear the screen, draw the sprite at its fixed center position. */ Demo.render(): void
Called once per screen refresh. The logo does not move so this is simple: clear the screen, draw the sprite at its fixed center position.
render
() {
// Erase the previous frame so nothing trails or ghosts.
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.clear: (paletteIndex: number) => void
Sets the frame clear color using a palette index. The renderer uses this color when clearing the full display at the start of the next frame.
@since0.1.0@parampaletteIndex - Palette index for the full-screen clear pass.
clear
(const C_BG: 1C_BG);
// Draw the logo on top, centered. // paletteOffset = 0 keeps the original sprite colors. if (this.Demo.spriteSheet: SpriteSheet | null
@type{SpriteSheet | null}
spriteSheet
&& this.Demo.spriteRect: Rect2i | null
@type{Rect2i | null}
spriteRect
) {
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
@type{SpriteSheet | null}
spriteSheet
, this.Demo.spriteRect: Rect2i
@type{Rect2i | null}
spriteRect
, this.Demo.pos: Vector2ipos, 0);
} } /** * Advances the TV fault state machine and the subtle band-wobble system. * Called every tick from update() when WebGPU effects are active. * * The machine has two layers: * 1. TV fault bursts - dramatic, infrequent, random type (H-HOLD, snow, etc.). * 2. Band wobble - mild pixel-tier jitter, always separate from fault bursts. */ Demo.updateCrtEffects(): void
Advances the TV fault state machine and the subtle band-wobble system. Called every tick from update() when WebGPU effects are active. The machine has two layers: 1. TV fault bursts - dramatic, infrequent, random type (H-HOLD, snow, etc.). 2. Band wobble - mild pixel-tier jitter, always separate from fault bursts.
updateCrtEffects
() {
// Feed the current engine time into effects that need it for animation. // BT.timeSeconds is the total elapsed seconds since the demo started. 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;
// --- TV fault burst --- if (this.Demo.glitchTicksLeft: numberglitchTicksLeft > 0) { // We are inside a fault burst. Compute a smooth envelope (0..1..0) so // the fault eases in and out rather than snapping on and off instantly. // t = 0 at the start of the burst, t = 1 at the end. 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); // Sine gives a bell-curve shape: 0 at edges, 1 at peak. this.Demo.applyRestingCrtUniforms(): void
Resets all effect uniforms to the calm, between-faults Orava CRT look. Called every tick, then overridden by applyGlitchUniforms during a fault.
applyRestingCrtUniforms
();
this.Demo.applyGlitchUniforms(envelope: number): void
Modifies effect uniforms to simulate one of the five Orava TV faults. The envelope argument is a 0..1 value (bell-curve shaped) that controls how strong the fault is at this moment in its lifetime.
@paramenvelope - 0 at the start and end of a burst, peaks at 1 in the middle.
applyGlitchUniforms
(const envelope: anyenvelope);
this.Demo.glitchTicksLeft: numberglitchTicksLeft--; if (this.Demo.glitchTicksLeft: numberglitchTicksLeft <= 0) { // Burst finished - schedule the next cooldown and reset band-wobble too. 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
(const GLITCH_COOLDOWN_MIN: 150GLITCH_COOLDOWN_MIN, const GLITCH_COOLDOWN_MAX: 420GLITCH_COOLDOWN_MAX);
this.Demo.bandWobbleCooldown: numberbandWobbleCooldown =
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
(const BAND_WOBBLE_COOLDOWN_MIN: 100BAND_WOBBLE_COOLDOWN_MIN, const BAND_WOBBLE_COOLDOWN_MAX: 280BAND_WOBBLE_COOLDOWN_MAX);
} // Return early: do not run the rest of the state machine during a burst. return; } // No fault active: apply the calm resting look. this.Demo.applyRestingCrtUniforms(): void
Resets all effect uniforms to the calm, between-faults Orava CRT look. Called every tick, then overridden by applyGlitchUniforms during a fault.
applyRestingCrtUniforms
();
// --- Band wobble (pixel-tier mild jitter) --- if (this.Demo.bandWobbleActive: numberbandWobbleActive > 0) { // Same bell-curve envelope as fault bursts, but smaller intensity. const const t: numbert = 1 - (this.Demo.bandWobbleActive: numberbandWobbleActive - 1) / this.Demo.bandWobbleDuration: numberbandWobbleDuration; const const envelope: anyenvelope = Math.sin(const t: numbert * Math.PI); this.Demo.pixelGlitch: PixelGlitch | null
@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
= const BAND_WOBBLE_INTENSITY: 0.11BAND_WOBBLE_INTENSITY * const envelope: anyenvelope;
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
= this.Demo.bandWobbleSeed: numberbandWobbleSeed;
this.Demo.bandWobbleActive: numberbandWobbleActive--; if (this.Demo.bandWobbleActive: numberbandWobbleActive <= 0) { this.Demo.pixelGlitch: PixelGlitch | null
@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;
this.Demo.bandWobbleCooldown: numberbandWobbleCooldown =
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
(const BAND_WOBBLE_COOLDOWN_MIN: 100BAND_WOBBLE_COOLDOWN_MIN, const BAND_WOBBLE_COOLDOWN_MAX: 280BAND_WOBBLE_COOLDOWN_MAX);
} } else { this.Demo.bandWobbleCooldown: numberbandWobbleCooldown--; if (this.Demo.bandWobbleCooldown: numberbandWobbleCooldown <= 0) { // Start a new band-wobble burst. this.Demo.bandWobbleDuration: numberbandWobbleDuration =
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
(const BAND_WOBBLE_ACTIVE_MIN: 3BAND_WOBBLE_ACTIVE_MIN, const BAND_WOBBLE_ACTIVE_MAX: 10BAND_WOBBLE_ACTIVE_MAX);
this.Demo.bandWobbleActive: numberbandWobbleActive = this.Demo.bandWobbleDuration: numberbandWobbleDuration; this.Demo.bandWobbleSeed: numberbandWobbleSeed =
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); // Different seed = different row pattern.
} } // --- Cooldown before the next TV fault --- this.Demo.glitchCooldown: numberglitchCooldown--; if (this.Demo.glitchCooldown: numberglitchCooldown <= 0) { // Time for a fault! 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(). 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_VROLLGLITCH_TYPES_VROLL);
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
(const GLITCH_ACTIVE_MIN: 4GLITCH_ACTIVE_MIN, const GLITCH_ACTIVE_MAX: 24GLITCH_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
(const GLITCH_INTENSITY_MIN: 0.3GLITCH_INTENSITY_MIN, const GLITCH_INTENSITY_MAX: 0.95GLITCH_INTENSITY_MAX);
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);
} } /** * Resets all effect uniforms to the calm, between-faults Orava CRT look. * Called every tick, then overridden by applyGlitchUniforms during a fault. */ Demo.applyRestingCrtUniforms(): void
Resets all effect uniforms to the calm, between-faults Orava CRT look. Called every tick, then overridden by applyGlitchUniforms during a fault.
applyRestingCrtUniforms
() {
this.Demo.pixelGlitch: PixelGlitch | null
@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;
this.Demo.aberration: ChromaticAberration | null
@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
= const ABERRATION_BASE: 0ABERRATION_BASE;
this.Demo.noise: Noise | null
@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
= const NOISE_BASE: 0.038NOISE_BASE;
this.Demo.flicker: Flicker | null
@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
= const FLICKER_BASE: 1FLICKER_BASE;
this.Demo.interference: Interference | null
@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
= const INTERFERENCE_BASE: 0INTERFERENCE_BASE;
this.Demo.rollLine: RollLine | null
@type{RollLine | null}
rollLine
.RollLine.amount: number
Roll line amplitude (mix factor onto a brightness boost).
amount
= const ROLL_BASE: 0.26ROLL_BASE;
this.Demo.rollLine: RollLine | null
@type{RollLine | null}
rollLine
.RollLine.speed: number
Scroll speed multiplier; final scroll velocity = `time * speed`.
speed
= const ROLL_SPEED: 0.92ROLL_SPEED;
} /** * Modifies effect uniforms to simulate one of the five Orava TV faults. * The envelope argument is a 0..1 value (bell-curve shaped) that controls * how strong the fault is at this moment in its lifetime. * * @param {number} envelope - 0 at the start and end of a burst, peaks at 1 in the middle. */ Demo.applyGlitchUniforms(envelope: number): void
Modifies effect uniforms to simulate one of the five Orava TV faults. The envelope argument is a 0..1 value (bell-curve shaped) that controls how strong the fault is at this moment in its lifetime.
@paramenvelope - 0 at the start and end of a burst, peaks at 1 in the middle.
applyGlitchUniforms
(envelope: number
- 0 at the start and end of a burst, peaks at 1 in the middle.
@paramenvelope - 0 at the start and end of a burst, peaks at 1 in the middle.
envelope
) {
// peak is how strong this specific burst is, scaled by the bell-curve envelope. const const peak: numberpeak = this.Demo.glitchPeak: numberglitchPeak * envelope: number
- 0 at the start and end of a burst, peaks at 1 in the middle.
@paramenvelope - 0 at the start and end of a burst, peaks at 1 in the middle.
envelope
;
if (this.Demo.glitchType: stringglitchType === 'hshift') { // H-HOLD: horizontal band shift in the pixel-tier index buffer. // The image looks like it slipped sideways on the tube. this.Demo.pixelGlitch: PixelGlitch | null
@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
= const peak: numberpeak;
// The torn edges also pick up a faint red/blue fringe, like a real set // struggling to keep the picture aligned during a horizontal-hold fault. this.Demo.aberration: ChromaticAberration | null
@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
= const ABERRATION_BASE: 0ABERRATION_BASE + const peak: numberpeak * 4;
} else if (this.Demo.glitchType: stringglitchType === 'noise') { // SNOW: extra random grain on top of the base noise. this.Demo.noise: Noise | null
@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
= const NOISE_BASE: 0.038NOISE_BASE + const peak: numberpeak * 0.1;
} else if (this.Demo.glitchType: stringglitchType === 'flicker') { // DIM: the brightness drops as if the tube needs warming up. // Lerp from FLICKER_BASE down to FLICKER_DIP, scaled by peak so the status // chip's intensity percentage matches how deep the dim goes. this.Demo.flicker: Flicker | null
@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
= const FLICKER_BASE: 1FLICKER_BASE - (const FLICKER_BASE: 1FLICKER_BASE - const FLICKER_DIP: 0.78FLICKER_DIP) * const peak: numberpeak;
} else if (this.Demo.glitchType: stringglitchType === 'interference') { // GHOST: a faint ghost copy of the image appears (aerial reflection). this.Demo.interference: Interference | null
@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
= const peak: numberpeak * 0.07;
} else if (this.Demo.glitchType: stringglitchType === 'vroll') { // V-ROLL: the scan band speeds up and brightens, like a vertical sync loss. this.Demo.rollLine: RollLine | null
@type{RollLine | null}
rollLine
.RollLine.amount: number
Roll line amplitude (mix factor onto a brightness boost).
amount
= const ROLL_BASE: 0.26ROLL_BASE + const peak: numberpeak * 0.35;
this.Demo.rollLine: RollLine | null
@type{RollLine | null}
rollLine
.RollLine.speed: number
Scroll speed multiplier; final scroll velocity = `time * speed`.
speed
= const ROLL_SPEED: 0.92ROLL_SPEED + const peak: numberpeak * 1.8;
} } } // Hand our Demo class to the engine. bootstrap() sets up the canvas, picks a backend, // creates one instance of Demo, then drives configure() -> init() -> update/render loop. 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
The BLIT386 logo centered on an 80x60 pixel canvas, upscaled 3x, and wrapped in the Tesla Orava B/W CRT post-process stack.
@implementsIBTDemo
Demo
);