/*
Sprite Effects: palette offsets as free sprite color variants, under a Tesla Orava CRT.
@description Palette offsets as game effects: damage flash, silhouette, ghost fade, team colors, and day or night.
Part of the BLIT386 demo series.
Prerequisites:
- Basics: https://demos.blit386.dev/basics
- Sprites: https://demos.blit386.dev/sprites
Live version: https://demos.blit386.dev/sprite-effects
Guides:
- Palette: https://blit386.dev/docs/guides/palette#per-draw-palette-offsets-zero-cost-color-variants
- Post-process effects: https://blit386.dev/docs/guides/post-process-effects#display-tier-effects
- Overlay: https://blit386.dev/docs/guides/overlay#custom-rows
Reference:
- Palette: https://blit386.dev/docs/api/palette#per-draw-offset
- Palette: https://blit386.dev/docs/api/palette#fill-a-block-of-slots
- Assets: https://blit386.dev/docs/api/assets#register-colors-into-the-palette
- Overlay: https://blit386.dev/docs/api/overlay#custom-rows-optional
---
WHAT YOU WILL SEE
-----------------
A 640x400 screen with the same 44x44 test sprite drawn eleven times, each copy recolored
through a different palette offset. The copies sit in three UI kit panels, each sprite
with its effect name and a one-line note on when a game would use it centered under it:
- Static (full width, top): Normal, Silhouette, Team Red, Team Blue, Team Green, Frozen.
- Animated (bottom left): Damage (flashes white and red for 30 ticks, once every 3
seconds), Ghost (alpha pulsing between 40 and 180), Invincible (hue rotating through
the rainbow), Poisoned (green tint pulsing 5 times per second).
- Day/night cycle (bottom right): the sprite dims toward a cool blue night tint and back
over 20 seconds, and a marker in the sprite's brightest ambient color crosses a
180-pixel bar split into four quarters labeled Day, Sunset, Night, Dawn.
The panels fill the band between the engine overlay's top and bottom strips.
The engine overlay is visible from the start, with the palette grid, a 64-pixel timing
chart in 'rich' diagnostics mode, the renderer diagnostics bar, and two custom rows:
"Tesla Orava: ON" and "TV fault: <fault> <strength>%".
On WebGPU the whole picture runs through a display-tier stack tuned for 1970s
Czechoslovak black-and-white Tesla Orava CRT sets. On the software renderer the sprite
effects still work; a two-line warm note in the top-left corner says the CRT look is
missing, and the overlay row reads "Tesla Orava: CRT OFF (software)".
WHAT YOU WILL LEARN
-------------------
- Palette offsets: BT.drawSprite(sheet, srcRect, position, paletteOffset) shifts every
pixel's palette index by paletteOffset, so one sprite image yields any number of color
variants with no extra textures and no per-pixel work.
- Registering a sprite's colors with SpriteSheet.loadColorsIntoPalette() and
SpriteSheet.loadIndexed(), and reading the registered Color32 list back.
- palette.fillBlock(start, source, transform) to write one recolored copy of the base
colors per theme block.
- Static blocks built once in init() versus dynamic blocks rewritten every tick in
update(), with plain loops to avoid allocating a closure per tick.
- Color32.luminance, Color32.fromHSL(), and Color32.lerp() as recoloring tools.
- BT.effectAdd() with a pixel-tier effect (PixelGlitch) and a display-tier chain, plus a
tick-driven fault state machine timed with BT.random.
- Custom overlay rows via overlayRows(), overlay styles from configure(), and
BT.timeSeconds-driven effect uniforms.
HOW THE THEME BLOCKS WORK
-------------------------
Every pixel of an indexed sprite stores a palette index. SpriteSheet.loadIndexed()
registers the sprite's unique opaque colors at COLOR_BASE (slot 12) - six colors for the
test sprite, so block 0 occupies slots 12-17. Each further block is a recolored copy of
those six colors, colorCount slots higher: block k starts at COLOR_BASE + k * colorCount.
Drawing with paletteOffset = k * colorCount makes every pixel land in block k instead of
block 0. Thirteen blocks fit in slots 12-89, far below the shared UI theme at 240-251.
Blocks 0-7 (original, silhouette, damage white, damage red, team red, team blue, team
green, frozen) are filled once in init(). Blocks 8-12 (damage flash, ghost, invincible,
poison, day/night) are rewritten every tick in update(); render() only changes the offset
it draws with.
FULLSCREEN CRT
--------------
The sprite grid is drawn at 640x400. The engine resolves the indexed buffer, upscales it
3x, and runs a display-tier stack: BarrelDistortion (curved tube), Interference,
RollLine (an always-on bright band scrolling top to bottom), Scanlines, RGBMask (a faint
grille), Vignette (light bezel darkening), Noise, Flicker, and Bloom (soft phosphor
halation). A pixel-tier PixelGlitch adds an occasional subtle horizontal band wobble.
TV faults fire every 150-420 ticks and last 4-24 ticks each, with a sine envelope that
ramps the chosen fault in and out: H-HOLD (band tear), SNOW (noise), DIM (flicker), GHOST
(interference), V-ROLL (a faster, brighter roll band). The fault list and labels come from
src/shared/crt-glitch.js; the chroma split used by the color CRT demos is left out
because a black-and-white tube has no color channels to split. The same building blocks
are explained in the PipBoy CRT and Basics Enhanced demos:
https://demos.blit386.dev/crt-pipboy and https://demos.blit386.dev/basics-enhanced.
Post-process needs WebGPU. The software renderer draws the sprite effects without the CRT
stack and shows the shared fallback note instead.
The panels, captions, day/night bar, and fallback note are drawn with the shared UI kit
(src/shared/ui.js), which installs its twelve UI colors in slots 240-251 via applyTheme().
TRY THIS
--------
- Watch the "TV fault" overlay row: it names the active fault and its strength in percent,
and reads "NONE 00%" between bursts.
- Press the backquote key (`) to hide the overlay and see the full CRT picture; press it
again to bring the overlay back.
- Add ?backend=software to the URL to force the software renderer and compare the bare
sprite effects with the CRT version.
*/
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,
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 Rect2i = Rect2i
class Rect2i
Integer rectangle for pixel-perfect bounds and regions.
Used throughout the engine for sprite regions, display-space bounds, and
zero-allocation geometry helpers. Both convenience getters and allocation-free
`*To()` helpers are provided so callers can choose between readability and
hot-path efficiency.Rect2i,
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_LABELSGLITCH_LABELS, import GLITCH_TYPES_VROLLGLITCH_TYPES_VROLL } from './shared/crt-glitch.js';
import { import isAvailableisAvailable, import SOFTWARE_FALLBACK_NOTESOFTWARE_FALLBACK_NOTE } from './shared/post-process-backend.js';
import { import applyThemeapplyTheme, import FONT_WFONT_W, 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').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 */
// Where sprite colors start in the palette. The 13 theme blocks stack upward from here:
// with the 6-color test sprite the top block ends at slot 89, far below the shared UI
// theme colors at slots 240-251.
const const COLOR_BASE: 12COLOR_BASE = 12;
// Palette slots of the shared UI theme. applyTheme() in init() writes the twelve UI kit
// colors into slots 240-251 (its default start slot). configure() runs BEFORE init(), so
// the overlay styles below cannot read this.theme yet - these constants spell out where
// each theme color will land once init() runs. UI_GAP is a static slot outside both the
// sprite theme range (12-89) and the applyTheme() range (240-251), set directly in
// init().
const const UI_BG: 240UI_BG = 240; // 'ui_bg' - deep navy screen background
const const UI_TEXT: 244UI_TEXT = 244; // 'ui_text' - off-white primary text
const const UI_DIM: 245UI_DIM = 245; // 'ui_text_dim' - secondary gray text
const const UI_GAP: 252UI_GAP = 252; // gray gap between overlay bands
const const UI_HEADER: 246UI_HEADER = 246; // 'ui_header' - warm amber (render bars, chart warnings)
const const UI_ACCENT: 247UI_ACCENT = 247; // 'ui_accent' - phosphor green (update bars)
const const UI_WARM: 248UI_WARM = 248; // 'ui_accent_warm' - orange (chart error frames)
const const UI_INFO: 249UI_INFO = 249; // 'ui_info' - light blue (chart tags)
// Theme block indices (as palette offsets from COLOR_BASE). Each block contains one entry
// per unique sprite color - that count lives in this.colorCount, read in init(). Offset =
// blockIndex * colorCount. Blocks 0-7 are static; blocks 8-12 are dynamic (rewritten in
// update()).
//
// Block 0 (offset 0): Original sprite colors.
// Block 1 (offset colorCount): Silhouette - all colors near-black.
// Block 2 (offset 2 * colorCount): Damage white - all colors bright white.
// Block 3 (offset 3 * colorCount): Damage red - all colors shifted red.
// Block 4 (offset 4 * colorCount): Team red.
// Block 5 (offset 5 * colorCount): Team blue.
// Block 6 (offset 6 * colorCount): Team green.
// Block 7 (offset 7 * colorCount): Frozen (cool blue).
// Block 8 (offset 8 * colorCount): Damage flash (dynamic: alternating white/red).
// Block 9 (offset 9 * colorCount): Ghost (dynamic: pulsing low alpha).
// Block 10 (offset 10 * colorCount): Invincibility (dynamic: hue rotation).
// Block 11 (offset 11 * colorCount): Poison (dynamic: pulsing green brightness).
// Block 12 (offset 12 * colorCount): Day/night ambient (dynamic: brightness cycle).
const const BLOCK_ORIGINAL: 0BLOCK_ORIGINAL = 0;
const const BLOCK_SILHOUETTE: 1BLOCK_SILHOUETTE = 1;
const const BLOCK_DAMAGE_WHITE: 2BLOCK_DAMAGE_WHITE = 2;
const const BLOCK_DAMAGE_RED: 3BLOCK_DAMAGE_RED = 3;
const const BLOCK_TEAM_RED: 4BLOCK_TEAM_RED = 4;
const const BLOCK_TEAM_BLUE: 5BLOCK_TEAM_BLUE = 5;
const const BLOCK_TEAM_GREEN: 6BLOCK_TEAM_GREEN = 6;
const const BLOCK_FROZEN: 7BLOCK_FROZEN = 7;
const const BLOCK_DAMAGE_FLASH: 8BLOCK_DAMAGE_FLASH = 8; // dynamic
const const BLOCK_GHOST: 9BLOCK_GHOST = 9; // dynamic
const const BLOCK_INVINCIBLE: 10BLOCK_INVINCIBLE = 10; // dynamic
const const BLOCK_POISON: 11BLOCK_POISON = 11; // dynamic
const const BLOCK_DAYNIGHT: 12BLOCK_DAYNIGHT = 12; // dynamic
// Logical game resolution (where render() draws). The demos page scales this up on
// screen.
const const DISPLAY_W: 640DISPLAY_W = 640;
const const DISPLAY_H: 400DISPLAY_H = 400;
// Layout of the three kit panels. The overlay starts visible and covers the top 122 and
// the bottom 82 pixels of the screen, so the panels sit centered in the band between:
// Static across the full width on top, Animated and Day/night side by side below it.
const const PANEL_X: 8PANEL_X = 8;
const const PANEL_GAP: 8PANEL_GAP = 8;
const const PANEL_H: 80PANEL_H = 80;
const const STATIC_PANEL_Y: 136STATIC_PANEL_Y = 136;
const const ANIMATED_PANEL_Y: numberANIMATED_PANEL_Y = const STATIC_PANEL_Y: 136STATIC_PANEL_Y + const PANEL_H: 80PANEL_H + const PANEL_GAP: 8PANEL_GAP;
// Every sprite sits in a cell this wide, with its name and note centered under it. Six
// cells fill the Static panel exactly (6 x 104 = 624 = 640 - 2 x 8).
const const CELL_W: 104CELL_W = 104;
// The Animated panel holds four cells; the Day/night panel takes the rest of the row.
const const ANIMATED_PANEL_W: numberANIMATED_PANEL_W = 4 * const CELL_W: 104CELL_W;
const const DAYNIGHT_PANEL_X: numberDAYNIGHT_PANEL_X = const PANEL_X: 8PANEL_X + const ANIMATED_PANEL_W: numberANIMATED_PANEL_W + const PANEL_GAP: 8PANEL_GAP;
const const DAYNIGHT_PANEL_W: numberDAYNIGHT_PANEL_W = const DISPLAY_W: 640DISPLAY_W - const PANEL_X: 8PANEL_X - const DAYNIGHT_PANEL_X: numberDAYNIGHT_PANEL_X;
// A kit panel with a title is 33 pixels taller than the space a ui.spacer() reserves
// inside it (22 above the content for the rim and title, 11 below for the bottom rim).
const const PANEL_SPACER: numberPANEL_SPACER = const PANEL_H: 80PANEL_H - 33;
// Where things sit inside a panel, measured down from its top edge: the 44x44 sprite
// frame, then the effect name, then the one-line note.
const const SPRITE_TOP: 14SPRITE_TOP = 14;
const const NAME_Y: 54NAME_Y = 54;
const const NOTE_Y: 64NOTE_Y = 64;
// The test sprite's visible pixels span x = 4 to 38 inside its 44-pixel frame, so their
// middle is 21 pixels in. Centering on that (not on the frame) lines the artwork up with
// the centered captions.
const const ART_CENTER_X: 21ART_CENTER_X = 21;
// The day/night time bar: 180 pixels wide, so the four phase labels sit 45 pixels apart.
const const DAYNIGHT_BAR_W: 180DAYNIGHT_BAR_W = 180;
const const DAYNIGHT_PHASES: {}DAYNIGHT_PHASES = ['Day', 'Sunset', 'Night', 'Dawn'];
// The cells of the two effect panels: which theme block to draw with, the effect's
// name and its text color role, and a note on when a game would use it.
const const STATIC_CELLS: {}STATIC_CELLS = [
{ block: numberblock: const BLOCK_ORIGINAL: 0BLOCK_ORIGINAL, name: stringname: 'Normal', color: stringcolor: 'text', note: stringnote: 'Default look' },
{ block: numberblock: const BLOCK_SILHOUETTE: 1BLOCK_SILHOUETTE, name: stringname: 'Silhouette', color: stringcolor: 'text', note: stringnote: 'Stealth' },
{ block: numberblock: const BLOCK_TEAM_RED: 4BLOCK_TEAM_RED, name: stringname: 'Team Red', color: stringcolor: 'warm', note: stringnote: 'Your team' },
{ block: numberblock: const BLOCK_TEAM_BLUE: 5BLOCK_TEAM_BLUE, name: stringname: 'Team Blue', color: stringcolor: 'info', note: stringnote: 'Enemy team' },
{ block: numberblock: const BLOCK_TEAM_GREEN: 6BLOCK_TEAM_GREEN, name: stringname: 'Team Green', color: stringcolor: 'accent', note: stringnote: 'Ally team' },
{ block: numberblock: const BLOCK_FROZEN: 7BLOCK_FROZEN, name: stringname: 'Frozen', color: stringcolor: 'info', note: stringnote: 'Slowed down' },
];
const const ANIMATED_CELLS: {}ANIMATED_CELLS = [
{ block: numberblock: const BLOCK_DAMAGE_FLASH: 8BLOCK_DAMAGE_FLASH, name: stringname: 'Damage', color: stringcolor: 'warm', note: stringnote: 'Hit flash' },
{ block: numberblock: const BLOCK_GHOST: 9BLOCK_GHOST, name: stringname: 'Ghost', color: stringcolor: 'info', note: stringnote: 'Spirit form' },
{ block: numberblock: const BLOCK_INVINCIBLE: 10BLOCK_INVINCIBLE, name: stringname: 'Invincible', color: stringcolor: 'text', note: stringnote: 'Power-up' },
{ block: numberblock: const BLOCK_POISON: 11BLOCK_POISON, name: stringname: 'Poisoned', color: stringcolor: 'accent', note: stringnote: 'Damage over time' },
];
// How large the canvas may appear in the browser (3x logical, crisp CSS upscale).
const const MAX_CANVAS_W: numberMAX_CANVAS_W = const DISPLAY_W: 640DISPLAY_W * 3;
const const MAX_CANVAS_H: numberMAX_CANVAS_H = const DISPLAY_H: 400DISPLAY_H * 3;
// The display-tier CRT runs on the upscaled RGBA buffer (3x logical, like the Basics
// Enhanced demo).
const const OUTPUT_W: numberOUTPUT_W = const DISPLAY_W: 640DISPLAY_W * 3;
const const OUTPUT_H: numberOUTPUT_H = const DISPLAY_H: 400DISPLAY_H * 3;
// Analog-TV fault bursts (ticks at the default 60 ticks per second).
const const GLITCH_COOLDOWN_MIN: 150GLITCH_COOLDOWN_MIN = 150;
const const GLITCH_COOLDOWN_MAX: 420GLITCH_COOLDOWN_MAX = 420;
const const GLITCH_ACTIVE_MIN: 4GLITCH_ACTIVE_MIN = 4;
const const GLITCH_ACTIVE_MAX: 24GLITCH_ACTIVE_MAX = 24;
const const GLITCH_INTENSITY_MIN: 0.3GLITCH_INTENSITY_MIN = 0.3;
const const GLITCH_INTENSITY_MAX: 0.95GLITCH_INTENSITY_MAX = 0.95;
// The shared fallback note is one long sentence. split('. ') cuts the string at the
// sentence break, giving an array of two shorter lines the UI kit draws one under the
// other when the software renderer is active (same pattern as the Basics Enhanced demo).
const const FALLBACK_LINES: anyFALLBACK_LINES = import SOFTWARE_FALLBACK_NOTESOFTWARE_FALLBACK_NOTE.split('. ');
// Resting values for the fault-driven display effects.
const const FLICKER_BASE: 1FLICKER_BASE = 1.0;
const const FLICKER_DIP: 0.78FLICKER_DIP = 0.78;
const const NOISE_BASE: 0.038NOISE_BASE = 0.038;
// Always-on bright band scrolling top to bottom (RollLine).
const const ROLL_BASE: 0.26ROLL_BASE = 0.26;
const const ROLL_SPEED: 0.92ROLL_SPEED = 0.92;
const const INTERFERENCE_BASE: 0INTERFERENCE_BASE = 0;
// Occasional subtle horizontal band offset (PixelGlitch), separate from the TV fault
// bursts.
const const BAND_WOBBLE_COOLDOWN_MIN: 100BAND_WOBBLE_COOLDOWN_MIN = 100;
const const BAND_WOBBLE_COOLDOWN_MAX: 280BAND_WOBBLE_COOLDOWN_MAX = 280;
const const BAND_WOBBLE_ACTIVE_MIN: 3BAND_WOBBLE_ACTIVE_MIN = 3;
const const BAND_WOBBLE_ACTIVE_MAX: 10BAND_WOBBLE_ACTIVE_MAX = 10;
const const BAND_WOBBLE_INTENSITY: 0.11BAND_WOBBLE_INTENSITY = 0.11;
// Spins the CPU until roughly `ms` milliseconds have passed. The timing chart maps bar
// height from real update()/render() time; this demo adds a gentle pulse so the scrolling
// bars are easy to see while you watch the effects.
function function burnCpuMs(ms: any): voidburnCpuMs(ms: anyms) {
if (ms: anyms <= 0) {
return;
}
// performance.now() returns a high-resolution clock in milliseconds.
const const deadline: anydeadline = performance.now() + ms: anyms;
while (performance.now() < const deadline: anydeadline) {
// Empty loop on purpose: waiting for the clock, not doing useful work.
}
}
// Palette-offset sprite effects under a Tesla Orava CRT stack. Static effects are
// pre-built in init(); dynamic effects are rewritten in update().
class class DemoDemo {
// The palette holds all colors for this demo.
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = null;
// The sprite sheet loaded from /sprites/test.png.
/** @type {SpriteSheet | null} */
Demo.sheet: SpriteSheet | nullsheet = null;
// The source rectangle covering the whole test sprite.
/** @type {Rect2i | null} */
Demo.charRect: Rect2i | nullcharRect = null;
// How many unique colors the sprite has - every theme block is this many palette
// slots wide. Set in init() after the colors are registered.
Demo.colorCount: numbercolorCount = 0;
// Slot map for the shared UI kit theme, filled in init() by applyTheme(). theme.bg,
// theme.panel, and friends are palette indices for this demo's own drawing.
Demo.theme: nulltheme = null;
// The sprite's original Color32 objects, darkest first (used to build theme blocks).
Demo.baseColors: {}baseColors = [];
// Seconds elapsed; drives the ghost, invincibility, and poison animations.
Demo.animTime: numberanimTime = 0;
// The tick of the last "damage event" (for the damage flash).
Demo.damageFlashTick: numberdamageFlashTick = 0;
// Reused for every sprite position in render(), so drawing allocates no new
// Vector2i.
Demo.drawPos: Vector2idrawPos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(0, 0);
/** @type {PixelGlitch | null} */
Demo.pixelGlitch: PixelGlitch | nullpixelGlitch = null;
/** @type {BarrelDistortion | null} */
Demo.barrel: BarrelDistortion | nullbarrel = 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;
// True when the WebGPU backend is active and the CRT stack was built.
Demo.effectsAvailable: booleaneffectsAvailable = false;
// Ticks until the next TV fault burst may start.
Demo.glitchCooldown: numberglitchCooldown = 0;
// How many ticks the current TV fault burst still has to run (0 = no burst active).
Demo.glitchTicksLeft: numberglitchTicksLeft = 0;
// Total length of the current burst in ticks, for the sine envelope.
Demo.glitchDuration: numberglitchDuration = 0;
// Which fault is running: a GLITCH_TYPES_VROLL key, or 'none'.
Demo.glitchType: stringglitchType = 'none';
// Peak strength of the current burst, 0 to 1.
Demo.glitchPeak: numberglitchPeak = 0;
// Ticks until the next band wobble may start.
Demo.bandWobbleCooldown: numberbandWobbleCooldown = 0;
// How many ticks the current band wobble still has to run (0 = no wobble active).
Demo.bandWobbleTicksLeft: numberbandWobbleTicksLeft = 0;
// Total length of the current wobble in ticks, for the sine envelope.
Demo.bandWobbleDuration: numberbandWobbleDuration = 0;
// PixelGlitch seed for the current wobble, so its bands stay put for its duration.
Demo.bandWobbleSeed: numberbandWobbleSeed = 0;
// Reused every frame for the two overlay rows, so no new array or object is
// allocated. overlayRows() rewrites both leftText values.
Demo.overlayRowData: {}overlayRowData = [
{ leftText: stringleftText: 'Tesla Orava CRT: OFF', textPaletteIndex: numbertextPaletteIndex: const UI_TEXT: 244UI_TEXT },
{ leftText: stringleftText: 'TV fault: NONE', textPaletteIndex: numbertextPaletteIndex: const UI_HEADER: 246UI_HEADER },
];
// A wider logical screen for the sprite grid; the display-tier Tesla Orava stack runs
// at a 3x upscale. The overlay starts visible with the palette grid, a tall timing
// chart, and the renderer diagnostics bar.
Demo.configure(): {
displaySize: Vector2i;
drawingBufferSize: Vector2i;
maxCanvasSize: Vector2i;
outputUpscaleFilter: string;
isOverlayPaletteEnabled: boolean;
isOverlayVisibleAtStart: boolean;
isOverlayTimingChartEnabled: boolean;
overlayTimingChartHeight: number;
overlayTimingChartDiagnostics: string;
isOverlayRendererDiagnosticsBarEnabled: 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(const DISPLAY_W: 640DISPLAY_W, const DISPLAY_H: 400DISPLAY_H),
drawingBufferSize: Vector2idrawingBufferSize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const OUTPUT_W: numberOUTPUT_W, const OUTPUT_H: numberOUTPUT_H),
maxCanvasSize: Vector2imaxCanvasSize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const MAX_CANVAS_W: numberMAX_CANVAS_W, const MAX_CANVAS_H: numberMAX_CANVAS_H),
outputUpscaleFilter: stringoutputUpscaleFilter: 'nearest',
isOverlayPaletteEnabled: booleanisOverlayPaletteEnabled: true,
isOverlayVisibleAtStart: booleanisOverlayVisibleAtStart: true,
// Opt in to the engine timing chart band under the title row.
// overlayTimingChartHeight sets the band height in pixels (default 22).
isOverlayTimingChartEnabled: booleanisOverlayTimingChartEnabled: true,
overlayTimingChartHeight: numberoverlayTimingChartHeight: 64,
overlayTimingChartDiagnostics: stringoverlayTimingChartDiagnostics: 'rich',
isOverlayRendererDiagnosticsBarEnabled: booleanisOverlayRendererDiagnosticsBarEnabled: true,
overlayStyle: {
barPaletteIndex: number;
textPaletteIndex: number;
gapPaletteIndex: number;
}
overlayStyle: {
barPaletteIndex: numberbarPaletteIndex: const UI_BG: 240UI_BG,
textPaletteIndex: numbertextPaletteIndex: const UI_DIM: 245UI_DIM,
gapPaletteIndex: numbergapPaletteIndex: const UI_GAP: 252UI_GAP,
},
overlayTimingChartStyle: {
updateBarPaletteIndex: number;
renderBarPaletteIndex: number;
warningPaletteIndex: number;
errorPaletteIndex: number;
tagPaletteIndex: number;
}
overlayTimingChartStyle: {
updateBarPaletteIndex: numberupdateBarPaletteIndex: const UI_ACCENT: 247UI_ACCENT,
renderBarPaletteIndex: numberrenderBarPaletteIndex: const UI_HEADER: 246UI_HEADER,
warningPaletteIndex: numberwarningPaletteIndex: const UI_HEADER: 246UI_HEADER,
errorPaletteIndex: numbererrorPaletteIndex: const UI_WARM: 248UI_WARM,
tagPaletteIndex: numbertagPaletteIndex: const UI_INFO: 249UI_INFO,
},
};
}
// Sets up the palette, builds all 13 theme blocks, loads the sprite, and builds the
// CRT stack when WebGPU is active. Returns false when the sprite fails to load.
async Demo.init(): unknowninit() {
console.log('[SpriteEffectsDemo] Initializing...');
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);
// Install the shared UI theme: applyTheme() writes the twelve UI kit colors into
// high palette slots (240-251), far above the sprite theme blocks (slots 12-89
// with the 6-color test sprite), and returns a map of friendly names to those
// slots (this.theme.bg, .panel, ...). All captions and legends draw with these.
this.Demo.theme: nulltheme = import applyThemeapplyTheme(this.Demo.palette: Palettepalette);
// The overlay gap color lives outside the theme's own range, so it is set
// directly.
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const UI_GAP: 252UI_GAP, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(60, 60, 70));
// Register the sprite colors: scan the PNG and write every unique opaque color
// into the palette starting at COLOR_BASE. The returned array is the same colors
// in palette-write order (sorted darkest-first by luminance). The theme-block
// builders below recolor each base color and write the result into a higher
// block.
this.Demo.baseColors: {}baseColors = 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('/sprites/test.png', this.Demo.palette: Palettepalette, const COLOR_BASE: 12COLOR_BASE);
const const colorCount: anycolorCount = this.Demo.baseColors: {}baseColors.length;
this.Demo.colorCount: numbercolorCount = const colorCount: anycolorCount;
// Build the 8 static theme blocks. Each block sits at COLOR_BASE + blockIndex *
// colorCount.
this.Demo.buildStaticThemeBlocks(): voidbuildStaticThemeBlocks();
// Dynamic blocks (8-12) start as copies of the original. update() replaces them
// each tick. palette.fillBlock(start, source, transform) writes
// transform(baseColor) into one slot per base color, starting at `start`.
for (let let block: numberblock = const BLOCK_DAMAGE_FLASH: 8BLOCK_DAMAGE_FLASH; let block: numberblock <= const BLOCK_DAYNIGHT: 12BLOCK_DAYNIGHT; let block: numberblock++) {
this.Demo.palette: Palettepalette.Palette.fillBlock(start: number, source: readonly Color32[], transform: (color: Color32, index: number) => Color32): numberWrites a transformed block of colors into contiguous palette slots.
Writes `transform(source[i], i)` into slot `start + i` for every `i` in `[0, source.length)`, delegating
each write to
{@link
set
}
so it inherits
{@link
set
}
's validation, including the rule that slot 0 must
stay transparent. Collapses the common pattern of looping
`palette.set(start + i, transform(baseColors[i]))` into one call.fillBlock(
const COLOR_BASE: 12COLOR_BASE + let block: numberblock * const colorCount: anycolorCount,
this.Demo.baseColors: {}baseColors,
(base: Color32base) => new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(base: Color32base.Color32.r: numberRed channel (0-255).r, base: Color32base.Color32.g: numberGreen channel (0-255).g, base: Color32base.Color32.b: numberBlue channel (0-255).b, base: Color32base.Color32.a: numberAlpha channel (0-255).a),
);
}
// Load and indexize the sprite. loadIndexed() registers the colors again at
// COLOR_BASE, in image scan order this time (sort: 'none'), overwriting block 0,
// then converts the sheet's pixels to palette indices against that block.
try {
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('/sprites/test.png', this.Demo.palette: Palettepalette, const COLOR_BASE: 12COLOR_BASE, {
sort?: "none" | "luminance" | undefinedsort: 'none',
});
this.Demo.sheet: SpriteSheet | nullsheet = const indexed: Promise<IndexedSpriteLoadResult>indexed.sheet;
this.Demo.charRect: Rect2i | nullcharRect = this.Demo.sheet: SpriteSheet | nullsheet.SpriteSheet.fullRect(): Rect2iReturns a source rectangle that covers the entire sprite sheet.fullRect();
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);
console.log(`[SpriteEffectsDemo] Loaded sprite: ${this.Demo.charRect: Rect2icharRect.Rect2i.width: numberWidth in pixels (defaults to 0).width}x${this.Demo.charRect: Rect2icharRect.Rect2i.height: numberHeight in pixels (defaults to 0).height}px`);
} catch (function (local var) error: unknownerror) {
console.error('[SpriteEffectsDemo] Failed to load sprite:', function (local var) error: unknownerror);
return false;
}
// Fault state is shared by both backends - initialize it once before the CRT
// check. BT.random.int(min, max) returns a whole number in [min, 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(const GLITCH_COOLDOWN_MIN: 150GLITCH_COOLDOWN_MIN, const GLITCH_COOLDOWN_MAX: 420GLITCH_COOLDOWN_MAX);
this.Demo.glitchTicksLeft: numberglitchTicksLeft = 0;
this.Demo.glitchDuration: numberglitchDuration = 0;
this.Demo.glitchType: stringglitchType = 'none';
this.Demo.glitchPeak: numberglitchPeak = 0;
this.Demo.effectsAvailable: booleaneffectsAvailable = import isAvailableisAvailable();
if (!this.Demo.effectsAvailable: booleaneffectsAvailable) {
console.log('[SpriteEffectsDemo] Initialization complete (no CRT stack).');
return true;
}
// Build the full Tesla Orava effect chain (see setupCrtStack() below).
this.Demo.setupCrtStack(): voidsetupCrtStack();
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);
this.Demo.bandWobbleTicksLeft: numberbandWobbleTicksLeft = 0;
this.Demo.bandWobbleDuration: numberbandWobbleDuration = 0;
console.log('[SpriteEffectsDemo] Initialization complete!');
return true;
}
// Advances animTime and rewrites all five dynamic theme blocks, then steps the CRT
// fault machine when the stack is active. The damage flash alternates the
// damage-white and damage-red recipes every 3 ticks for 30 ticks after each damage
// event; ghost pulses alpha between 40 and 180; invincibility rotates hue around the
// color wheel; poison pulses brightness; day/night cycles brightness over 20 seconds.
Demo.update(): voidupdate() {
this.Demo.animTime: numberanimTime += 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.deltaSeconds: numberFixed-step seconds per update tick.
Equivalent to `1 / BT.targetFPS` when `BT.targetFPS` is finite and positive.
Falls back to `1 / 60` when target FPS is non-finite or non-positive.deltaSeconds;
if (!this.Demo.colorCount: numbercolorCount) {
return;
}
// Trigger a damage event every 3 seconds (180 ticks).
if (const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.ticks: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks % 180 === 0) {
this.Demo.damageFlashTick: numberdamageFlashTick = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.ticks: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks;
}
this.Demo.updateDamageFlashBlock(): voidupdateDamageFlashBlock();
this.Demo.updateGhostBlock(): voidupdateGhostBlock();
this.Demo.updateInvincibleBlock(): voidupdateInvincibleBlock();
this.Demo.updatePoisonBlock(): voidupdatePoisonBlock();
this.Demo.updateDayNightBlock(): voidupdateDayNightBlock();
if (this.Demo.effectsAvailable: booleaneffectsAvailable) {
this.Demo.updateCrtEffects(): voidupdateCrtEffects();
}
// Extra update() work so the overlay timing chart shows green scrolling bars.
// Math.sin swings between -1 and 1; * 0.5 + 0.5 remaps that to 0-1 for a smooth
// pulse of 0.5-2.5 ms. The combined update+render busy-wait stays well under a
// 60 FPS frame (~16 ms) so low-power devices stay responsive.
const const chartUpdateLoadMs: numberchartUpdateLoadMs = 0.5 + (Math.sin(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 * 1.5) * 0.5 + 0.5) * 2;
function burnCpuMs(ms: any): voidburnCpuMs(const chartUpdateLoadMs: numberchartUpdateLoadMs);
}
// Runs once per screen refresh and draws all the sprite effect demonstrations. No
// Color32 objects appear in draw calls - only palette indices and offsets.
Demo.render(): voidrender() {
// Clear the whole screen with the shared UI theme's background color.
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(this.Demo.theme: nulltheme.bg);
// Extra render() work so the timing chart shows amber scrolling bars (0.3-1.8
// ms).
const const chartRenderLoadMs: numberchartRenderLoadMs = 0.3 + (Math.cos(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 * 2.2) * 0.5 + 0.5) * 1.5;
function burnCpuMs(ms: any): voidburnCpuMs(const chartRenderLoadMs: numberchartRenderLoadMs);
// Draw the Static and Animated panels.
this.Demo.renderStaticEffects(): voidrenderStaticEffects();
this.Demo.renderDynamicEffects(): voidrenderDynamicEffects();
// The Day/night panel, right of the Animated one.
this.Demo.renderDayNightCycle(): voidrenderDayNightCycle();
// Software renderer: warn on-canvas that the Tesla Orava look is missing. A small
// borderless kit group in the top-left corner; the shared note was split into two
// lines up top (FALLBACK_LINES) so it stays short.
if (!this.Demo.effectsAvailable: booleaneffectsAvailable) {
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { margin: numbermargin: 2, pad: numberpad: 2 });
for (const const line: anyline of const FALLBACK_LINES: anyFALLBACK_LINES) {
import uiui.label(const line: anyline, { color: stringcolor: 'warm' });
}
import uiui.end();
}
}
// Two custom overlay rows: the Tesla Orava stack state and the current analog-TV
// fault with its strength in percent. Returns the reused overlayRowData array.
Demo.overlayRows(): {}overlayRows() {
if (this.Demo.effectsAvailable: booleaneffectsAvailable) {
this.Demo.overlayRowData: {}overlayRowData[0].leftText = 'Tesla Orava: ON';
const const faultLabel: anyfaultLabel = import GLITCH_LABELSGLITCH_LABELS[this.Demo.glitchType: stringglitchType] ?? 'NONE';
const const faultValue: anyfaultValue = this.Demo.glitchTicksLeft: numberglitchTicksLeft > 0 ? Math.round(this.Demo.glitchPeak: numberglitchPeak * 100) : 0;
this.Demo.overlayRowData: {}overlayRowData[1].leftText = `TV fault: ${const faultLabel: anyfaultLabel} ${String(const faultValue: anyfaultValue).padStart(2, '0')}%`;
} else {
// Software renderer: no CRT stack, so the fault machine never fires. The full
// explanation lives on the canvas itself (see render()), not in the overlay.
this.Demo.overlayRowData: {}overlayRowData[0].leftText = 'Tesla Orava: CRT OFF (software)';
this.Demo.overlayRowData: {}overlayRowData[1].leftText = 'TV fault: NONE';
}
return this.Demo.overlayRowData: {}overlayRowData;
}
// Builds the Tesla Orava effect chain once in init(): the pixel-tier band tear plus
// the display-tier tube look, in back-to-front order.
Demo.setupCrtStack(): voidsetupCrtStack() {
// Pixel tier: indexed-buffer horizontal band tear (V-hold style).
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 = 4;
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: Tesla Orava B/W tube - curved glass, faint grille, soft halation.
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.07;
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;
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;
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.42;
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 = -7;
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: 400DISPLAY_H;
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.07;
this.Demo.mask: RGBMaskmask.RGBMask.size: numberMask cell pitch in output (display-chain) pixels. Smaller = denser mask.size = 5;
this.Demo.mask: RGBMaskmask.RGBMask.border: numberBorder darkening within each mask cell. 0 disables, 1 strong.border = 0.45;
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;
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;
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;
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;
for (const const fx: anyfx of [
this.Demo.barrel: BarrelDistortionbarrel,
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);
}
}
// Drives the RollLine scroll, the TV fault bursts, and the occasional subtle band
// offset wobble. A fault burst takes priority: while one runs, the wobble is skipped.
Demo.updateCrtEffects(): voidupdateCrtEffects() {
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;
if (this.Demo.glitchTicksLeft: numberglitchTicksLeft > 0) {
// t runs 0 to 1 over the burst; sin(t * PI) is a 0 -> 1 -> 0 envelope.
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) {
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;
}
this.Demo.applyRestingCrtUniforms(): voidapplyRestingCrtUniforms();
if (this.Demo.bandWobbleTicksLeft: numberbandWobbleTicksLeft > 0) {
// Same sine envelope as the fault bursts, scaled to the wobble's intensity.
const const t: numbert = 1 - (this.Demo.bandWobbleTicksLeft: numberbandWobbleTicksLeft - 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.bandWobbleTicksLeft: numberbandWobbleTicksLeft--;
if (this.Demo.bandWobbleTicksLeft: numberbandWobbleTicksLeft <= 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) {
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.bandWobbleTicksLeft: numberbandWobbleTicksLeft = 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);
}
}
this.Demo.glitchCooldown: numberglitchCooldown--;
if (this.Demo.glitchCooldown: numberglitchCooldown <= 0) {
// Start a burst: BT.random.pick() chooses one fault type from the list,
// float() picks its peak strength and a fresh band seed.
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);
}
}
// Resting Tesla Orava look: scrolling roll band plus calm noise and flicker.
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.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 the active fault onto the resting uniforms. `envelope` runs 0 -> 1 -> 0 over
// the burst, so every fault ramps in and out instead of snapping.
Demo.applyGlitchUniforms(envelope: any): voidapplyGlitchUniforms(envelope: anyenvelope) {
const const peak: numberpeak = this.Demo.glitchPeak: numberglitchPeak * envelope: anyenvelope;
if (this.Demo.glitchType: stringglitchType === 'hshift') {
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;
} else if (this.Demo.glitchType: stringglitchType === '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') {
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) * envelope: anyenvelope;
} else if (this.Demo.glitchType: stringglitchType === 'interference') {
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') {
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;
}
}
// Builds the 7 static recolored blocks (1-7) from the base colors. Called once in
// init() - these never change after setup.
Demo.buildStaticThemeBlocks(): voidbuildStaticThemeBlocks() {
// Block 0 (original) holds the sprite's own colors, written by the loaders in
// init(). n is how many unique colors the sprite has - every block is n slots
// wide.
const const n: numbern = this.Demo.colorCount: numbercolorCount;
// Each block below is a palette.fillBlock(start, source, transform) call: it
// walks this.baseColors once and writes transform(baseColor) into one slot per
// color, starting at `start`. That replaces a hand-written for loop over the same
// colors.
// Block 1: Silhouette - near-black with slight variation to preserve depth cues.
// Channels are floored like blocks 4-7 so every recipe hands Color32 whole bytes.
this.Demo.palette: Palette | nullpalette.Palette.fillBlock(start: number, source: readonly Color32[], transform: (color: Color32, index: number) => Color32): numberWrites a transformed block of colors into contiguous palette slots.
Writes `transform(source[i], i)` into slot `start + i` for every `i` in `[0, source.length)`, delegating
each write to
{@link
set
}
so it inherits
{@link
set
}
's validation, including the rule that slot 0 must
stay transparent. Collapses the common pattern of looping
`palette.set(start + i, transform(baseColors[i]))` into one call.fillBlock(const COLOR_BASE: 12COLOR_BASE + const BLOCK_SILHOUETTE: 1BLOCK_SILHOUETTE * const n: numbern, this.Demo.baseColors: {}baseColors, (base: Color32base) => {
// The perceived brightness of this color (0-255).
const const lum: anylum = Math.floor(base: Color32base.Color32.luminance: numberPerceived (Rec. 601) luminance of this color, ignoring alpha.luminance);
return new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(Math.floor(const lum: anylum * 0.08), Math.floor(const lum: anylum * 0.08), Math.floor(const lum: anylum * 0.1), base: Color32base.Color32.a: numberAlpha channel (0-255).a);
});
// Block 2: Damage white - everything shifted toward bright white.
this.Demo.palette: Palette | nullpalette.Palette.fillBlock(start: number, source: readonly Color32[], transform: (color: Color32, index: number) => Color32): numberWrites a transformed block of colors into contiguous palette slots.
Writes `transform(source[i], i)` into slot `start + i` for every `i` in `[0, source.length)`, delegating
each write to
{@link
set
}
so it inherits
{@link
set
}
's validation, including the rule that slot 0 must
stay transparent. Collapses the common pattern of looping
`palette.set(start + i, transform(baseColors[i]))` into one call.fillBlock(const COLOR_BASE: 12COLOR_BASE + const BLOCK_DAMAGE_WHITE: 2BLOCK_DAMAGE_WHITE * const n: numbern, this.Demo.baseColors: {}baseColors, (base: Color32base) => {
const const lum: anylum = Math.floor(base: Color32base.Color32.luminance: numberPerceived (Rec. 601) luminance of this color, ignoring alpha.luminance);
const const whitened: anywhitened = Math.floor(128 + const lum: anylum * 0.5);
return new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(const whitened: anywhitened, const whitened: anywhitened, const whitened: anywhitened, base: Color32base.Color32.a: numberAlpha channel (0-255).a);
});
// Block 3: Damage red - everything shifted toward red.
this.Demo.palette: Palette | nullpalette.Palette.fillBlock(start: number, source: readonly Color32[], transform: (color: Color32, index: number) => Color32): numberWrites a transformed block of colors into contiguous palette slots.
Writes `transform(source[i], i)` into slot `start + i` for every `i` in `[0, source.length)`, delegating
each write to
{@link
set
}
so it inherits
{@link
set
}
's validation, including the rule that slot 0 must
stay transparent. Collapses the common pattern of looping
`palette.set(start + i, transform(baseColors[i]))` into one call.fillBlock(const COLOR_BASE: 12COLOR_BASE + const BLOCK_DAMAGE_RED: 3BLOCK_DAMAGE_RED * const n: numbern, this.Demo.baseColors: {}baseColors, (base: Color32base) => {
const const lum: anylum = Math.floor(base: Color32base.Color32.luminance: numberPerceived (Rec. 601) luminance of this color, ignoring alpha.luminance);
return new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(Math.min(255, const lum: anylum + 80), Math.floor(const lum: anylum * 0.3), Math.floor(const lum: anylum * 0.3), base: Color32base.Color32.a: numberAlpha channel (0-255).a);
});
// Block 4: Team red - multiply the base colors with a red tint.
this.Demo.palette: Palette | nullpalette.Palette.fillBlock(start: number, source: readonly Color32[], transform: (color: Color32, index: number) => Color32): numberWrites a transformed block of colors into contiguous palette slots.
Writes `transform(source[i], i)` into slot `start + i` for every `i` in `[0, source.length)`, delegating
each write to
{@link
set
}
so it inherits
{@link
set
}
's validation, including the rule that slot 0 must
stay transparent. Collapses the common pattern of looping
`palette.set(start + i, transform(baseColors[i]))` into one call.fillBlock(
const COLOR_BASE: 12COLOR_BASE + const BLOCK_TEAM_RED: 4BLOCK_TEAM_RED * const n: numbern,
this.Demo.baseColors: {}baseColors,
(base: Color32base) =>
new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(
Math.min(255, Math.floor(base: Color32base.Color32.r: numberRed channel (0-255).r * 1.4)),
Math.floor(base: Color32base.Color32.g: numberGreen channel (0-255).g * 0.5),
Math.floor(base: Color32base.Color32.b: numberBlue channel (0-255).b * 0.5),
base: Color32base.Color32.a: numberAlpha channel (0-255).a,
),
);
// Block 5: Team blue - multiply with a blue tint.
this.Demo.palette: Palette | nullpalette.Palette.fillBlock(start: number, source: readonly Color32[], transform: (color: Color32, index: number) => Color32): numberWrites a transformed block of colors into contiguous palette slots.
Writes `transform(source[i], i)` into slot `start + i` for every `i` in `[0, source.length)`, delegating
each write to
{@link
set
}
so it inherits
{@link
set
}
's validation, including the rule that slot 0 must
stay transparent. Collapses the common pattern of looping
`palette.set(start + i, transform(baseColors[i]))` into one call.fillBlock(
const COLOR_BASE: 12COLOR_BASE + const BLOCK_TEAM_BLUE: 5BLOCK_TEAM_BLUE * const n: numbern,
this.Demo.baseColors: {}baseColors,
(base: Color32base) =>
new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(
Math.floor(base: Color32base.Color32.r: numberRed channel (0-255).r * 0.5),
Math.floor(base: Color32base.Color32.g: numberGreen channel (0-255).g * 0.7),
Math.min(255, Math.floor(base: Color32base.Color32.b: numberBlue channel (0-255).b * 1.6)),
base: Color32base.Color32.a: numberAlpha channel (0-255).a,
),
);
// Block 6: Team green - multiply with a green tint.
this.Demo.palette: Palette | nullpalette.Palette.fillBlock(start: number, source: readonly Color32[], transform: (color: Color32, index: number) => Color32): numberWrites a transformed block of colors into contiguous palette slots.
Writes `transform(source[i], i)` into slot `start + i` for every `i` in `[0, source.length)`, delegating
each write to
{@link
set
}
so it inherits
{@link
set
}
's validation, including the rule that slot 0 must
stay transparent. Collapses the common pattern of looping
`palette.set(start + i, transform(baseColors[i]))` into one call.fillBlock(
const COLOR_BASE: 12COLOR_BASE + const BLOCK_TEAM_GREEN: 6BLOCK_TEAM_GREEN * const n: numbern,
this.Demo.baseColors: {}baseColors,
(base: Color32base) =>
new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(
Math.floor(base: Color32base.Color32.r: numberRed channel (0-255).r * 0.5),
Math.min(255, Math.floor(base: Color32base.Color32.g: numberGreen channel (0-255).g * 1.4)),
Math.floor(base: Color32base.Color32.b: numberBlue channel (0-255).b * 0.5),
base: Color32base.Color32.a: numberAlpha channel (0-255).a,
),
);
// Block 7: Frozen - push toward a cold blue-white.
this.Demo.palette: Palette | nullpalette.Palette.fillBlock(start: number, source: readonly Color32[], transform: (color: Color32, index: number) => Color32): numberWrites a transformed block of colors into contiguous palette slots.
Writes `transform(source[i], i)` into slot `start + i` for every `i` in `[0, source.length)`, delegating
each write to
{@link
set
}
so it inherits
{@link
set
}
's validation, including the rule that slot 0 must
stay transparent. Collapses the common pattern of looping
`palette.set(start + i, transform(baseColors[i]))` into one call.fillBlock(const COLOR_BASE: 12COLOR_BASE + const BLOCK_FROZEN: 7BLOCK_FROZEN * const n: numbern, this.Demo.baseColors: {}baseColors, (base: Color32base) => {
const const lum: anylum = Math.floor(base: Color32base.Color32.luminance: numberPerceived (Rec. 601) luminance of this color, ignoring alpha.luminance);
return new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(Math.floor(const lum: anylum * 0.7 + 40), Math.floor(const lum: anylum * 0.8 + 40), Math.min(255, const lum: anylum + 80), base: Color32base.Color32.a: numberAlpha channel (0-255).a);
});
}
// The 5 update*Block() methods below run every tick (called from update()), unlike
// buildStaticThemeBlocks() which runs once in init(). They stay as plain for loops
// instead of palette.fillBlock(start, source, transform) calls on purpose: fillBlock
// takes a transform function, and writing `(base) => ...` inline at each call site
// would create a new closure 60 times a second. A raw loop allocates nothing extra
// per tick beyond the Color32 it writes.
// Damage flash: alternates between the damage-white and damage-red recipes every 3
// ticks for the first 30 ticks after a damage event, then returns to the original
// colors.
Demo.updateDamageFlashBlock(): voidupdateDamageFlashBlock() {
const const flashAge: numberflashAge = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.ticks: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks - this.Demo.damageFlashTick: numberdamageFlashTick;
const const n: numbern = this.Demo.colorCount: numbercolorCount;
for (let let i: numberi = 0; let i: numberi < const n: numbern; let i: numberi++) {
const const base: anybase = this.Demo.baseColors: {}baseColors[let i: numberi];
const const lum: anylum = Math.floor(const base: anybase.luminance);
let let color: anycolor;
if (const flashAge: numberflashAge < 30) {
// Math.floor(flashAge / 3) % 2 alternates 0 and 1 every 3 ticks: 0 =
// white, 1 = red.
const const phase: numberphase = Math.floor(const flashAge: numberflashAge / 3) % 2;
const const whitened: anywhitened = Math.floor(128 + const lum: anylum * 0.5);
const const redShift: anyredShift = Math.min(255, const lum: anylum + 80);
let color: anycolor =
const phase: numberphase === 0
? new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(const whitened: anywhitened, const whitened: anywhitened, const whitened: anywhitened, const base: anybase.a)
: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(const redShift: anyredShift, Math.floor(const lum: anylum * 0.3), Math.floor(const lum: anylum * 0.3), const base: anybase.a);
} else {
// Outside the flash window: the original color.
let color: anycolor = new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(const base: anybase.r, const base: anybase.g, const base: anybase.b, const base: anybase.a);
}
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const COLOR_BASE: 12COLOR_BASE + const BLOCK_DAMAGE_FLASH: 8BLOCK_DAMAGE_FLASH * const n: numbern + let i: numberi, let color: Color32color);
}
}
// Ghost: the sprite appears semi-transparent with a blue-white tint. Alpha pulses
// between 40 and 180 on a sine wave, three radians per second.
Demo.updateGhostBlock(): voidupdateGhostBlock() {
// Math.sin oscillates between -1 and 1; * 0.5 + 0.5 shifts it to 0-1.
const const pulse: numberpulse = Math.sin(this.Demo.animTime: numberanimTime * 3) * 0.5 + 0.5;
const const alpha: anyalpha = Math.floor(40 + const pulse: numberpulse * 140); // 40-180
const const n: numbern = this.Demo.colorCount: numbercolorCount;
for (let let i: numberi = 0; let i: numberi < const n: numbern; let i: numberi++) {
const const base: anybase = this.Demo.baseColors: {}baseColors[let i: numberi];
const const lum: anylum = Math.floor(const base: anybase.luminance);
// Push toward a cool blue-white while reducing alpha.
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(
const COLOR_BASE: 12COLOR_BASE + const BLOCK_GHOST: 9BLOCK_GHOST * const n: numbern + let i: numberi,
new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(Math.floor(const lum: anylum * 0.8 + 40), Math.floor(const lum: anylum * 0.8 + 40), Math.min(255, const lum: anylum + 60), const alpha: anyalpha),
);
}
}
// Invincibility: cycles the entire sprite through the rainbow. The hue rotates 200
// degrees per second.
Demo.updateInvincibleBlock(): voidupdateInvincibleBlock() {
const const hue: numberhue = (this.Demo.animTime: numberanimTime * 200) % 360;
const const n: numbern = this.Demo.colorCount: numbercolorCount;
for (let let i: numberi = 0; let i: numberi < const n: numbern; let i: numberi++) {
const const base: anybase = this.Demo.baseColors: {}baseColors[let i: numberi];
// Color32.fromHSL(hue, saturation, lightness) takes hue in degrees (0-360)
// and saturation and lightness as percentages (0-100). Lightness follows the
// base color's luminance so darker parts stay darker.
const const lum: anylum = const base: anybase.luminance;
const const lightness: numberlightness = 30 + (const lum: anylum / 255) * 40; // 30-70 percent
const const rainbow: Color32rainbow = class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32.Color32.fromHSL(h: number, s: number, l: number, a?: number): Color32Creates a color from HSL values.fromHSL(const hue: numberhue, 100, const lightness: numberlightness);
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(
const COLOR_BASE: 12COLOR_BASE + const BLOCK_INVINCIBLE: 10BLOCK_INVINCIBLE * const n: numbern + let i: numberi,
new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(const rainbow: Color32rainbow.Color32.r: numberRed channel (0-255).r, const rainbow: Color32rainbow.Color32.g: numberGreen channel (0-255).g, const rainbow: Color32rainbow.Color32.b: numberBlue channel (0-255).b, const base: anybase.a),
);
}
}
// Poison: a green tint whose brightness pulses, five radians per second.
Demo.updatePoisonBlock(): voidupdatePoisonBlock() {
const const pulse: numberpulse = Math.sin(this.Demo.animTime: numberanimTime * 5) * 0.2 + 0.8; // 0.6-1.0
const const n: numbern = this.Demo.colorCount: numbercolorCount;
for (let let i: numberi = 0; let i: numberi < const n: numbern; let i: numberi++) {
const const base: anybase = this.Demo.baseColors: {}baseColors[let i: numberi];
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(
const COLOR_BASE: 12COLOR_BASE + const BLOCK_POISON: 11BLOCK_POISON * const n: numbern + let i: numberi,
new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(
Math.floor(const base: anybase.r * 0.5 * const pulse: numberpulse),
Math.min(255, Math.floor(const base: anybase.g * 1.4 * const pulse: numberpulse)),
Math.floor(const base: anybase.b * 0.5 * const pulse: numberpulse),
const base: anybase.a,
),
);
}
}
// Day/night: a brightness factor that cycles over 20 seconds (1200 ticks). At midday
// the factor is 1.0; at midnight it drops to 0.3.
Demo.updateDayNightBlock(): voidupdateDayNightBlock() {
const const cycle: numbercycle = (const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.ticks: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks % 1200) / 1200; // 0-1
const const brightness: numberbrightness = (Math.cos(const cycle: numbercycle * Math.PI * 2) + 1) * 0.35 + 0.3; // 0.3-1.0
const const n: numbern = this.Demo.colorCount: numbercolorCount;
for (let let i: numberi = 0; let i: numberi < const n: numbern; let i: numberi++) {
const const base: anybase = this.Demo.baseColors: {}baseColors[let i: numberi];
// Blend from a fixed cool night tint (slight blue) toward the sprite's
// daylight color as brightness approaches 1. Color32.lerp(a, b, t): t = 0 is
// all `a`, t = 1 is all `b`.
const const nightTint: Color32nightTint = new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(0, 0, 30, const base: anybase.a);
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const COLOR_BASE: 12COLOR_BASE + const BLOCK_DAYNIGHT: 12BLOCK_DAYNIGHT * const n: numbern + let i: numberi, class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32.Color32.lerp(a: Color32, b: Color32, t: number): Color32Linearly interpolates between two colors.
Each RGBA channel is interpolated independently. `t` is clamped to [0, 1].lerp(const nightTint: Color32nightTint, const base: anybase, const brightness: numberbrightness));
}
}
// Draws one kit panel with a title and an empty body tall enough for a row of
// sprite cells. The sprites and captions are drawn after ui.end(), so they land on
// top of the panel.
Demo.renderPanel(x: any, y: any, width: any, title: any): voidrenderPanel(x: anyx, y: anyy, width: anywidth, title: anytitle) {
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { x: anyx, y: anyy, width: anywidth });
import uiui.panel(title: anytitle);
import uiui.spacer(const PANEL_SPACER: numberPANEL_SPACER);
import uiui.end();
}
// Draws one effect cell: the sprite drawn with the cell's palette offset, then its
// name and note centered under it. Every character of the system font is FONT_W (6)
// pixels wide, so a caption is text.length * FONT_W pixels wide, and starting it half
// that width left of the middle centers it.
Demo.renderCell(cellX: any, panelY: any, cell: any): voidrenderCell(cellX: anycellX, panelY: anypanelY, cell: anycell) {
const const centerX: anycenterX = cellX: anycellX + const CELL_W: 104CELL_W / 2;
// The palette offset picks the theme block: block k starts k * colorCount slots
// above block 0, so every pixel of the sprite lands in that block's colors.
this.Demo.drawPos: Vector2idrawPos.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(const centerX: anycenterX - const ART_CENTER_X: 21ART_CENTER_X, panelY: anypanelY + const SPRITE_TOP: 14SPRITE_TOP);
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.sheet: SpriteSheet | nullsheet, this.Demo.charRect: Rect2i | nullcharRect, this.Demo.drawPos: Vector2idrawPos, cell: anycell.block * this.Demo.colorCount: numbercolorCount);
import uiui.caption(const centerX: anycenterX - (cell: anycell.name.length * import FONT_WFONT_W) / 2, panelY: anypanelY + const NAME_Y: 54NAME_Y, cell: anycell.name, { color: anycolor: cell: anycell.color });
import uiui.caption(const centerX: anycenterX - (cell: anycell.note.length * import FONT_WFONT_W) / 2, panelY: anypanelY + const NOTE_Y: 64NOTE_Y, cell: anycell.note, { color: stringcolor: 'dim' });
}
// Draws the Static panel: six effects whose theme blocks were built once in init() -
// Normal, Silhouette, Team Red, Team Blue, Team Green, Frozen.
Demo.renderStaticEffects(): voidrenderStaticEffects() {
this.Demo.renderPanel(x: any, y: any, width: any, title: any): voidrenderPanel(const PANEL_X: 8PANEL_X, const STATIC_PANEL_Y: 136STATIC_PANEL_Y, const DISPLAY_W: 640DISPLAY_W - 2 * const PANEL_X: 8PANEL_X, 'Static - blocks built once in init()');
for (let let i: numberi = 0; let i: numberi < const STATIC_CELLS: {}STATIC_CELLS.length; let i: numberi++) {
this.Demo.renderCell(cellX: any, panelY: any, cell: any): voidrenderCell(const PANEL_X: 8PANEL_X + let i: numberi * const CELL_W: 104CELL_W, const STATIC_PANEL_Y: 136STATIC_PANEL_Y, const STATIC_CELLS: {}STATIC_CELLS[let i: numberi]);
}
}
// Draws the Animated panel: four effects whose theme blocks update() rewrites every
// tick - Damage, Ghost, Invincible, Poisoned. render() draws them exactly like the
// static ones; only the colors in their blocks change.
Demo.renderDynamicEffects(): voidrenderDynamicEffects() {
this.Demo.renderPanel(x: any, y: any, width: any, title: any): voidrenderPanel(const PANEL_X: 8PANEL_X, const ANIMATED_PANEL_Y: numberANIMATED_PANEL_Y, const ANIMATED_PANEL_W: numberANIMATED_PANEL_W, 'Animated - rewritten in update()');
for (let let i: numberi = 0; let i: numberi < const ANIMATED_CELLS: {}ANIMATED_CELLS.length; let i: numberi++) {
this.Demo.renderCell(cellX: any, panelY: any, cell: any): voidrenderCell(const PANEL_X: 8PANEL_X + let i: numberi * const CELL_W: 104CELL_W, const ANIMATED_PANEL_Y: numberANIMATED_PANEL_Y, const ANIMATED_CELLS: {}ANIMATED_CELLS[let i: numberi]);
}
}
// Draws the Day/night panel: the sprite in the current ambient light with a short
// label beside it, and a time bar under both with a marker crossing it once every 20
// seconds.
Demo.renderDayNightCycle(): voidrenderDayNightCycle() {
const const panelX: numberpanelX = const DAYNIGHT_PANEL_X: numberDAYNIGHT_PANEL_X;
const const panelY: numberpanelY = const ANIMATED_PANEL_Y: numberANIMATED_PANEL_Y;
this.Demo.renderPanel(x: any, y: any, width: any, title: any): voidrenderPanel(const panelX: numberpanelX, const panelY: numberpanelY, const DAYNIGHT_PANEL_W: numberDAYNIGHT_PANEL_W, 'Day/night cycle');
// The bar starts 10 pixels in from the panel edge, and the sprite is shifted so
// its visible pixels (4 pixels in from its frame) start at the same x.
const const barX: numberbarX = const panelX: numberpanelX + 10;
const const barY: numberbarY = const panelY: numberpanelY + const SPRITE_TOP: 14SPRITE_TOP + 41;
const const barHeight: 6barHeight = 6;
this.Demo.drawPos: Vector2idrawPos.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(const barX: numberbarX - 4, const panelY: numberpanelY + const SPRITE_TOP: 14SPRITE_TOP);
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.sheet: SpriteSheet | nullsheet, this.Demo.charRect: Rect2i | nullcharRect, this.Demo.drawPos: Vector2idrawPos, const BLOCK_DAYNIGHT: 12BLOCK_DAYNIGHT * this.Demo.colorCount: numbercolorCount);
// The label sits right of the sprite, level with the middle of its artwork.
import uiui.caption(const barX: numberbarX + 46, const panelY: numberpanelY + const SPRITE_TOP: 14SPRITE_TOP + 12, 'Ambient light', { color: stringcolor: 'text' });
import uiui.caption(const barX: numberbarX + 46, const panelY: numberpanelY + const SPRITE_TOP: 14SPRITE_TOP + 22, '20-second loop', { color: stringcolor: 'dim' });
// The bar is drawn inside the panel, so it uses ui.drawRectFill() and
// ui.drawRect(): the same arguments as BT.drawRectFill() and BT.drawRect(), but
// drawn as sprites so they land on top of the panel instead of under it. The
// track is the darker screen background, so it stands out from the panel fill.
import uiui.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(const barX: numberbarX, const barY: numberbarY, const DAYNIGHT_BAR_W: 180DAYNIGHT_BAR_W, const barHeight: 6barHeight), this.Demo.theme: nulltheme.bg);
// The bar is split into four equal quarters, one per phase, by three thin tick
// lines in the border color.
const const quarterW: numberquarterW = const DAYNIGHT_BAR_W: 180DAYNIGHT_BAR_W / const DAYNIGHT_PHASES: {}DAYNIGHT_PHASES.length;
for (let let i: numberi = 1; let i: numberi < const DAYNIGHT_PHASES: {}DAYNIGHT_PHASES.length; let i: numberi++) {
import uiui.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(const barX: numberbarX + let i: numberi * const quarterW: numberquarterW, const barY: numberbarY, 1, const barHeight: 6barHeight), this.Demo.theme: nulltheme.border);
}
// cycle runs 0-1 over 1200 ticks, the same clock updateDayNightBlock() uses.
const const cycle: numbercycle = (const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.ticks: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks % 1200) / 1200;
const const markerX: anymarkerX = const barX: numberbarX + Math.floor((const DAYNIGHT_BAR_W: 180DAYNIGHT_BAR_W - 1) * const cycle: numbercycle);
// The marker is painted with the last slot of the day/night block (COLOR_BASE +
// 12 * colorCount + colorCount - 1). The base colors are sorted darkest first, so
// that is the sprite's brightest color: bright at noon, a dim blue at midnight,
// always visible against the dark track.
const const markerSlot: numbermarkerSlot = const COLOR_BASE: 12COLOR_BASE + const BLOCK_DAYNIGHT: 12BLOCK_DAYNIGHT * this.Demo.colorCount: numbercolorCount + this.Demo.colorCount: numbercolorCount - 1;
import uiui.drawRect(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(const barX: numberbarX, const barY: numberbarY, const DAYNIGHT_BAR_W: 180DAYNIGHT_BAR_W, const barHeight: 6barHeight), this.Demo.theme: nulltheme.border);
import uiui.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(const markerX: anymarkerX - 1, const barY: numberbarY - 2, 3, const barHeight: 6barHeight + 4), const markerSlot: numbermarkerSlot);
// Phase labels, each centered in its own quarter of the bar.
for (let let i: numberi = 0; let i: numberi < const DAYNIGHT_PHASES: {}DAYNIGHT_PHASES.length; let i: numberi++) {
const const label: anylabel = const DAYNIGHT_PHASES: {}DAYNIGHT_PHASES[let i: numberi];
const const labelX: numberlabelX = const barX: numberbarX + let i: numberi * const quarterW: numberquarterW + (const quarterW: numberquarterW - const label: anylabel.length * import FONT_WFONT_W) / 2;
import uiui.caption(const labelX: numberlabelX, const barY: numberbarY + const barHeight: 6barHeight + 2, const label: anylabel, { color: stringcolor: 'dim' });
}
}
}
// Hand the Demo class to BLIT386 to start the demo 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);