// Sprite Effects: shows how to use palette offsets to create game effects.
// @description Palette offsets as game effects: damage flash, silhouette, ghost fade, team colors, and day or night.
//
// Prerequisites: Basics (https://demos.blit386.dev/basics),
// Sprites (https://demos.blit386.dev/sprites).
// Guide: https://blit386.dev/docs/guides/palette#per-draw-palette-offsets-zero-cost-color-variants
//
// In the palette-based rendering system, each sprite pixel stores a palette index.
// By drawing the same sprite with a different palette offset (the fourth argument
// to BT.drawSprite), every pixel shifts to a different color block in the palette.
// This replaces what older APIs called "tinting" (multiplying pixels by a color).
//
// Common uses in retro games:
// - Damage flash: swap to a block of reds, then back to normal after 30 ticks
// - Silhouette: use a block where all colors are black
// - Ghost: a block where all colors have low alpha
// - Team colors: separate pre-built blocks for red/blue/green teams
// - Status effects: frozen (all shifted to cold blues), poisoned (pulsing greens)
// - Day/night: a block dynamically updated every tick to reflect ambient light
//
// For STATIC effects (normal, silhouette, team red/blue/green, frozen): we build color
// blocks once in init() and never change them.
//
// For DYNAMIC effects (damage flash, ghost pulse, invincibility, poison, day/night):
// we update the color block in update() every tick and draw with that offset in render().
//
// We learned about palette offsets in Sprites demo:
// https://demos.blit386.dev/sprites
//
// FULLSCREEN CRT (Tesla Orava TV)
// The sprite grid is drawn at 640x400, then the engine resolves and upscales it and runs
// a display-tier stack tuned for 1970s Czechoslovak B/W CRT sets (Tesla Orava 131/226/229):
// curved tube, soft phosphor halation, scanlines, light bezel vignette, a roll band
// scrolling top-to-bottom, subtle offset-band wobble, and occasional TV faults (tear,
// snow, dim, ghost, roll). Same building blocks as basics-enhanced demo; chroma split is omitted (B/W).
// Post-process needs WebGPU; the software renderer still shows the sprite effects without CRT.
//
// We learned about composing effects in PipBoy CRT and Basics Enhanced.
//
// Captions, the day/night legend, and the software fallback note are drawn with the shared
// UI kit (src/shared/ui.js), which installs its twelve UI colors high in the palette
// (slots 240-251) via applyTheme().
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;
... 106 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 uiui, import UI_ANCHORSUI_ANCHORS } from './shared/ui.js';
/** @typedef {import('blit386').IBTDemo} IBTDemo */
/** @typedef {import('blit386').HardwareSettings} HardwareSettings */
/** @typedef {import('blit386').Palette} Palette */
/** @typedef {import('blit386').SpriteSheet} SpriteSheet */
/** @typedef {import('blit386').Rect2i} Rect2i */
/** @typedef {import('blit386').PixelGlitch} PixelGlitch */
/** @typedef {import('blit386').BarrelDistortion} BarrelDistortion */
/** @typedef {import('blit386').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.
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 UI elements; a static slot outside both the sprite
// theme range (12-89) and the applyTheme() range (240-251), set directly in init().
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, extracted in init(). Offset = blockIndex * colorCount.
// Blocks 0..7 are static; blocks 8..12 are dynamic (updated in update()).
//
// Block 0 (offset 0): Original stone 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: toggling 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;
// 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;
// Display-tier CRT runs on the upscaled RGBA buffer (3x logical, like 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 glitch bursts (ticks at default 60 FPS).
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 us an array of two shorter lines the UI kit can draw one under
// the other when the software renderer is active (same pattern as basics-enhanced demo).
const const FALLBACK_LINES: anyFALLBACK_LINES = import SOFTWARE_FALLBACK_NOTESOFTWARE_FALLBACK_NOTE.split('. ');
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 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.
*
* @param {number} ms - Target delay in milliseconds.
*/
function function burnCpuMs(ms: number): voidSpins 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.burnCpuMs(ms: number- Target delay in milliseconds.ms) {
if (ms: number- Target delay in milliseconds.ms <= 0) {
return;
}
// performance.now() returns a high-resolution clock in milliseconds.
const const deadline: anydeadline = performance.now() + ms: number- Target delay in milliseconds.ms;
while (performance.now() < const deadline: anydeadline) {
// Empty loop on purpose: we are waiting for the clock, not doing useful work.
}
}
/**
* Demonstrates palette-offset based sprite effects.
* Static effects are pre-built in init(); dynamic effects update in update().
*
* @implements {IBTDemo}
*/
class class DemoDemonstrates palette-offset based sprite effects.
Static effects are pre-built in init(); dynamic effects update in update().Demo {
// 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 for the rock 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. Computed in init() after the colors are extracted.
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 our own drawing.
Demo.theme: nulltheme = null;
// The extracted original Color32 objects (used to build theme blocks).
Demo.baseColors: {}baseColors = [];
// animTime drives all dynamic effects.
Demo.animTime: numberanimTime = 0;
// Which tick the last "damage event" occurred on (for the damage flash).
Demo.damageFlashTick: numberdamageFlashTick = 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;
Demo.effectsAvailable: booleaneffectsAvailable = false;
Demo.glitchCooldown: numberglitchCooldown = 0;
// How many ticks the current TV fault burst still has to run (0 = no burst active).
Demo.glitchTicksLeft: numberglitchTicksLeft = 0;
Demo.glitchDuration: numberglitchDuration = 0;
Demo.glitchType: stringglitchType = 'none';
Demo.glitchPeak: numberglitchPeak = 0;
Demo.bandWobbleCooldown: numberbandWobbleCooldown = 0;
// How many ticks the current band wobble still has to run (0 = no wobble active).
Demo.bandWobbleTicksLeft: numberbandWobbleTicksLeft = 0;
Demo.bandWobbleDuration: numberbandWobbleDuration = 0;
Demo.bandWobbleSeed: numberbandWobbleSeed = 0;
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 },
];
/**
* Wider logical screen for the sprite grid; display-tier Tesla Orava runs at 3x upscale.
*
* @returns {Partial<HardwareSettings>}
*/
Demo.configure(): Partial<HardwareSettings>Wider logical screen for the sprite grid; display-tier Tesla Orava runs at 3x upscale.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 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, and loads the sprite.
*
* @returns {Promise<boolean>} Returns true when everything is ready.
*/
async Demo.init(): Promise<boolean>Sets up the palette, builds all 13 theme blocks, and loads the sprite.init() {
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;
... 106 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 we set it 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));
// Extract sprite colors
// Ask the engine to scan the PNG and add every unique color it finds into our palette,
// starting at COLOR_BASE. The returned array is the same colors in palette-write order
// (sorted darkest-first by brightness). We keep them so the theme-block builders can
// tint each base color and write the result into a higher slot.
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(): voidBuilds the 8 static theme blocks by transforming the base colors.
Called once in init() - these never change after setup.buildStaticThemeBlocks();
// Dynamic blocks (8..12) start as copies of the original.
// update() will replace 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 sprite
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";
}): 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;
... 106 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;
}
// Glitch state is shared by both backends - initialize once before the CRT check.
// BT.random is the engine's shared random number generator.
// Its int() method returns a whole number from the first value up to (but not including) the second,
// so this waits a random number of ticks before the first burst.
this.Demo.glitchCooldown: numberglitchCooldown = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run. Mutating the instance (for example `BT.random.int(10)`)
advances the shared stream.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 render()).
this.Demo.setupCrtStack(): voidBuilds the Tesla Orava effect chain once in init(): the pixel-tier band tear plus
the display-tier tube look, in back-to-front order.setupCrtStack();
this.Demo.bandWobbleCooldown: numberbandWobbleCooldown = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run. Mutating the instance (for example `BT.random.int(10)`)
advances the shared stream.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 updates all five dynamic theme blocks.
*
* The damage flash toggles between Block 2 (white) and Block 3 (red) every 3 ticks.
* Ghost pulses alpha between 40 and 180.
* Invincibility rotates hue around the color wheel.
* Poison pulses brightness up and down.
* Day/night smoothly cycles brightness over 20 seconds.
*/
Demo.update(): voidAdvances animTime and updates all five dynamic theme blocks.
The damage flash toggles between Block 2 (white) and Block 3 (red) every 3 ticks.
Ghost pulses alpha between 40 and 180.
Invincibility rotates hue around the color wheel.
Poison pulses brightness up and down.
Day/night smoothly cycles brightness over 20 seconds.update() {
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;
... 106 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;
... 106 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;
... 106 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(): voidDamage flash: alternates between "all white" and "all red" every 3 ticks
for the first 30 ticks after damage. Fades back to normal after that.updateDamageFlashBlock();
this.Demo.updateGhostBlock(): voidGhost: the sprite appears semi-transparent with a blue-white tint.
Alpha pulses between 40 and 180 using a sine wave.updateGhostBlock();
this.Demo.updateInvincibleBlock(): voidInvincibility: cycles the entire sprite through the rainbow.
The hue rotates 200 degrees per second.updateInvincibleBlock();
this.Demo.updatePoisonBlock(): voidPoison: a green tint that pulses brighter and darker 5 times per second.updatePoisonBlock();
this.Demo.updateDayNightBlock(): voidDay/night: a brightness multiplier that cycles over 20 seconds (1200 ticks).
At midday the multiplier is ~1.0; at midnight it drops to ~0.3.updateDayNightBlock();
if (this.Demo.effectsAvailable: booleaneffectsAvailable) {
this.Demo.updateCrtEffects(): voidDrives RollLine scroll, TV fault bursts, and occasional subtle band offset wobble.updateCrtEffects();
}
// 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.
// Keep the combined update+render busy-wait well under a 60 FPS frame (~16 ms)
// so low-power devices stay responsive while the timing chart still shows a pulse.
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;
... 106 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: number): voidSpins 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.burnCpuMs(const chartUpdateLoadMs: numberchartUpdateLoadMs);
}
/**
* Runs once per screen refresh to draw all the sprite effect demonstrations.
* Notice: NO Color32 objects appear in draw calls - only palette indices and offsets.
*/
Demo.render(): voidRuns once per screen refresh to draw all the sprite effect demonstrations.
Notice: NO Color32 objects appear in draw calls - only palette indices and offsets.render() {
// 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;
... 106 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 yellow scrolling bars.
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;
... 106 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: number): voidSpins 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.burnCpuMs(const chartRenderLoadMs: numberchartRenderLoadMs);
// Draw both effect rows.
this.Demo.renderStaticEffects(): voidDraws the first row: six static palette-offset effects.
Normal, Silhouette, Team Red/Blue/Green, Frozen.renderStaticEffects();
this.Demo.renderDynamicEffects(): voidDraws the second row: four dynamic palette-offset effects (updated in update()).
Damage Flash, Ghost, Invincibility, Poison.renderDynamicEffects();
// Day/night cycle at the bottom.
this.Demo.renderDayNightCycle(): voidShows a day/night cycle effect: the sprite dims at night and brightens at noon.
A progress bar shows the current phase.renderDayNightCycle();
// 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 and easy to read.
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();
}
}
/**
* Tesla Orava stack and current analog-TV fault (overlay custom rows).
*
* @returns {readonly { leftText: string }[]}
*/
Demo.overlayRows(): readonly {
leftText: string;
}[]
Tesla Orava stack and current analog-TV fault (overlay custom rows).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(): voidBuilds the Tesla Orava effect chain once in init(): the pixel-tier band tear plus
the display-tier tube look, in back-to-front order.setupCrtStack() {
// 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;
... 106 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;
... 106 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 RollLine scroll, TV fault bursts, and occasional subtle band offset wobble.
*/
Demo.updateCrtEffects(): voidDrives RollLine scroll, TV fault bursts, and occasional subtle band offset wobble.updateCrtEffects() {
const const seconds: numberseconds = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.timeSeconds: numberFixed-step elapsed time in seconds (`BT.ticks * BT.deltaSeconds`).timeSeconds;
this.Demo.rollLine: RollLine | nullrollLine.RollLine.time: numberWall-clock seconds; demos typically drive this each frame.time = const seconds: numberseconds;
this.Demo.noise: Noise | nullnoise.Noise.time: numberWall-clock seconds; reseeds the noise each frame.time = const seconds: numberseconds;
this.Demo.interference: Interference | nullinterference.Interference.time: numberWall-clock seconds; reseeds the row offsets each frame.time = const seconds: numberseconds;
if (this.Demo.glitchTicksLeft: numberglitchTicksLeft > 0) {
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(): voidResting Tesla Orava look: scrolling roll band plus calm noise/flicker.applyRestingCrtUniforms();
this.Demo.applyGlitchUniforms(envelope: number): 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;
... 106 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. 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;
... 106 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. 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(): voidResting Tesla Orava look: scrolling roll band plus calm noise/flicker.applyRestingCrtUniforms();
if (this.Demo.bandWobbleTicksLeft: numberbandWobbleTicksLeft > 0) {
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;
... 106 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. 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;
... 106 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. 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;
... 106 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. 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) {
// pick() draws one item out of a list, like taking a card off the top of a shuffled deck.
// float() is the decimal cousin of int().
this.Demo.glitchType: stringglitchType = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run. Mutating the instance (for example `BT.random.int(10)`)
advances the shared stream.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;
... 106 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. 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;
... 106 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. 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;
... 106 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. 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/flicker. */
Demo.applyRestingCrtUniforms(): voidResting Tesla Orava look: scrolling roll band plus calm noise/flicker.applyRestingCrtUniforms() {
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;
}
/**
* @param {number} envelope
*/
Demo.applyGlitchUniforms(envelope: number): voidapplyGlitchUniforms(envelope: numberenvelope) {
const const peak: numberpeak = this.Demo.glitchPeak: numberglitchPeak * envelope: numberenvelope;
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: numberenvelope;
} 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 8 static theme blocks by transforming the base colors.
* Called once in init() - these never change after setup.
*/
Demo.buildStaticThemeBlocks(): voidBuilds the 8 static theme blocks by transforming the base colors.
Called once in init() - these never change after setup.buildStaticThemeBlocks() {
// Block 0 (original) is already filled by SpriteSheet.loadColorsIntoPalette above.
// 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.
// Floor channels 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 average brightness of this pixel (0..255 range).
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 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 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() above), unlike
// buildStaticThemeBlocks() which only 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
// brand-new function 60 times a second. A raw loop reuses the same code without allocating
// anything extra per tick.
/**
* Damage flash: alternates between "all white" and "all red" every 3 ticks
* for the first 30 ticks after damage. Fades back to normal after that.
*/
Demo.updateDamageFlashBlock(): voidDamage flash: alternates between "all white" and "all red" every 3 ticks
for the first 30 ticks after damage. Fades back to normal after that.updateDamageFlashBlock() {
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;
... 106 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 = show white flash; 1 = show 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: use 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 using a sine wave.
*/
Demo.updateGhostBlock(): voidGhost: the sprite appears semi-transparent with a blue-white tint.
Alpha pulses between 40 and 180 using a sine wave.updateGhostBlock() {
// Math.sin oscillates between -1 and 1; we shift 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(): voidInvincibility: cycles the entire sprite through the rainbow.
The hue rotates 200 degrees per second.updateInvincibleBlock() {
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];
// fromHSL takes hue (0-360), saturation (0-100), lightness (0-100).
// We use varying lightness 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%
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 that pulses brighter and darker 5 times per second.
*/
Demo.updatePoisonBlock(): voidPoison: a green tint that pulses brighter and darker 5 times per second.updatePoisonBlock() {
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 multiplier that cycles over 20 seconds (1200 ticks).
* At midday the multiplier is ~1.0; at midnight it drops to ~0.3.
*/
Demo.updateDayNightBlock(): voidDay/night: a brightness multiplier that cycles over 20 seconds (1200 ticks).
At midday the multiplier is ~1.0; at midnight it drops to ~0.3.updateDayNightBlock() {
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;
... 106 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
// colors as brightness approaches 1.
// Color32.lerp(a, b, t): t=0 is all `a`, t=1 is all `b`; matches the old per-channel formula.
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 the first row: six static palette-offset effects.
* Normal, Silhouette, Team Red/Blue/Green, Frozen.
*/
Demo.renderStaticEffects(): voidDraws the first row: six static palette-offset effects.
Normal, Silhouette, Team Red/Blue/Green, Frozen.renderStaticEffects() {
const const row1Y: 130row1Y = 130;
const const spacing: 100spacing = 100;
const const n: numbern = this.Demo.colorCount: numbercolorCount;
// Each cell is the same sprite drawn with a different palette offset, with a
// named caption and a one-line "when you would use this" note under it.
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => voidDraws a sprite region from an indexed sprite sheet.
Sprite draws are batched internally. Grouping draws from the same
{@link
SpriteSheet
}
minimizes batch flushes and reduces GPU state changes.
The sprite sheet must have been converted to palette indices via
`spriteSheet.indexize(palette)` before the first draw call. Prefer
`SpriteSheet.loadIndexed(...)` for one-call setup.
**Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1.
Index 0 is always transparent and is discarded by the fragment shader. The final palette
lookup is `storedIndex + paletteOffset`, so:
- `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`.
`palette[0]` is never reachable because stored indices start at 1.
- `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored
at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`,
and so on. Use this for palette-swap effects such as team colors or damage flashes.
**Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to
the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's
robust buffer access returns 0 for every component; because the fragment shader forces alpha
to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative
JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also
produces out-of-bounds black pixels.drawSprite(this.Demo.sheet: SpriteSheet | nullsheet, this.Demo.charRect: Rect2i | nullcharRect, new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(10, const row1Y: 130row1Y), const BLOCK_ORIGINAL: 0BLOCK_ORIGINAL * const n: numbern);
import uiui.caption(6, const row1Y: 130row1Y + 40, 'Normal', { color: stringcolor: 'text' });
import uiui.caption(6, const row1Y: 130row1Y + 50, 'Default look', { color: stringcolor: 'dim' });
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => voidDraws a sprite region from an indexed sprite sheet.
Sprite draws are batched internally. Grouping draws from the same
{@link
SpriteSheet
}
minimizes batch flushes and reduces GPU state changes.
The sprite sheet must have been converted to palette indices via
`spriteSheet.indexize(palette)` before the first draw call. Prefer
`SpriteSheet.loadIndexed(...)` for one-call setup.
**Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1.
Index 0 is always transparent and is discarded by the fragment shader. The final palette
lookup is `storedIndex + paletteOffset`, so:
- `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`.
`palette[0]` is never reachable because stored indices start at 1.
- `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored
at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`,
and so on. Use this for palette-swap effects such as team colors or damage flashes.
**Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to
the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's
robust buffer access returns 0 for every component; because the fragment shader forces alpha
to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative
JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also
produces out-of-bounds black pixels.drawSprite(this.Demo.sheet: SpriteSheet | nullsheet, this.Demo.charRect: Rect2i | nullcharRect, new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(10 + const spacing: 100spacing, const row1Y: 130row1Y), const BLOCK_SILHOUETTE: 1BLOCK_SILHOUETTE * const n: numbern);
import uiui.caption(6 + const spacing: 100spacing, const row1Y: 130row1Y + 40, 'Silhouette', { color: stringcolor: 'text' });
import uiui.caption(6 + const spacing: 100spacing, const row1Y: 130row1Y + 50, 'Stealth', { color: stringcolor: 'dim' });
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => voidDraws a sprite region from an indexed sprite sheet.
Sprite draws are batched internally. Grouping draws from the same
{@link
SpriteSheet
}
minimizes batch flushes and reduces GPU state changes.
The sprite sheet must have been converted to palette indices via
`spriteSheet.indexize(palette)` before the first draw call. Prefer
`SpriteSheet.loadIndexed(...)` for one-call setup.
**Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1.
Index 0 is always transparent and is discarded by the fragment shader. The final palette
lookup is `storedIndex + paletteOffset`, so:
- `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`.
`palette[0]` is never reachable because stored indices start at 1.
- `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored
at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`,
and so on. Use this for palette-swap effects such as team colors or damage flashes.
**Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to
the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's
robust buffer access returns 0 for every component; because the fragment shader forces alpha
to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative
JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also
produces out-of-bounds black pixels.drawSprite(this.Demo.sheet: SpriteSheet | nullsheet, this.Demo.charRect: Rect2i | nullcharRect, new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(10 + const spacing: 100spacing * 2, const row1Y: 130row1Y), const BLOCK_TEAM_RED: 4BLOCK_TEAM_RED * const n: numbern);
import uiui.caption(6 + const spacing: 100spacing * 2, const row1Y: 130row1Y + 40, 'Team Red', { color: stringcolor: 'warm' });
import uiui.caption(6 + const spacing: 100spacing * 2, const row1Y: 130row1Y + 50, 'Friendly team', { color: stringcolor: 'dim' });
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => voidDraws a sprite region from an indexed sprite sheet.
Sprite draws are batched internally. Grouping draws from the same
{@link
SpriteSheet
}
minimizes batch flushes and reduces GPU state changes.
The sprite sheet must have been converted to palette indices via
`spriteSheet.indexize(palette)` before the first draw call. Prefer
`SpriteSheet.loadIndexed(...)` for one-call setup.
**Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1.
Index 0 is always transparent and is discarded by the fragment shader. The final palette
lookup is `storedIndex + paletteOffset`, so:
- `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`.
`palette[0]` is never reachable because stored indices start at 1.
- `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored
at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`,
and so on. Use this for palette-swap effects such as team colors or damage flashes.
**Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to
the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's
robust buffer access returns 0 for every component; because the fragment shader forces alpha
to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative
JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also
produces out-of-bounds black pixels.drawSprite(this.Demo.sheet: SpriteSheet | nullsheet, this.Demo.charRect: Rect2i | nullcharRect, new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(10 + const spacing: 100spacing * 3, const row1Y: 130row1Y), const BLOCK_TEAM_BLUE: 5BLOCK_TEAM_BLUE * const n: numbern);
import uiui.caption(6 + const spacing: 100spacing * 3, const row1Y: 130row1Y + 40, 'Team Blue', { color: stringcolor: 'info' });
import uiui.caption(6 + const spacing: 100spacing * 3, const row1Y: 130row1Y + 50, 'Enemy team', { color: stringcolor: 'dim' });
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => voidDraws a sprite region from an indexed sprite sheet.
Sprite draws are batched internally. Grouping draws from the same
{@link
SpriteSheet
}
minimizes batch flushes and reduces GPU state changes.
The sprite sheet must have been converted to palette indices via
`spriteSheet.indexize(palette)` before the first draw call. Prefer
`SpriteSheet.loadIndexed(...)` for one-call setup.
**Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1.
Index 0 is always transparent and is discarded by the fragment shader. The final palette
lookup is `storedIndex + paletteOffset`, so:
- `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`.
`palette[0]` is never reachable because stored indices start at 1.
- `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored
at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`,
and so on. Use this for palette-swap effects such as team colors or damage flashes.
**Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to
the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's
robust buffer access returns 0 for every component; because the fragment shader forces alpha
to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative
JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also
produces out-of-bounds black pixels.drawSprite(this.Demo.sheet: SpriteSheet | nullsheet, this.Demo.charRect: Rect2i | nullcharRect, new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(10 + const spacing: 100spacing * 4, const row1Y: 130row1Y), const BLOCK_TEAM_GREEN: 6BLOCK_TEAM_GREEN * const n: numbern);
import uiui.caption(6 + const spacing: 100spacing * 4, const row1Y: 130row1Y + 40, 'Team Green', { color: stringcolor: 'accent' });
import uiui.caption(6 + const spacing: 100spacing * 4, const row1Y: 130row1Y + 50, 'Ally team', { color: stringcolor: 'dim' });
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => voidDraws a sprite region from an indexed sprite sheet.
Sprite draws are batched internally. Grouping draws from the same
{@link
SpriteSheet
}
minimizes batch flushes and reduces GPU state changes.
The sprite sheet must have been converted to palette indices via
`spriteSheet.indexize(palette)` before the first draw call. Prefer
`SpriteSheet.loadIndexed(...)` for one-call setup.
**Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1.
Index 0 is always transparent and is discarded by the fragment shader. The final palette
lookup is `storedIndex + paletteOffset`, so:
- `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`.
`palette[0]` is never reachable because stored indices start at 1.
- `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored
at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`,
and so on. Use this for palette-swap effects such as team colors or damage flashes.
**Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to
the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's
robust buffer access returns 0 for every component; because the fragment shader forces alpha
to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative
JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also
produces out-of-bounds black pixels.drawSprite(this.Demo.sheet: SpriteSheet | nullsheet, this.Demo.charRect: Rect2i | nullcharRect, new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(10 + const spacing: 100spacing * 5, const row1Y: 130row1Y), const BLOCK_FROZEN: 7BLOCK_FROZEN * const n: numbern);
import uiui.caption(6 + const spacing: 100spacing * 5, const row1Y: 130row1Y + 40, 'Frozen', { color: stringcolor: 'info' });
import uiui.caption(6 + const spacing: 100spacing * 5, const row1Y: 130row1Y + 50, 'Slow freeze status', { color: stringcolor: 'dim' });
}
/**
* Draws the second row: four dynamic palette-offset effects (updated in update()).
* Damage Flash, Ghost, Invincibility, Poison.
*/
Demo.renderDynamicEffects(): voidDraws the second row: four dynamic palette-offset effects (updated in update()).
Damage Flash, Ghost, Invincibility, Poison.renderDynamicEffects() {
const const row2Y: 190row2Y = 190;
const const spacing: 100spacing = 100;
const const n: numbern = this.Demo.colorCount: numbercolorCount;
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => voidDraws a sprite region from an indexed sprite sheet.
Sprite draws are batched internally. Grouping draws from the same
{@link
SpriteSheet
}
minimizes batch flushes and reduces GPU state changes.
The sprite sheet must have been converted to palette indices via
`spriteSheet.indexize(palette)` before the first draw call. Prefer
`SpriteSheet.loadIndexed(...)` for one-call setup.
**Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1.
Index 0 is always transparent and is discarded by the fragment shader. The final palette
lookup is `storedIndex + paletteOffset`, so:
- `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`.
`palette[0]` is never reachable because stored indices start at 1.
- `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored
at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`,
and so on. Use this for palette-swap effects such as team colors or damage flashes.
**Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to
the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's
robust buffer access returns 0 for every component; because the fragment shader forces alpha
to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative
JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also
produces out-of-bounds black pixels.drawSprite(this.Demo.sheet: SpriteSheet | nullsheet, this.Demo.charRect: Rect2i | nullcharRect, new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(10, const row2Y: 190row2Y), const BLOCK_DAMAGE_FLASH: 8BLOCK_DAMAGE_FLASH * const n: numbern);
import uiui.caption(6, const row2Y: 190row2Y + 40, 'Damage', { color: stringcolor: 'warm' });
import uiui.caption(6, const row2Y: 190row2Y + 50, 'Hit flash', { color: stringcolor: 'dim' });
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => voidDraws a sprite region from an indexed sprite sheet.
Sprite draws are batched internally. Grouping draws from the same
{@link
SpriteSheet
}
minimizes batch flushes and reduces GPU state changes.
The sprite sheet must have been converted to palette indices via
`spriteSheet.indexize(palette)` before the first draw call. Prefer
`SpriteSheet.loadIndexed(...)` for one-call setup.
**Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1.
Index 0 is always transparent and is discarded by the fragment shader. The final palette
lookup is `storedIndex + paletteOffset`, so:
- `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`.
`palette[0]` is never reachable because stored indices start at 1.
- `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored
at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`,
and so on. Use this for palette-swap effects such as team colors or damage flashes.
**Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to
the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's
robust buffer access returns 0 for every component; because the fragment shader forces alpha
to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative
JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also
produces out-of-bounds black pixels.drawSprite(this.Demo.sheet: SpriteSheet | nullsheet, this.Demo.charRect: Rect2i | nullcharRect, new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(10 + const spacing: 100spacing, const row2Y: 190row2Y), const BLOCK_GHOST: 9BLOCK_GHOST * const n: numbern);
import uiui.caption(6 + const spacing: 100spacing, const row2Y: 190row2Y + 40, 'Ghost', { color: stringcolor: 'info' });
import uiui.caption(6 + const spacing: 100spacing, const row2Y: 190row2Y + 50, 'Spirit', { color: stringcolor: 'dim' });
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => voidDraws a sprite region from an indexed sprite sheet.
Sprite draws are batched internally. Grouping draws from the same
{@link
SpriteSheet
}
minimizes batch flushes and reduces GPU state changes.
The sprite sheet must have been converted to palette indices via
`spriteSheet.indexize(palette)` before the first draw call. Prefer
`SpriteSheet.loadIndexed(...)` for one-call setup.
**Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1.
Index 0 is always transparent and is discarded by the fragment shader. The final palette
lookup is `storedIndex + paletteOffset`, so:
- `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`.
`palette[0]` is never reachable because stored indices start at 1.
- `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored
at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`,
and so on. Use this for palette-swap effects such as team colors or damage flashes.
**Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to
the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's
robust buffer access returns 0 for every component; because the fragment shader forces alpha
to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative
JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also
produces out-of-bounds black pixels.drawSprite(this.Demo.sheet: SpriteSheet | nullsheet, this.Demo.charRect: Rect2i | nullcharRect, new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(10 + const spacing: 100spacing * 2, const row2Y: 190row2Y), const BLOCK_INVINCIBLE: 10BLOCK_INVINCIBLE * const n: numbern);
import uiui.caption(6 + const spacing: 100spacing * 2, const row2Y: 190row2Y + 40, 'Invincible', { color: stringcolor: 'text' });
import uiui.caption(6 + const spacing: 100spacing * 2, const row2Y: 190row2Y + 50, 'Power-up mode', { color: stringcolor: 'dim' });
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => voidDraws a sprite region from an indexed sprite sheet.
Sprite draws are batched internally. Grouping draws from the same
{@link
SpriteSheet
}
minimizes batch flushes and reduces GPU state changes.
The sprite sheet must have been converted to palette indices via
`spriteSheet.indexize(palette)` before the first draw call. Prefer
`SpriteSheet.loadIndexed(...)` for one-call setup.
**Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1.
Index 0 is always transparent and is discarded by the fragment shader. The final palette
lookup is `storedIndex + paletteOffset`, so:
- `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`.
`palette[0]` is never reachable because stored indices start at 1.
- `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored
at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`,
and so on. Use this for palette-swap effects such as team colors or damage flashes.
**Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to
the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's
robust buffer access returns 0 for every component; because the fragment shader forces alpha
to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative
JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also
produces out-of-bounds black pixels.drawSprite(this.Demo.sheet: SpriteSheet | nullsheet, this.Demo.charRect: Rect2i | nullcharRect, new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(10 + const spacing: 100spacing * 3, const row2Y: 190row2Y), const BLOCK_POISON: 11BLOCK_POISON * const n: numbern);
import uiui.caption(6 + const spacing: 100spacing * 3, const row2Y: 190row2Y + 40, 'Poisoned', { color: stringcolor: 'accent' });
import uiui.caption(6 + const spacing: 100spacing * 3, const row2Y: 190row2Y + 50, 'Poison time', { color: stringcolor: 'dim' });
}
/**
* Shows a day/night cycle effect: the sprite dims at night and brightens at noon.
* A progress bar shows the current phase.
*/
Demo.renderDayNightCycle(): voidShows a day/night cycle effect: the sprite dims at night and brightens at noon.
A progress bar shows the current phase.renderDayNightCycle() {
const const baseY: 260baseY = 260;
const const n: numbern = this.Demo.colorCount: numbercolorCount;
import uiui.caption(10, const baseY: 260baseY, 'Day/Night Cycle:', { color: stringcolor: 'header' });
import uiui.caption(10, const baseY: 260baseY + 10, 'Ambient light palette offset', { color: stringcolor: 'dim' });
// Draw the sprite with the day/night block.
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => voidDraws a sprite region from an indexed sprite sheet.
Sprite draws are batched internally. Grouping draws from the same
{@link
SpriteSheet
}
minimizes batch flushes and reduces GPU state changes.
The sprite sheet must have been converted to palette indices via
`spriteSheet.indexize(palette)` before the first draw call. Prefer
`SpriteSheet.loadIndexed(...)` for one-call setup.
**Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1.
Index 0 is always transparent and is discarded by the fragment shader. The final palette
lookup is `storedIndex + paletteOffset`, so:
- `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`.
`palette[0]` is never reachable because stored indices start at 1.
- `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored
at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`,
and so on. Use this for palette-swap effects such as team colors or damage flashes.
**Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to
the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's
robust buffer access returns 0 for every component; because the fragment shader forces alpha
to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative
JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also
produces out-of-bounds black pixels.drawSprite(this.Demo.sheet: SpriteSheet | nullsheet, this.Demo.charRect: Rect2i | nullcharRect, new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(10, const baseY: 260baseY + 18), const BLOCK_DAYNIGHT: 12BLOCK_DAYNIGHT * const n: numbern);
// Progress bar showing time of day. The bar stays hand-drawn (a kit meter shows a
// 0..1 fill, not a moving marker) but its track and outline use theme colors.
const const barX: 60barX = 60;
const const barY: numberbarY = const baseY: 260baseY + 30;
const const barWidth: 240barWidth = 240;
const const barHeight: 10barHeight = 10;
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(const barX: 60barX, const barY: numberbarY, const barWidth: 240barWidth, const barHeight: 10barHeight), this.Demo.theme: nulltheme.panel);
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;
... 106 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 indicatorX: anyindicatorX = const barX: 60barX + Math.floor(const barWidth: 240barWidth * const cycle: numbercycle);
// The indicator rectangle uses the current day/night color (block 12, first color).
// We compute the actual index: COLOR_BASE + 12 * n + 0 = first slot in the day/night block.
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(const indicatorX: anyindicatorX - 2, const barY: numberbarY - 2, 4, const barHeight: 10barHeight + 4), const COLOR_BASE: 12COLOR_BASE + const BLOCK_DAYNIGHT: 12BLOCK_DAYNIGHT * const n: numbern);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRect: (rect: Rect2i, paletteIndex: number) => voidDraws an unfilled rectangle outline.drawRect(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(const barX: 60barX, const barY: numberbarY, const barWidth: 240barWidth, const barHeight: 10barHeight), this.Demo.theme: nulltheme.border);
// Phase labels.
import uiui.caption(const barX: 60barX, const barY: numberbarY + 14, 'Day', { color: stringcolor: 'dim' });
import uiui.caption(const barX: 60barX + 60, const barY: numberbarY + 14, 'Sunset', { color: stringcolor: 'dim' });
import uiui.caption(const barX: 60barX + 120, const barY: numberbarY + 14, 'Night', { color: stringcolor: 'dim' });
import uiui.caption(const barX: 60barX + 180, const barY: numberbarY + 14, 'Dawn', { 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 DemoDemonstrates palette-offset based sprite effects.
Static effects are pre-built in init(); dynamic effects update in update().Demo);