/*
Basics Enhanced: the Basics bouncing sprite behind a hand-built CRT post-process chain.
@description The bouncing sprite from Basics again, with optional visual effects over the same PipBoy palette.
Part of the BLIT386 demo series.
Prerequisites:
- Basics: https://demos.blit386.dev/basics
- PipBoy CRT: https://demos.blit386.dev/crt-pipboy
- CRT Toggle: https://demos.blit386.dev/crt-toggle
Live version: https://demos.blit386.dev/basics-enhanced
Guides:
- Post-Process Effects: https://blit386.dev/docs/guides/post-process-effects#pixel-tier-effects
- Game Loop: https://blit386.dev/docs/guides/game-loop#smoothing-motion-between-updates
- Overlay: https://blit386.dev/docs/guides/overlay#custom-rows
Reference:
- Rendering: https://blit386.dev/docs/api/rendering#post-process-effects
- Game Loop: https://blit386.dev/docs/api/game-loop#interpolating-render-state-with-renderalpha
- Overlay: https://blit386.dev/docs/api/overlay#custom-rows
---
WHAT YOU WILL SEE
-----------------
The same scene as Basics: the 32x28 BLIT386 logo bouncing around a 320x240 PipBoy-green
screen at 30 ticks per second, with a dim UI kit hint in the top-left corner. Under WebGPU
the whole frame is additionally routed through eleven post-process effects: a curved tube,
scanlines, an RGB mask, vignette, grain, flicker, bloom, a slow roll line, and - every few
seconds - a short glitch burst that tears, splits, snows, dims, or ghosts the picture for
a fraction of a second before the screen settles again.
The engine overlay (Backquote) carries four custom rows: Position, Bounces, CRT stack
ON/OFF, and the current glitch type with its strength in percent. Each wall hit drops an
"H" or "V" tag on the timing chart.
WHAT YOU WILL LEARN
-------------------
- The two post-process tiers and why they differ:
1. Pixel tier - runs on the logical index buffer (320x240 palette indices, before the
palette is resolved into RGB). Effects here distort the indexed image itself. Only
PixelGlitch sits here.
2. Display tier - runs after the palette is resolved and the image is upscaled to
drawingBufferSize (960x720 here, 3x logical). Effects here work in full-color RGB
and can blur, warp, tint, and bloom the final image. The other ten effects in this
demo live here.
- Why hand-compose ten display-tier effects instead of calling BT.preset.crtPipBoy() as
CRT Toggle does: keeping each effect object lets update() drive individual uniforms
from a state machine. The glitch machine picks one of five glitch types, ramps it up
over a sine envelope, and ramps it down again.
- BT.effectAdd(effect) registers effects in draw order: the first added runs first.
- Animated effects (RollLine, Noise, Interference) need BT.timeSeconds fed into their
time uniform every tick.
- Smoothing a 30-tick-per-second simulation with BT.renderAlpha and Vector2i.lerp, the
same fix Basics explains in detail.
- Overlay customization: overlayStyle, overlayTimingChartStyle, overlayRows(), and
BT.assignTag().
SOFTWARE FALLBACK
-----------------
Post-process needs WebGPU. Under the software renderer the bouncing sprite still runs, but
no effect is registered, the glitch state machine never starts, and a warm two-line note
drawn with the shared UI kit (src/shared/ui.js) plus the "CRT stack OFF (software)"
overlay row explain the reduced mode.
TRY THIS
--------
- Press Backquote (`) or click/tap the symbol in the bottom-left corner of the canvas to
open the engine overlay. Watch the Glitch row during a burst: the label names the fault
(H-HOLD, CHROMA, SNOW, DIM, GHOST) and the percentage is its rolled peak strength.
- Bursts arrive every 4-12 seconds and last 0.17-1 second (120-360 and 5-30 ticks at 30
ticks per second).
- Add ?backend=software to the URL to see the fallback path.
*/
import {
type BarrelDistortion = BarrelDistortion
class BarrelDistortion
Barrel distortion that warps UVs outward from the screen center.
Display-tier: operates on the upscaled output. Applying this in the pixel
tier (logical 320x240) discretizes the curve onto the source texel grid,
which CSS upscale then magnifies into visible step artifacts. At output
resolution the curve has enough resolution to express smoothly.
The math comes from Timothy Lottes's public-domain `crt-lottes.glsl`:
`warp(uv) = uv + delta * d2 * curvature` where `delta = uv - 0.5` and
`d2 = dot(delta, delta)`.BarrelDistortion,
type Bloom = Bloom
class Bloom
Single-pass box-blur bloom.
Samples a 5x5 neighborhood (25 taps) around each fragment, averages, then
mixes with the original color by
{@link
glow
}
.
{@link
spread
}
scales the
texel offset so the bloom radius can be tuned independently of the source
resolution.
Display-tier: bloom mixes neighboring pixels into intermediate hues that
are not in the active palette. Running it in pixel space would violate the
palette-pixel aesthetic; running it on the upscaled output reads as the
warm phosphor glow of an old monitor instead.
The implementation matches the original PipBoy bloom shader. A future
optimization would be a two-pass separable Gaussian (5 + 5 = 10 taps); add
it once a GPU perf test demands it.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 ABERRATION_BASEABERRATION_BASE,
import applyGlitchUniformsapplyGlitchUniforms,
import FLICKER_BASEFLICKER_BASE,
import GLITCH_ACTIVE_MAXGLITCH_ACTIVE_MAX,
import GLITCH_ACTIVE_MINGLITCH_ACTIVE_MIN,
import GLITCH_COOLDOWN_MAXGLITCH_COOLDOWN_MAX,
import GLITCH_COOLDOWN_MINGLITCH_COOLDOWN_MIN,
import GLITCH_INTENSITY_MAXGLITCH_INTENSITY_MAX,
import GLITCH_INTENSITY_MINGLITCH_INTENSITY_MIN,
import GLITCH_LABELSGLITCH_LABELS,
import GLITCH_TYPES_CHROMAGLITCH_TYPES_CHROMA,
import NOISE_BASENOISE_BASE,
import PIXEL_GLITCH_BAND_HEIGHTPIXEL_GLITCH_BAND_HEIGHT,
import resetGlitchUniformsresetGlitchUniforms,
} from './shared/crt-glitch.js';
import { import isAvailableisAvailable, import SOFTWARE_FALLBACK_NOTESOFTWARE_FALLBACK_NOTE } from './shared/post-process-backend.js';
import { import applyThemeapplyTheme, import uiui, import UI_ANCHORSUI_ANCHORS } from './shared/ui.js';
/** @typedef {import('blit386').Palette} Palette */
/** @typedef {import('blit386').SpriteSheet} SpriteSheet */
/** @typedef {import('blit386').Rect2i} Rect2i */
/** @typedef {import('blit386').PixelGlitch} PixelGlitch */
/** @typedef {import('blit386').BarrelDistortion} BarrelDistortion */
/** @typedef {import('blit386').ChromaticAberration} ChromaticAberration */
/** @typedef {import('blit386').Interference} Interference */
/** @typedef {import('blit386').RollLine} RollLine */
/** @typedef {import('blit386').Scanlines} Scanlines */
/** @typedef {import('blit386').RGBMask} RGBMask */
/** @typedef {import('blit386').Vignette} Vignette */
/** @typedef {import('blit386').Noise} Noise */
/** @typedef {import('blit386').Flicker} Flicker */
/** @typedef {import('blit386').Bloom} Bloom */
// Palette slots match the Basics demo so the two demos read as the same scene.
const const C_BG: 1C_BG = 1; // almost-black with a faint green tint
const const C_OVERLAY_BAR: 2C_OVERLAY_BAR = 2; // bar behind overlay custom rows
const const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN = 3; // PipBoy green (position, CRT status)
const const C_OVERLAY_AMBER: 4C_OVERLAY_AMBER = 4; // amber accent (bounces, glitch readout)
const const C_OVERLAY_ERROR: 5C_OVERLAY_ERROR = 5; // red tint for timing-chart error markers
const const SPRITE_BASE: 10SPRITE_BASE = 10;
const const SPRITE_URL: "/sprites/logo-1.png"SPRITE_URL = '/sprites/logo-1.png';
const const TARGET_FPS: 30TARGET_FPS = 30;
// The display-tier chain runs on an output buffer larger than the logical screen: 3x.
const const OUTPUT_W: 960OUTPUT_W = 960;
const const OUTPUT_H: 720OUTPUT_H = 720;
// The shared fallback note is one long sentence, too wide for a 320-pixel screen in the
// 6-pixel-wide system font. split('. ') cuts it at the sentence break into two shorter
// lines the UI kit draws one under the other.
const const FALLBACK_LINES: anyFALLBACK_LINES = import SOFTWARE_FALLBACK_NOTESOFTWARE_FALLBACK_NOTE.split('. ');
// The Basics demo plus a hand-built CRT post-process chain and periodic glitch bursts.
class class DemoDemo {
// --- Bouncing sprite (same roles as the Basics demo) ---
// Top-left corner of the logo on screen, in whole pixels.
Demo.pos: Vector2ipos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(160, 120);
// Pixels the logo moves per update() tick, x and y separately.
Demo.speed: Vector2ispeed = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(1, 1);
// Logo position at the start of the most recent update() tick, before that tick moved
// it. render() blends between this and pos using BT.renderAlpha so the logo glides
// between ticks instead of jumping - the same fix as Basics, whose render() comment
// explains why targetFPS 30 makes the stutter especially visible.
Demo.prevPos: Vector2iprevPos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(160, 120);
// Logo width and height in pixels; filled from the loaded PNG in init().
Demo.size: Vector2isize = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(16, 16);
// Counts wall hits so overlayRows() can show a running total.
Demo.bounces: numberbounces = 0;
// Every color this demo draws with; built in init().
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = null;
// Loaded indexed sprite sheet (GPU texture plus palette mapping).
/** @type {SpriteSheet | null} */
Demo.spriteSheet: SpriteSheet | nullspriteSheet = null;
// Which rectangle inside the PNG to draw: the full image for this logo.
/** @type {Rect2i | null} */
Demo.spriteRect: Rect2i | nullspriteRect = null;
// --- Post-process effect handles (WebGPU only) ---
// Pixel tier: shifts horizontal bands in the index buffer before palette resolve.
/** @type {PixelGlitch | null} */
Demo.pixelGlitch: PixelGlitch | nullpixelGlitch = null;
// Display tier: the CRT stack, run on the upscaled RGBA buffer after palette resolve.
/** @type {BarrelDistortion | null} */
Demo.barrel: BarrelDistortion | nullbarrel = null;
/** @type {ChromaticAberration | null} */
Demo.aberration: ChromaticAberration | nullaberration = null;
/** @type {Interference | null} */
Demo.interference: Interference | nullinterference = null;
/** @type {RollLine | null} */
Demo.rollLine: RollLine | nullrollLine = null;
/** @type {Scanlines | null} */
Demo.scanlines: Scanlines | nullscanlines = null;
/** @type {RGBMask | null} */
Demo.mask: RGBMask | nullmask = null;
/** @type {Vignette | null} */
Demo.vignette: Vignette | nullvignette = null;
/** @type {Noise | null} */
Demo.noise: Noise | nullnoise = null;
/** @type {Flicker | null} */
Demo.flicker: Flicker | nullflicker = null;
/** @type {Bloom | null} */
Demo.bloom: Bloom | nullbloom = null;
// --- Glitch state machine (same idea as the PipBoy CRT demo) ---
// Ticks until the next random glitch burst starts; counts down while idle.
Demo.glitchCooldown: numberglitchCooldown = 0;
// Ticks remaining in the current burst; 0 means a calm screen.
Demo.glitchTicksLeft: numberglitchTicksLeft = 0;
// How long this burst was scheduled to last, used for the fade envelope.
Demo.glitchDuration: numberglitchDuration = 0;
// Which glitch type is active: 'none' or one of GLITCH_TYPES_CHROMA.
Demo.glitchType: stringglitchType = 'none';
// Peak strength rolled for this burst, 0..1 before the envelope is applied.
Demo.glitchPeak: numberglitchPeak = 0;
// True when WebGPU post-process is available; false in the software fallback.
Demo.effectsAvailable: booleaneffectsAvailable = false;
// Reused every frame for overlayRows(): position, bounces, CRT status, glitch
// readout.
Demo.overlayRowData: {}overlayRowData = [
{ leftText: stringleftText: 'Position (0, 0)', textPaletteIndex: numbertextPaletteIndex: const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN },
{ leftText: stringleftText: 'Bounces 0', textPaletteIndex: numbertextPaletteIndex: const C_OVERLAY_AMBER: 4C_OVERLAY_AMBER },
{ leftText: stringleftText: 'CRT stack OFF', textPaletteIndex: numbertextPaletteIndex: const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN },
{ leftText: stringleftText: 'Glitch NONE', textPaletteIndex: numbertextPaletteIndex: const C_OVERLAY_AMBER: 4C_OVERLAY_AMBER },
];
Demo.configure(): {
displaySize: Vector2i;
drawingBufferSize: Vector2i;
maxCanvasSize: Vector2i;
outputUpscaleFilter: string;
targetFPS: number;
isDetectingDroppedFrames: boolean;
isOverlayTimingChartEnabled: boolean;
overlayStyle: {
barPaletteIndex: number;
textPaletteIndex: number;
gapPaletteIndex: number;
};
overlayTimingChartStyle: {
updateBarPaletteIndex: number;
renderBarPaletteIndex: number;
warningPaletteIndex: number;
errorPaletteIndex: number;
tagPaletteIndex: number;
};
}
configure() {
return {
displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(320, 240),
// The display-tier CRT runs on the upscaled RGBA buffer, 3x logical here.
drawingBufferSize: Vector2idrawingBufferSize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const OUTPUT_W: 960OUTPUT_W, const OUTPUT_H: 720OUTPUT_H),
// The demos layout may scale the canvas up to 4x logical on screen; the
// engine default cap is 960x720.
maxCanvasSize: Vector2imaxCanvasSize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(320 * 4, 240 * 4),
outputUpscaleFilter: stringoutputUpscaleFilter: 'nearest',
targetFPS: numbertargetFPS: const TARGET_FPS: 30TARGET_FPS,
isDetectingDroppedFrames: booleanisDetectingDroppedFrames: true,
// Opt in to the overlay timing chart (update vs render CPU bars above the
// FPS row). Bar colors default to overlayStyle; explicit indices keep them
// on this palette.
isOverlayTimingChartEnabled: booleanisOverlayTimingChartEnabled: true,
overlayStyle: {
barPaletteIndex: number;
textPaletteIndex: number;
gapPaletteIndex: number;
}
overlayStyle: {
barPaletteIndex: numberbarPaletteIndex: const C_OVERLAY_BAR: 2C_OVERLAY_BAR,
textPaletteIndex: numbertextPaletteIndex: const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN,
gapPaletteIndex: numbergapPaletteIndex: const C_OVERLAY_BAR: 2C_OVERLAY_BAR,
},
overlayTimingChartStyle: {
updateBarPaletteIndex: number;
renderBarPaletteIndex: number;
warningPaletteIndex: number;
errorPaletteIndex: number;
tagPaletteIndex: number;
}
overlayTimingChartStyle: {
updateBarPaletteIndex: numberupdateBarPaletteIndex: const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN,
renderBarPaletteIndex: numberrenderBarPaletteIndex: const C_OVERLAY_AMBER: 4C_OVERLAY_AMBER,
warningPaletteIndex: numberwarningPaletteIndex: const C_OVERLAY_AMBER: 4C_OVERLAY_AMBER,
errorPaletteIndex: numbererrorPaletteIndex: const C_OVERLAY_ERROR: 5C_OVERLAY_ERROR,
tagPaletteIndex: numbertagPaletteIndex: const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN,
},
};
}
async Demo.init(): unknowninit() {
// --- Palette (matches the Basics demo PipBoy green scene) ---
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);
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(16, 28, 16));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_BAR: 2C_OVERLAY_BAR, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(24, 44, 28));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(80, 200, 110));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_AMBER: 4C_OVERLAY_AMBER, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(220, 180, 60));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_ERROR: 5C_OVERLAY_ERROR, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(200, 70, 70));
// --- Sprite load (same two-step path as the Basics demo) ---
// Step 1: scan the PNG and copy every unique color into palette slots starting at
// SPRITE_BASE, so indexed drawing knows which slot each pixel uses.
await class 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: 10SPRITE_BASE);
// Step 2: loadIndexed builds the GPU sheet plus a full-image source rectangle.
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: 10SPRITE_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;
// Install the shared UI kit colors. The scene owns slots 1-5 and the logo owns
// slots from SPRITE_BASE up, so the kit's default range (240-251, at the top of
// the palette) is free. The kit colors are only for the on-canvas hint and
// fallback note drawn in render().
import applyThemeapplyTheme(this.Demo.palette: Palettepalette);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteSet: (palette: Palette) => voidStores the active engine palette.
Use this to swap the **entire palette** (e.g. switch between a day and night
theme). After this call the renderer uploads the new palette uniform on the
next frame.
**Palette-value swap (change what a slot looks like):** mutate the live
{@link
BT.palette
}
in place with `palette.set(slot, newColor)`. The renderer
uploads dirty slots on the next frame; no `paletteSet()` or
{@link
BT.spritesRefresh
}
needed.
**Palette-layout swap (same colors, different slot positions):** build a new
palette with the same colors at new indices, call `paletteSet()`, then call
{@link
BT.spritesRefresh
}
so every sprite sheet re-maps its original RGBA
pixels against the new slot layout.paletteSet(this.Demo.palette: Palettepalette);
this.Demo.size: Vector2isize = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(this.Demo.spriteSheet: SpriteSheet | nullspriteSheet.SpriteSheet.size: Vector2iGets the sprite-sheet dimensions in pixels.size.Vector2i.x: numberHorizontal component (defaults to 0).x, this.Demo.spriteSheet: SpriteSheet | nullspriteSheet.SpriteSheet.size: Vector2iGets the sprite-sheet dimensions in pixels.size.Vector2i.y: numberVertical component (defaults to 0).y);
this.Demo.pos: Vector2ipos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(
Math.floor(const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 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.Vector2i.x: numberHorizontal component (defaults to 0).x / 2 - this.Demo.size: Vector2isize.Vector2i.x: numberHorizontal component (defaults to 0).x / 2),
Math.floor(const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 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.Vector2i.y: numberVertical component (defaults to 0).y / 2 - this.Demo.size: Vector2isize.Vector2i.y: numberVertical component (defaults to 0).y / 2),
);
// Keep prevPos in sync with the real starting position so the very first render()
// does not slide in from the (160, 120) placeholder.
this.Demo.prevPos: Vector2iprevPos = this.Demo.pos: Vector2ipos;
// Post-process requires WebGPU; the software renderer skips the whole CRT stack.
this.Demo.effectsAvailable: booleaneffectsAvailable = import isAvailableisAvailable();
if (!this.Demo.effectsAvailable: booleaneffectsAvailable) {
// Software renderer: update() never runs the glitch machine, so there is
// nothing to seed.
return true;
}
// --- Pixel tier: chunky band glitch on the index buffer ---
this.Demo.pixelGlitch: PixelGlitch | 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 = import PIXEL_GLITCH_BAND_HEIGHTPIXEL_GLITCH_BAND_HEIGHT;
// Intensity 0 means no shift; the state machine raises it during hshift bursts.
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: hand-built CRT chain ---
// Resting values; the glitch machine mutates aberration, interference, noise, and
// flicker during a burst and restores them afterwards.
this.Demo.barrel: BarrelDistortion | nullbarrel = new new BarrelDistortion(): BarrelDistortionBarrel distortion that warps UVs outward from the screen center.
Display-tier: operates on the upscaled output. Applying this in the pixel
tier (logical 320x240) discretizes the curve onto the source texel grid,
which CSS upscale then magnifies into visible step artifacts. At output
resolution the curve has enough resolution to express smoothly.
The math comes from Timothy Lottes's public-domain `crt-lottes.glsl`:
`warp(uv) = uv + delta * d2 * curvature` where `delta = uv - 0.5` and
`d2 = dot(delta, delta)`.BarrelDistortion();
this.Demo.barrel: BarrelDistortionbarrel.BarrelDistortion.curvature: numberCurvature strength. Typical values:
- `0.02` - subtle, like a flat-screen monitor.
- `0.05` - moderate desktop CRT.
- `0.10` - heavy small CRT or pocket TV.curvature = 0.05;
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 = import ABERRATION_BASEABERRATION_BASE;
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 = 0;
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 = 0.1;
this.Demo.rollLine: RollLinerollLine.RollLine.speed: numberScroll speed multiplier; final scroll velocity = `time * speed`.speed = 1.0;
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.55;
this.Demo.scanlines: Scanlinesscanlines.Scanlines.strength: numberNegative gaussian falloff parameter for scanline brightness. More
negative values produce sharper dark bands. PipBoy reference: `-8.0`.strength = -8;
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 = 240;
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.18;
this.Demo.mask: RGBMaskmask.RGBMask.size: numberMask cell pitch in output (display-chain) pixels. Smaller = denser mask.size = 6;
this.Demo.mask: RGBMaskmask.RGBMask.border: numberBorder darkening within each mask cell. 0 disables, 1 strong.border = 0.5;
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.35;
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 = import NOISE_BASENOISE_BASE;
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 = import FLICKER_BASEFLICKER_BASE;
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 = 3.0;
this.Demo.bloom: Bloombloom.Bloom.glow: numberMix factor between the original sample and the blurred neighborhood.glow = 0.18;
// Register every display-tier effect in draw order: the first added runs first.
for (const const fx: anyfx of [
this.Demo.barrel: BarrelDistortionbarrel,
this.Demo.aberration: ChromaticAberrationaberration,
this.Demo.interference: Interferenceinterference,
this.Demo.rollLine: RollLinerollLine,
this.Demo.scanlines: Scanlinesscanlines,
this.Demo.mask: RGBMaskmask,
this.Demo.vignette: Vignettevignette,
this.Demo.noise: Noisenoise,
this.Demo.flicker: Flickerflicker,
this.Demo.bloom: Bloombloom,
]) {
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.effectAdd: (effect: Effect) => voidAppends a fullscreen post-processing effect to whichever chain matches
its declared
{@link
Effect.tier
}
.
- `tier='pixel'` -> pixel chain (logical resolution).
- `tier='display'` -> display chain (output resolution); requires
`drawingBufferSize` in effective hardware settings (`configure()` or
`defaultConfig()`).
Effects run in registration order within each tier. The pixel chain runs
first, followed by the upscale pass, followed by the display chain. Each
{@link
Effect
}
instance owns its own GPU resources and may be mutated
each frame from demo code.effectAdd(const fx: anyfx);
}
// BT.random is the engine's shared random number generator. int(min, max) returns
// a whole number from min up to but not including max, so this waits a random
// number of ticks before the first burst.
this.Demo.glitchCooldown: numberglitchCooldown = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(import GLITCH_COOLDOWN_MINGLITCH_COOLDOWN_MIN, import GLITCH_COOLDOWN_MAXGLITCH_COOLDOWN_MAX);
this.Demo.glitchTicksLeft: numberglitchTicksLeft = 0;
this.Demo.glitchDuration: numberglitchDuration = 0;
this.Demo.glitchType: stringglitchType = 'none';
this.Demo.glitchPeak: numberglitchPeak = 0;
return true;
}
Demo.update(): voidupdate() {
// --- Bounce logic (same rules as Basics; game logic lives only in update()) ---
// Remember where the logo was before this tick moves it, so render() can draw a
// smooth in-between position instead of a pop.
this.Demo.prevPos: Vector2iprevPos = this.Demo.pos: Vector2ipos;
// Move the logo by adding speed to position: one step per tick.
this.Demo.pos: Vector2ipos = this.Demo.pos: Vector2ipos.Vector2i.add(other: Vector2i): Vector2iAdds another vector and returns the result as a new vector.add(this.Demo.speed: Vector2ispeed);
// Left/right wall test. pos is the sprite's top-left corner, so the right edge is
// at pos.x + size.x, compared against displaySize.x - size.x.
if (this.Demo.pos: Vector2ipos.Vector2i.x: numberHorizontal component (defaults to 0).x <= 0 || this.Demo.pos: Vector2ipos.Vector2i.x: numberHorizontal component (defaults to 0).x >= const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 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.Vector2i.x: numberHorizontal component (defaults to 0).x - this.Demo.size: Vector2isize.Vector2i.x: numberHorizontal component (defaults to 0).x) {
// Flip the horizontal direction.
this.Demo.speed: Vector2ispeed.Vector2i.x: numberHorizontal component (defaults to 0).x = -this.Demo.speed: Vector2ispeed.Vector2i.x: numberHorizontal component (defaults to 0).x;
this.Demo.bounces: numberbounces++;
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.assignTag: (label?: string) => voidPlaces a labeled marker on the overlay timing chart at the current tick.
Requires `isOverlayTimingChartEnabled: true` in `configure()`. Tags scroll with the chart
history and are pruned when they leave the visible window. Empty labels become
`"Untitled"`. Chart width resets add an automatic `"Start"` tag.assignTag('H');
}
// Top/bottom wall test uses the same idea on the y axis.
if (this.Demo.pos: Vector2ipos.Vector2i.y: numberVertical component (defaults to 0).y <= 0 || this.Demo.pos: Vector2ipos.Vector2i.y: numberVertical component (defaults to 0).y >= const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 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.Vector2i.y: numberVertical component (defaults to 0).y - this.Demo.size: Vector2isize.Vector2i.y: numberVertical component (defaults to 0).y) {
this.Demo.speed: Vector2ispeed.Vector2i.y: numberVertical component (defaults to 0).y = -this.Demo.speed: Vector2ispeed.Vector2i.y: numberVertical component (defaults to 0).y;
this.Demo.bounces: numberbounces++;
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.assignTag: (label?: string) => voidPlaces a labeled marker on the overlay timing chart at the current tick.
Requires `isOverlayTimingChartEnabled: true` in `configure()`. Tags scroll with the chart
history and are pruned when they leave the visible window. Empty labels become
`"Untitled"`. Chart width resets add an automatic `"Start"` tag.assignTag('V');
}
// Animated CRT uniforms need elapsed time; skip when effects are unavailable.
if (this.Demo.effectsAvailable: booleaneffectsAvailable) {
const const seconds: numberseconds = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 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;
} else {
return;
}
// --- Glitch state machine (PipBoy CRT demo pattern) ---
if (this.Demo.glitchTicksLeft: numberglitchTicksLeft > 0) {
// Inside a burst: build a 0 -> 1 -> 0 envelope so the effect ramps in and
// out. t goes from 0 at burst start to 1 on the last tick; sin(t * PI) is a
// smooth hump.
const const t: numbert = 1 - (this.Demo.glitchTicksLeft: numberglitchTicksLeft - 1) / this.Demo.glitchDuration: numberglitchDuration;
const const envelope: anyenvelope = Math.sin(const t: numbert * Math.PI);
import applyGlitchUniformsapplyGlitchUniforms(this, const envelope: anyenvelope);
this.Demo.glitchTicksLeft: numberglitchTicksLeft--;
if (this.Demo.glitchTicksLeft: numberglitchTicksLeft <= 0) {
// Burst finished: return effect uniforms to their calm resting values.
import resetGlitchUniformsresetGlitchUniforms(this);
this.Demo.glitchCooldown: numberglitchCooldown = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(import GLITCH_COOLDOWN_MINGLITCH_COOLDOWN_MIN, import GLITCH_COOLDOWN_MAXGLITCH_COOLDOWN_MAX);
}
return;
}
// Idle between bursts: count down the cooldown ticks.
this.Demo.glitchCooldown: numberglitchCooldown--;
if (this.Demo.glitchCooldown: numberglitchCooldown <= 0) {
// Roll a new burst. 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_CHROMAGLITCH_TYPES_CHROMA);
this.Demo.glitchDuration: numberglitchDuration = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(import GLITCH_ACTIVE_MINGLITCH_ACTIVE_MIN, import GLITCH_ACTIVE_MAXGLITCH_ACTIVE_MAX);
this.Demo.glitchTicksLeft: numberglitchTicksLeft = this.Demo.glitchDuration: numberglitchDuration;
this.Demo.glitchPeak: numberglitchPeak = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.float(min: number, max: number): numberReturns the next pseudo-random float in [min, max).float(import GLITCH_INTENSITY_MINGLITCH_INTENSITY_MIN, import GLITCH_INTENSITY_MAXGLITCH_INTENSITY_MAX);
// Fresh seed so the PixelGlitch band noise looks different each burst.
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);
}
}
Demo.render(): voidrender() {
// Clear the logical framebuffer to the PipBoy background color (slot C_BG, set in
// init()).
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);
// Blend prevPos toward pos by BT.renderAlpha to get the logo's position at this
// exact render moment instead of only its last-tick position. Same fix, same
// reason as the Basics demo.
const const drawPos: Vector2idrawPos = 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.Vector2i.lerp(a: Vector2i, b: Vector2i, t: number): Vector2iLinearly interpolates between two vectors.
Result is truncated to integers. t is clamped to [0, 1].lerp(this.Demo.prevPos: Vector2iprevPos, this.Demo.pos: Vector2ipos, const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.renderAlpha: numberFractional progress between the last completed fixed update and the next.
Intended for interpolating render state between fixed-update steps.renderAlpha);
// Draw the logo at its smoothed position. paletteOffset 0 keeps the sprite's
// original indexed colors from SPRITE_BASE.
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 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: SpriteSheet | nullspriteSheet, this.Demo.spriteRect: Rect2i | nullspriteRect, const drawPos: Vector2idrawPos, 0);
// On-canvas text drawn with the shared UI kit: a borderless label group (no
// ui.panel() call, so no box) pinned to the top-left corner. A small margin and
// padding keep it tucked near the edge.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { margin: numbermargin: 2, pad: numberpad: 2 });
// Hint: the engine overlay (FPS, position, CRT status) toggles with Backquote or
// the small symbol in the bottom-left corner of the canvas.
import uiui.label('Press ~ or click/tap the symbol below', { color: stringcolor: 'dim' });
// Software renderer: warn that the CRT look is missing. The shared note was split
// into two lines up top (FALLBACK_LINES) so it fits the screen width.
if (!this.Demo.effectsAvailable: booleaneffectsAvailable) {
for (const const line: anyline of const FALLBACK_LINES: anyFALLBACK_LINES) {
import uiui.label(const line: anyline, { color: stringcolor: 'warm' });
}
}
import uiui.end();
// Position, bounces, CRT stack, and glitch readout live in overlayRows(), not
// here. After this pass finishes, WebGPU runs the CRT post-process chain on the
// result.
}
// Position, bounces, CRT status, and glitch readout: the Basics rows plus two more.
Demo.overlayRows(): {}overlayRows() {
this.Demo.overlayRowData: {}overlayRowData[0].leftText = `Position (${this.Demo.pos: Vector2ipos.Vector2i.x: numberHorizontal component (defaults to 0).x}, ${this.Demo.pos: Vector2ipos.Vector2i.y: numberVertical component (defaults to 0).y})`;
this.Demo.overlayRowData: {}overlayRowData[1].leftText = `Bounces ${this.Demo.bounces: numberbounces}`;
if (this.Demo.effectsAvailable: booleaneffectsAvailable) {
this.Demo.overlayRowData: {}overlayRowData[2].leftText = 'CRT stack ON';
const const glitchLabel: anyglitchLabel = import GLITCH_LABELSGLITCH_LABELS[this.Demo.glitchType: stringglitchType] ?? 'NONE';
const const glitchValue: anyglitchValue = this.Demo.glitchTicksLeft: numberglitchTicksLeft > 0 ? Math.round(this.Demo.glitchPeak: numberglitchPeak * 100) : 0;
this.Demo.overlayRowData: {}overlayRowData[3].leftText = `Glitch ${const glitchLabel: anyglitchLabel} ${String(const glitchValue: anyglitchValue).padStart(2, '0')}%`;
} else {
// Software renderer: no CRT stack, so the glitch machine never fires. The
// full explanation lives on the canvas itself (see render()), not in the
// overlay.
this.Demo.overlayRowData: {}overlayRowData[2].leftText = 'CRT stack OFF (software)';
this.Demo.overlayRowData: {}overlayRowData[3].leftText = 'Glitch NONE';
}
return this.Demo.overlayRowData: {}overlayRowData;
}
}
function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>One-liner bootstrap function for BLIT386 demos.
Handles canvas retrieval and engine initialization. Backend selection
(WebGPU or software fallback) is managed internally by BTAPI.
This function provides a streamlined way to start a demo with sensible defaults
while allowing customization through options.bootstrap(class DemoDemo);