/*
Logo Low-Res: the BLIT386 logo on an 80x60 screen, upscaled 3x inside a B/W CRT stack.
@description The BLIT386 logo on a tiny 80x60 screen, upscaled nearest-neighbor inside a monochrome CRT stack.
@pageTitle BLIT386 Demo – Logo Low-Res
@ogScale integer
Part of the BLIT386 demo series.
Prerequisites:
- Basics: https://demos.blit386.dev/basics
- Sprite Effects: https://demos.blit386.dev/sprite-effects
Live version: https://demos.blit386.dev/logo-lowres
Guides:
- Post-Process Effects: https://blit386.dev/docs/guides/post-process-effects#display-tier-effects
Reference:
- Rendering: https://blit386.dev/docs/api/rendering#post-process-effects
- Core: https://blit386.dev/docs/api/core#resolution-model
---
WHAT YOU WILL SEE
-----------------
- The 32x28 logo sprite from Basics centered on a light gray 80x60 canvas. That is one
quarter of the usual 320x240 in each direction - smaller than a Game Boy screen
(160x144).
- The engine upscales that 80x60 picture 3x to 240x180 with nearest-neighbor filtering,
so each logical pixel becomes a hard-edged 3x3 block.
- On top of the upscaled image runs the Tesla Orava B/W CRT stack introduced in Sprite
Effects: scanlines, a bright scrolling roll band (RollLine), light grain (Noise), an RGB
stripe mask (RGBMask), a gentle vignette, soft bloom, and brightness waver (Flicker).
ChromaticAberration and Interference are registered too but rest at zero.
- Every 2.5-7 seconds a TV fault burst lasting 4-24 ticks: horizontal hold, snow, dimming,
ghosting, or vertical roll, each eased in and out over a sine envelope. Separately,
every 1.7-4.7 seconds a milder 3-10 tick band wobble jitters a few rows.
- Post-process needs WebGPU. Under the software renderer the logo is drawn without any
CRT effect, and nothing on screen says so: this demo has no HUD and the engine overlay
is disabled.
WHAT YOU WILL LEARN
-------------------
- displaySize 80x60 plus drawingBufferSize 240x180 and outputUpscaleFilter 'nearest':
the logical picture is drawn small and stretched to an integer multiple before the
display tier runs.
- A pixel-tier effect (PixelGlitch, bandHeight 2) shifting palette-index rows before the
palette resolve, and nine display-tier effects on the resolved RGBA buffer.
- Scanlines density = DISPLAY_H puts one scanline cycle on every logical row; RGBMask
size 3 with border 0 gives one clean R-G-B stripe cycle per logical pixel.
- Two independent cooldown/active state machines driven by BT.random.int(),
BT.random.float(), and BT.random.pick(GLITCH_TYPES_VROLL), resting uniforms restored
every tick before the active fault is layered on.
- isOverlayEnabled: false for an immersive piece with no dev HUD.
WHY UPSCALE FIRST AND THEN ADD CRT?
-----------------------------------
The engine renders the 80x60 palette-indexed picture into a 240x180 RGBA buffer (the
drawingBufferSize step). The display-tier effects run after that, on the RGBA buffer, so
they see 240x180 pixels and can paint convincing scanlines, stripes, and glow across the
whole frame even though the game itself only used 80x60 logical pixels.
*/
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.Bloom,
function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>One-liner bootstrap function for BLIT386 demos.
Handles canvas retrieval and engine initialization. Backend selection
(WebGPU or software fallback) is managed internally by BTAPI.
This function provides a streamlined way to start a demo with sensible defaults
while allowing customization through options.bootstrap,
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT,
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.ChromaticAberration,
class Color32Mutable 32-bit RGBA color value with 8-bit channels.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.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`.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.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).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.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`).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.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.SpriteSheet,
class Vector2iInteger 2D vector for pixel-perfect positioning.
Used for points, sizes, directions, and camera offsets throughout the engine.
The API includes both allocation-free `*To()` / `*InPlace()` variants and
convenience methods that return new vectors.Vector2i,
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.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').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 land. 80x60 is 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 to 240x180
// (exactly 3x) before the display-tier effects run, so each logical pixel is 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; this demo's own colors start at index 1.
const const C_BG: 1C_BG = 1; // light gray background
const const SPRITE_BASE: 3SPRITE_BASE = 3; // first palette slot reserved for the logo's own colors
// --- Sprite source ---
const const SPRITE_URL: "/sprites/logo-1.png"SPRITE_URL = '/sprites/logo-1.png';
// --- Orava CRT constants ---
// These numbers set the resting look of the B/W CRT between faults.
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 while the set works 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. 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 while the set is tuned correctly
// --- Analog-TV fault burst settings ---
// Every so often the virtual TV loses its signal and shows one fault. Cooldown is the
// number of ticks between faults; active is how long a fault lasts. At the default 60
// ticks per second one tick is 1/60 s.
const const GLITCH_COOLDOWN_MIN: 150GLITCH_COOLDOWN_MIN = 150; // at least 2.5 seconds between faults
const const GLITCH_COOLDOWN_MAX: 420GLITCH_COOLDOWN_MAX = 420; // up to 7 seconds between faults
const const GLITCH_ACTIVE_MIN: 4GLITCH_ACTIVE_MIN = 4; // a fault lasts at least 4 ticks (~0.07 s)
const const GLITCH_ACTIVE_MAX: 24GLITCH_ACTIVE_MAX = 24; // a 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) ---
// Separate from the bigger TV fault bursts: a real CRT sometimes shows 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
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; // a wobble lasts at least 3 ticks (0.05 s)
const const BAND_WOBBLE_ACTIVE_MAX: 10BAND_WOBBLE_ACTIVE_MAX = 10; // a 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 canvas, upscaled 3x, and wrapped in the Tesla
// Orava B/W CRT post-process stack.
class class DemoDemo {
// The palette: a numbered list of every color this demo uses.
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = null;
// The loaded logo sprite sheet; null until init() finishes downloading it.
/** @type {SpriteSheet | null} */
Demo.spriteSheet: SpriteSheet | nullspriteSheet = null;
// Which part of the sprite sheet to draw: the full image here.
/** @type {Rect2i | null} */
Demo.spriteRect: Rect2i | nullspriteRect = null;
// Pixel position of the logo's top-left corner, set once in init().
Demo.pos: Vector2ipos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(0, 0);
// --- Post-process effect objects ---
// Each one is created in init() when WebGPU is available. They stay null under the
// software renderer.
// Pixel tier: horizontal band shift (horizontal-hold style tear).
/** @type {PixelGlitch | null} */
Demo.pixelGlitch: PixelGlitch | nullpixelGlitch = null;
// Red/blue fringe on bright edges, raised only during hshift faults.
/** @type {ChromaticAberration | null} */
Demo.aberration: ChromaticAberration | nullaberration = null;
// Ghost image overlaid on the picture.
/** @type {Interference | null} */
Demo.interference: Interference | nullinterference = null;
// Bright band that scrolls top to bottom.
/** @type {RollLine | null} */
Demo.rollLine: RollLine | nullrollLine = null;
// Dark horizontal lines between pixel rows.
/** @type {Scanlines | null} */
Demo.scanlines: Scanlines | nullscanlines = null;
// Subtle RGB stripe pattern.
/** @type {RGBMask | null} */
Demo.mask: RGBMask | nullmask = null;
// Darkened corners, like a real tube.
/** @type {Vignette | null} */
Demo.vignette: Vignette | nullvignette = null;
// Constant film-grain noise.
/** @type {Noise | null} */
Demo.noise: Noise | nullnoise = null;
// Brightness waver.
/** @type {Flicker | null} */
Demo.flicker: Flicker | nullflicker = null;
// Soft glow on bright areas.
/** @type {Bloom | null} */
Demo.bloom: Bloom | nullbloom = null;
// True when WebGPU is active and the 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 runs. glitchDuration remembers how long the current fault
// was scheduled to last, for the 0..1 envelope that eases faults in and out.
// glitchType is one of GLITCH_TYPES_VROLL or 'none'. glitchPeak is how strong this
// particular 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;
// Runs once at startup: sets the tiny screen resolution, the 3x upscale buffer, the
// nearest-neighbor filter, and turns the engine overlay off.
Demo.configure(): {
displaySize: Vector2i;
drawingBufferSize: Vector2i;
outputUpscaleFilter: string;
isOverlayEnabled: boolean;
}
configure() {
return {
// Tiny 80x60 logical canvas.
displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const DISPLAY_W: 80DISPLAY_W, const DISPLAY_H: 60DISPLAY_H),
// 3x upscale to 240x180. The display-tier effects run on this larger buffer.
drawingBufferSize: Vector2idrawingBufferSize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const OUTPUT_W: 240OUTPUT_W, const OUTPUT_H: 180OUTPUT_H),
// Keep each logical pixel a crisp hard-edged square; the CRT effects layer
// the soft, curved-glass look on top.
outputUpscaleFilter: stringoutputUpscaleFilter: 'nearest',
// The engine overlay is disabled so its HUD never covers the CRT image. This
// demo draws no HUD of its own.
isOverlayEnabled: booleanisOverlayEnabled: false,
};
}
// Runs once before the loop starts: sets up the palette, loads the sprite, centers
// it, and registers the Orava CRT effects when WebGPU is available. `await` pauses
// this function while the PNG downloads.
async Demo.init(): unknowninit() {
// --- Palette setup ---
// BT.paletteCreate(256) makes a fresh palette with 256 empty color slots.
this.Demo.palette: Palette | nullpalette = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteCreate: (size?: number) => PaletteCreates a standalone palette instance.paletteCreate(256);
// Color32(red, green, blue): each channel is 0 (none) to 255 (maximum).
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_BG: 1C_BG, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(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 SpriteSheetSprite-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.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.loadColorsIntoPalette(const SPRITE_URL: "/sprites/logo-1.png"SPRITE_URL, this.Demo.palette: Palettepalette, const SPRITE_BASE: 3SPRITE_BASE);
// Turn the 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 SpriteSheetSprite-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.SpriteSheet.SpriteSheet.loadIndexed(url: string, palette: Palette, startSlot: number, options?: {
sort?: "luminance" | "none";
tileSize?: number | Vector2i;
}): 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)`.loadIndexed(const SPRITE_URL: "/sprites/logo-1.png"SPRITE_URL, this.Demo.palette: Palettepalette, const SPRITE_BASE: 3SPRITE_BASE, { sort?: "none" | "luminance" | undefinedsort: 'none' });
this.Demo.spriteSheet: SpriteSheet | nullspriteSheet = const indexed: Promise<IndexedSpriteLoadResult>indexed.sheet;
this.Demo.spriteRect: Rect2i | nullspriteRect = const indexed: Promise<IndexedSpriteLoadResult>indexed.srcRect;
// Activate the 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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteSet: (palette: Palette) => voidStores the active engine palette.
Use this to swap the **entire palette** (e.g. switch between a day and night
theme). After this call the renderer uploads the new palette uniform on the
next frame.
**Palette-value swap (change what a slot looks like):** mutate the live
{@link
BT.palette
}
in place with `palette.set(slot, newColor)`. The renderer
uploads dirty slots on the next frame; no `paletteSet()` or
{@link
BT.spritesRefresh
}
needed.
**Palette-layout swap (same colors, different slot positions):** build a new
palette with the same colors at new indices, call `paletteSet()`, then call
{@link
BT.spritesRefresh
}
so every sprite sheet re-maps its original RGBA
pixels against the new slot layout.paletteSet(this.Demo.palette: Palettepalette);
// --- 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 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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.displaySize: Vector2iActive 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.displaySize;
this.Demo.pos: Vector2ipos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(
Math.floor(const screen: Vector2iscreen.Vector2i.x: numberHorizontal component (defaults to 0).x / 2 - this.Demo.spriteSheet: SpriteSheet | nullspriteSheet.SpriteSheet.size: Vector2iGets the sprite-sheet dimensions in pixels.size.Vector2i.x: numberHorizontal component (defaults to 0).x / 2),
Math.floor(const screen: Vector2iscreen.Vector2i.y: numberVertical component (defaults to 0).y / 2 - this.Demo.spriteSheet: SpriteSheet | nullspriteSheet.SpriteSheet.size: Vector2iGets the sprite-sheet dimensions in pixels.size.Vector2i.y: numberVertical component (defaults to 0).y / 2),
);
// --- Post-process effects (WebGPU only) ---
// isAvailable() checks BT.activeBackend === 'webgpu'. When the browser fell back
// to the Canvas 2D software renderer, all effect setup is skipped.
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: a horizontal band shift that mimics a TV losing its horizontal
// hold. It runs before the palette-to-RGBA resolve, so it shifts raw
// palette-index rows and each shifted row is then resolved to a different color.
// Intensity 0 means no shift right now; it rises during hshift faults and band
// wobbles.
this.Demo.pixelGlitch: PixelGlitch | nullpixelGlitch = new new PixelGlitch(): PixelGlitchChunky pixel-aligned horizontal glitch: every Nth row of source pixels gets
a random horizontal shift. Shifts snap to integer source-pixel offsets so
palette indices move whole-texel (no RGB resampling).
Pixel-tier: runs on the logical `r8uint` framebuffer (palette indices).PixelGlitch();
this.Demo.pixelGlitch: PixelGlitchpixelGlitch.PixelGlitch.bandHeight: numberHeight of each glitch band in source pixels. Each band gets a single
shift value, so larger bands produce chunkier glitches.bandHeight = 2; // two logical-pixel-tall bands on a 60 px screen
this.Demo.pixelGlitch: PixelGlitchpixelGlitch.PixelGlitch.intensity: numberGlitch strength in `[0, 1]`. Scales the per-band horizontal shift
magnitude. Bands are shifted when their hash exceeds ~0.85 (~15% of
bands). `0` disables.intensity = 0;
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.effectAdd: (effect: Effect) => voidAppends a fullscreen post-processing effect to whichever chain matches
its declared
{@link
Effect.tier
}
.
- `tier='pixel'` -> pixel chain (logical resolution).
- `tier='display'` -> display chain (output resolution); requires
`drawingBufferSize` in effective hardware settings (`configure()` or
`defaultConfig()`).
Effects run in registration order within each tier. The pixel chain runs
first, followed by the upscale pass, followed by the display chain. Each
{@link
Effect
}
instance owns its own GPU resources and may be mutated
each frame from demo code.effectAdd(this.Demo.pixelGlitch: PixelGlitchpixelGlitch);
// Display tier: the effects below run on the full 240x180 RGBA buffer after
// upscaling.
// Chromatic aberration splits the red and blue channels slightly at bright edges.
// On a B/W CRT this is very faint; it only rises during hshift faults.
this.Demo.aberration: ChromaticAberration | nullaberration = new new ChromaticAberration(): ChromaticAberrationRGB channel offset that simulates lens chromatic aberration: red samples
left of the fragment, blue samples right, green stays centered.
Display-tier: spreads color along the lens axis. At logical resolution the
single-pixel offset is too coarse and reads as a glitch instead of a soft
fringe.ChromaticAberration();
this.Demo.aberration: ChromaticAberrationaberration.ChromaticAberration.aberration: numberChannel offset in display-chain (output) pixels. Reasonable values are `0.5` to `3.0`.
Set to `0` to disable.aberration = const ABERRATION_BASE: 0ABERRATION_BASE;
// Interference layers a faint ghost copy of the picture over itself. On a real TV
// this appears when the signal bounces off a wall before reaching the aerial.
this.Demo.interference: Interference | nullinterference = new new Interference(): InterferencePer-row horizontal jitter that simulates analog signal interference.
Each output row gets a deterministic random horizontal offset seeded by
row index and time. Row offsets are stable for one frame and re-seed every
frame, producing a buzzing-noise feel.
Display-tier. Drives jitter from
{@link
time
}
; demos typically pass
`BT.ticks / BT.targetFPS`.Interference();
this.Demo.interference: Interferenceinterference.Interference.amount: numberMaximum horizontal offset as a UV fraction (e.g. `0.06` shifts the row
by up to ~6% of the image width). Set to `0` to disable.amount = const INTERFERENCE_BASE: 0INTERFERENCE_BASE;
// RollLine simulates the bright horizontal band that sweeps top to bottom on a
// CRT, visible on old TV footage as a slightly bright line drifting through the
// picture.
this.Demo.rollLine: RollLine | nullrollLine = new new RollLine(): RollLineSlowly scrolling vertical interference band that brightens a horizontal
stripe of the image. Combination of three cosines + smoothstep gives the
stripe a soft top/bottom edge.
Display-tier. Demo drives
{@link
time
}
(typically `BT.ticks / BT.targetFPS`).RollLine();
this.Demo.rollLine: RollLinerollLine.RollLine.amount: numberRoll line amplitude (mix factor onto a brightness boost).amount = const ROLL_BASE: 0.26ROLL_BASE;
this.Demo.rollLine: RollLinerollLine.RollLine.speed: numberScroll 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 is cycles per view: DISPLAY_H puts one cycle on each logical pixel row
// (every 3 output pixels). strength is the gaussian falloff: more negative means
// sharper, narrower dark bands (the engine default is -8).
this.Demo.scanlines: Scanlines | nullscanlines = new new Scanlines(): ScanlinesCRT scanlines: alternating bright/dark horizontal bands aligned to the
source vertical resolution.
Display-tier: at output resolution there is enough vertical pixels for
scanlines to read as alternating bright/dark bands. At logical 320x240 the
Gaussian weight quantizes to one of two values per source row and you lose
the smooth fade.Scanlines();
this.Demo.scanlines: Scanlinesscanlines.Scanlines.amount: numberScanline mix amount in `[0, 1]`. 0 disables.amount = 0.05;
this.Demo.scanlines: Scanlinesscanlines.Scanlines.strength: numberNegative gaussian falloff parameter for scanline brightness. More
negative values produce sharper dark bands. PipBoy reference: `-8.0`.strength = -40;
this.Demo.scanlines: Scanlinesscanlines.Scanlines.density: numberNumber of scanline cycles vertically. Should match the demo's logical
source vertical resolution so each "source pixel row" maps to one
scanline cycle. Defaults to `240`, the most common pixel-art height.
Set to e.g. `200` for VGA-style 320x200 games or `144` for Game Boy
resolution.density = const DISPLAY_H: 60DISPLAY_H;
// RGBMask adds a subpixel stripe pattern across the whole output image. size is
// how many output pixels make one full R-G-B cycle; size 3 makes each subpixel
// exactly one output pixel wide (R | G | B | R | G | B ...), the same layout as a
// real LCD or OLED RGB stripe. With the 3x upscale, each logical pixel is 3
// output pixels wide, so one R-G-B cycle lines up exactly with one logical pixel.
// border 0 disables the cell-edge darkening and the vertical stagger that would
// make each logical pixel look like a 2x2 block of subpixels, leaving clean
// horizontal-only RGB stripes.
this.Demo.mask: RGBMask | nullmask = new new RGBMask(): RGBMaskCRT shadow mask: per-pixel R/G/B vertical-stripe pattern with darkened
cell borders, simulating the phosphor grille of an aperture-grille CRT.
Display-tier: at output resolution there are enough output pixels per
mask cell to read as colored stripes. The cell pitch (in output pixels) is
the
{@link
size
}
parameter.
Math is a direct WGSL port of the libretro `crt-lottes.glsl` mask code.RGBMask();
this.Demo.mask: RGBMaskmask.RGBMask.intensity: numberMask brightness mix amount in `[0, 1]`. 0 hides the mask.intensity = 0.05;
this.Demo.mask: RGBMaskmask.RGBMask.size: numberMask cell pitch in output (display-chain) pixels. Smaller = denser mask.size = 3;
this.Demo.mask: RGBMaskmask.RGBMask.border: numberBorder darkening within each mask cell. 0 disables, 1 strong.border = 0;
// Vignette darkens the corners slightly, like the shadow of a CRT bezel.
this.Demo.vignette: Vignette | nullvignette = new new Vignette(): VignetteEdge-darkening vignette: smooth radial fade from full brightness at the
center to black at the corners.
Display-tier: applies to the whole simulated screen, not the underlying
pixel art.Vignette();
this.Demo.vignette: Vignettevignette.Vignette.amount: numberVignette darkening exponent. Higher values produce a stronger vignette
with a sharper falloff. PipBoy reference: `0.2`. Set to `0` to disable.amount = 0.1;
// Noise adds a tiny random grain on every pixel, every frame: the thermal noise
// of a real CRT's electron gun. 0.038 is very faint.
this.Demo.noise: Noise | nullnoise = new new Noise(): NoiseAdditive per-pixel pseudo-random noise. Reseeds each frame from
{@link
time
}
so the noise pattern animates.
Display-tier.Noise();
this.Demo.noise: Noisenoise.Noise.amount: numberNoise amplitude as a `[-amount, +amount]` additive perturbation on each
channel. Reasonable values are `0.005` to `0.05`. Set to `0` to disable.amount = const NOISE_BASE: 0.038NOISE_BASE;
// Flicker multiplies the overall brightness each frame. 1.0 is fully bright;
// during a "dim" fault it dips toward FLICKER_DIP.
this.Demo.flicker: Flicker | nullflicker = new new Flicker(): FlickerBrightness multiplier - the simplest CRT animation knob.
Demos drive
{@link
amount
}
per frame to simulate flicker (e.g. with
`0.95 + sin(t) * 0.05`). The effect is intentionally trivial so the demo
controls the pattern; for procedural noise-driven flicker, combine with
the
{@link
Noise
}
effect.
Display-tier.Flicker();
this.Demo.flicker: Flickerflicker.Flicker.amount: numberBrightness multiplier. `1` is unmodulated; values below `1` darken the
frame. The demo typically updates this each frame from a sin wave or
random source.amount = const FLICKER_BASE: 1FLICKER_BASE;
// Bloom adds a soft glow around bright areas, the phosphor afterglow of a hot
// CRT. spread sets the glow radius; glow sets how much it brightens the
// surroundings.
this.Demo.bloom: Bloom | nullbloom = new new Bloom(): BloomSingle-pass box-blur bloom.
Samples a 5x5 neighborhood (25 taps) around each fragment, averages, then
mixes with the original color by
{@link
glow
}
.
{@link
spread
}
scales the
texel offset so the bloom radius can be tuned independently of the source
resolution.
Display-tier: bloom mixes neighboring pixels into intermediate hues that
are not in the active palette. Running it in pixel space would violate the
palette-pixel aesthetic; running it on the upscaled output reads as the
warm phosphor glow of an old monitor instead.
The implementation matches the original PipBoy bloom shader. A future
optimization would be a two-pass separable Gaussian (5 + 5 = 10 taps); add
it once a GPU perf test demands it.Bloom();
this.Demo.bloom: Bloombloom.Bloom.spread: numberTexel offset multiplier for the box-blur kernel.spread = 2.2;
this.Demo.bloom: Bloombloom.Bloom.glow: numberMix 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: ChromaticAberrationaberration,
this.Demo.interference: Interferenceinterference,
this.Demo.rollLine: RollLinerollLine,
this.Demo.scanlines: Scanlinesscanlines,
this.Demo.mask: RGBMaskmask,
this.Demo.vignette: Vignettevignette,
this.Demo.noise: Noisenoise,
this.Demo.flicker: Flickerflicker,
this.Demo.bloom: Bloombloom,
]) {
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.effectAdd: (effect: Effect) => voidAppends a fullscreen post-processing effect to whichever chain matches
its declared
{@link
Effect.tier
}
.
- `tier='pixel'` -> pixel chain (logical resolution).
- `tier='display'` -> display chain (output resolution); requires
`drawingBufferSize` in effective hardware settings (`configure()` or
`defaultConfig()`).
Effects run in registration order within each tier. The pixel chain runs
first, followed by the upscale pass, followed by the display chain. Each
{@link
Effect
}
instance owns its own GPU resources and may be mutated
each frame from demo code.effectAdd(const fx: anyfx);
}
// Pick a random delay before the first TV fault and the first band wobble.
// BT.random is the engine's shared random number generator; int(min, max) returns
// a whole number from min up to but not including max.
this.Demo.glitchCooldown: numberglitchCooldown = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(const BAND_WOBBLE_COOLDOWN_MIN: 100BAND_WOBBLE_COOLDOWN_MIN, const BAND_WOBBLE_COOLDOWN_MAX: 280BAND_WOBBLE_COOLDOWN_MAX);
return true;
}
// Runs at the fixed tick rate (60 per second by default). The logo never moves, so
// only the CRT effect state machine needs driving.
Demo.update(): voidupdate() {
if (this.Demo.effectsAvailable: booleaneffectsAvailable) {
this.Demo.updateCrtEffects(): voidupdateCrtEffects();
}
}
// Runs once per screen refresh. The logo does not move: clear the screen, draw the
// sprite at its fixed center position.
Demo.render(): voidrender() {
// 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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.clear: (paletteIndex: number) => voidSets the frame clear color using a palette index.
The renderer uses this color when clearing the full display at the start
of the next frame.clear(const C_BG: 1C_BG);
// Draw the logo on top, centered. paletteOffset 0 keeps the original sprite
// colors.
if (this.Demo.spriteSheet: SpriteSheet | nullspriteSheet && this.Demo.spriteRect: Rect2i | nullspriteRect) {
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => void (+1 overload)drawSprite(this.Demo.spriteSheet: SpriteSheetspriteSheet, this.Demo.spriteRect: Rect2ispriteRect, 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. Two layers: dramatic, infrequent
// TV fault bursts of a random type, and a mild pixel-tier band wobble. The wobble
// counters pause while a fault burst is running.
Demo.updateCrtEffects(): voidupdateCrtEffects() {
// Feed the current engine time into the effects that animate. 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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.timeSeconds: numberFixed-step elapsed time in seconds (`BT.ticks * BT.deltaSeconds`).timeSeconds;
this.Demo.rollLine: RollLine | nullrollLine.RollLine.time: numberSeconds; demos drive this each frame. Use BT.timeSeconds so BT.renderAt can seek it.time = const seconds: numberseconds;
this.Demo.noise: Noise | nullnoise.Noise.time: numberSeconds; reseeds the noise each frame. Drive it from BT.timeSeconds so BT.renderAt can seek it.time = const seconds: numberseconds;
this.Demo.interference: Interference | nullinterference.Interference.time: numberSeconds; reseeds the row offsets each frame. Drive it from BT.timeSeconds so BT.renderAt can seek it.time = const seconds: numberseconds;
// --- TV fault burst ---
if (this.Demo.glitchTicksLeft: numberglitchTicksLeft > 0) {
// Inside a fault burst. Compute a smooth 0..1..0 envelope so the fault eases
// in and out instead of snapping on and off: t is 0 at the start of the burst
// and 1 at the end, and sin(t * PI) is a bell curve, 0 at the edges and 1 at
// the peak.
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);
this.Demo.applyRestingCrtUniforms(): voidapplyRestingCrtUniforms();
this.Demo.applyGlitchUniforms(envelope: any): voidapplyGlitchUniforms(const envelope: anyenvelope);
this.Demo.glitchTicksLeft: numberglitchTicksLeft--;
if (this.Demo.glitchTicksLeft: numberglitchTicksLeft <= 0) {
// Burst finished: schedule the next cooldown and re-roll the band wobble
// cooldown 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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(const BAND_WOBBLE_COOLDOWN_MIN: 100BAND_WOBBLE_COOLDOWN_MIN, const BAND_WOBBLE_COOLDOWN_MAX: 280BAND_WOBBLE_COOLDOWN_MAX);
}
// Return early: the rest of the state machine does not run during a burst.
return;
}
// No fault active: apply the calm resting look.
this.Demo.applyRestingCrtUniforms(): voidapplyRestingCrtUniforms();
// --- Band wobble (pixel-tier mild jitter) ---
if (this.Demo.bandWobbleActive: numberbandWobbleActive > 0) {
// Same bell-curve envelope as the fault bursts, at a much 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 | nullpixelGlitch.PixelGlitch.intensity: numberGlitch strength in `[0, 1]`. Scales the per-band horizontal shift
magnitude. Bands are shifted when their hash exceeds ~0.85 (~15% of
bands). `0` disables.intensity = const BAND_WOBBLE_INTENSITY: 0.11BAND_WOBBLE_INTENSITY * const envelope: anyenvelope;
this.Demo.pixelGlitch: PixelGlitch | nullpixelGlitch.PixelGlitch.seed: numberPer-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 | nullpixelGlitch.PixelGlitch.intensity: numberGlitch strength in `[0, 1]`. Scales the per-band horizontal shift
magnitude. Bands are shifted when their hash exceeds ~0.85 (~15% of
bands). `0` disables.intensity = 0;
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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(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. A different seed gives a different row
// pattern.
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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.float(min: number, max: number): numberReturns the next pseudo-random float in [min, max).float(0, 1000);
}
}
// --- Cooldown before the next TV fault ---
this.Demo.glitchCooldown: numberglitchCooldown--;
if (this.Demo.glitchCooldown: numberglitchCooldown <= 0) {
// Time for a fault. pick() returns one item of a list; float(min, max) is the
// decimal counterpart 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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.pick<string>(arr: readonly string[]): stringReturns one element chosen uniformly from a non-empty array.pick(import GLITCH_TYPES_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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.float(min: number, max: number): numberReturns the next pseudo-random float in [min, max).float(const GLITCH_INTENSITY_MIN: 0.3GLITCH_INTENSITY_MIN, const GLITCH_INTENSITY_MAX: 0.95GLITCH_INTENSITY_MAX);
this.Demo.pixelGlitch: PixelGlitch | nullpixelGlitch.PixelGlitch.seed: numberPer-glitch random seed. Change between glitches to vary the band noise
pattern.seed = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.float(min: number, max: number): numberReturns the next pseudo-random float in [min, max).float(0, 1000);
}
}
// Resets every fault-driven uniform to the calm, between-faults Orava look. Called
// every tick, then overridden by applyGlitchUniforms() during a fault.
Demo.applyRestingCrtUniforms(): voidapplyRestingCrtUniforms() {
this.Demo.pixelGlitch: PixelGlitch | nullpixelGlitch.PixelGlitch.intensity: numberGlitch strength in `[0, 1]`. Scales the per-band horizontal shift
magnitude. Bands are shifted when their hash exceeds ~0.85 (~15% of
bands). `0` disables.intensity = 0;
this.Demo.aberration: ChromaticAberration | nullaberration.ChromaticAberration.aberration: numberChannel 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 | nullnoise.Noise.amount: numberNoise amplitude as a `[-amount, +amount]` additive perturbation on each
channel. Reasonable values are `0.005` to `0.05`. Set to `0` to disable.amount = const NOISE_BASE: 0.038NOISE_BASE;
this.Demo.flicker: Flicker | nullflicker.Flicker.amount: numberBrightness multiplier. `1` is unmodulated; values below `1` darken the
frame. The demo typically updates this each frame from a sin wave or
random source.amount = const FLICKER_BASE: 1FLICKER_BASE;
this.Demo.interference: Interference | nullinterference.Interference.amount: numberMaximum horizontal offset as a UV fraction (e.g. `0.06` shifts the row
by up to ~6% of the image width). Set to `0` to disable.amount = const INTERFERENCE_BASE: 0INTERFERENCE_BASE;
this.Demo.rollLine: RollLine | nullrollLine.RollLine.amount: numberRoll line amplitude (mix factor onto a brightness boost).amount = const ROLL_BASE: 0.26ROLL_BASE;
this.Demo.rollLine: RollLine | nullrollLine.RollLine.speed: numberScroll speed multiplier; final scroll velocity = `time * speed`.speed = const ROLL_SPEED: 0.92ROLL_SPEED;
}
// Layers one of the five Orava TV faults onto the resting uniforms. `envelope` is the
// bell-curve value for this tick: 0 at the start and end of a burst, 1 in the middle.
Demo.applyGlitchUniforms(envelope: any): voidapplyGlitchUniforms(envelope: anyenvelope) {
// peak is how strong this specific burst is, scaled by the bell-curve envelope.
const const peak: numberpeak = this.Demo.glitchPeak: numberglitchPeak * envelope: anyenvelope;
if (this.Demo.glitchType: stringglitchType === 'hshift') {
// H-HOLD: a horizontal band shift in the pixel-tier index buffer, as if the
// image slipped sideways on the tube.
this.Demo.pixelGlitch: PixelGlitch | nullpixelGlitch.PixelGlitch.intensity: numberGlitch strength in `[0, 1]`. Scales the per-band horizontal shift
magnitude. Bands are shifted when their hash exceeds ~0.85 (~15% of
bands). `0` disables.intensity = 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 | nullaberration.ChromaticAberration.aberration: numberChannel 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 | nullnoise.Noise.amount: numberNoise amplitude as a `[-amount, +amount]` additive perturbation on each
channel. Reasonable values are `0.005` to `0.05`. Set to `0` to disable.amount = 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 a strong burst dims
// deeper than a weak one.
this.Demo.flicker: Flicker | nullflicker.Flicker.amount: numberBrightness multiplier. `1` is unmodulated; values below `1` darken the
frame. The demo typically updates this each frame from a sin wave or
random source.amount = 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 | nullinterference.Interference.amount: numberMaximum horizontal offset as a UV fraction (e.g. `0.06` shifts the row
by up to ~6% of the image width). Set to `0` to disable.amount = 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 | nullrollLine.RollLine.amount: numberRoll line amplitude (mix factor onto a brightness boost).amount = const ROLL_BASE: 0.26ROLL_BASE + const peak: numberpeak * 0.35;
this.Demo.rollLine: RollLine | nullrollLine.RollLine.speed: numberScroll speed multiplier; final scroll velocity = `time * speed`.speed = const ROLL_SPEED: 0.92ROLL_SPEED + const peak: numberpeak * 1.8;
}
}
}
// Hand the Demo class to the engine. bootstrap() sets up the canvas, picks a backend,
// creates one Demo instance, then drives configure() -> init() -> the 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.bootstrap(class DemoDemo);