/*
Reduced Motion: a warp starfield that damps itself when the viewer asks for less motion.
@description A warp starfield whose speed, streaks, shake, and blinking all damp for reduced motion.
Part of the BLIT386 demo series.
Prerequisites:
- Basics: https://demos.blit386.dev/basics
- Starfield: https://demos.blit386.dev/starfield
Live version: https://demos.blit386.dev/reduced-motion
Guides:
- Splash: https://blit386.dev/docs/guides/splash#reduced-motion
- Game loop: https://blit386.dev/docs/guides/game-loop#smoothing-motion-between-updates
Reference:
- Core: https://blit386.dev/docs/api/core#reduced-motion
- Camera: https://blit386.dev/docs/api/camera
- Easing: https://blit386.dev/docs/api/easing#applyeasing
---
WHAT YOU WILL SEE
-----------------
A 320x240 cockpit window looking straight down the flight path: a two-tone frame around
the screen and an instrument housing in the top-right corner with a 12x12 lamp labeled
"WARP", and under it the current phase and the jump count ("CRUISE x0"). 140 stars
drift out from the center. The flight cycles through four phases: cruise (2.5 s), jump
(0.75 s), warp (2 s), and drop (0.75 s). At the jump the stars speed up and stretch into
long radial streaks, the hull kicks sideways by up to 5 pixels, and the lamp starts
blinking twice a second. The drop reverses it and the cycle repeats.
Three UI kit panels sit around the window: "Reduced motion" (top-left) shows the OS
preference, the local override, the effective state, and a counter of change-hook calls;
"Controls" under it holds the two controls; "When reduced" (bottom-right) lists the four
damped effects. Everything sits clear of the engine overlay's bars, so opening it with
the backquote key hides nothing.
When reduced motion is preferred, the demo keeps flying but damps every effect: stars at
20 percent speed, no streaks, no shake, and a lamp that stays lit instead of blinking. It
never freezes, because a frozen screen reads as a crash.
WHAT YOU WILL LEARN
-------------------
- BT.isReducedMotionPreferred resolves the ?reducedmotion / ?noreducedmotion URL flags
over the browser's prefers-reduced-motion media query. The demo reads it once in init().
- onReducedMotionChange(prefersReduced) on the demo class is called by the engine when the
preference changes while the page is open. It reports changes only, never the initial
value.
- Perspective projection: dividing a star's offset by its depth turns a flat list of
points into a 3D tunnel, and the outward acceleration falls out of that division.
- Deriving a streak from speed: the tail is the star's position STREAK_TICKS ticks ago, so
one speed number controls both the motion and the streak length.
- BT.renderAlpha blends each star's previous and current depth so the field moves smoothly
between fixed updates.
- Screen shake as a camera offset (BT.cameraSet), and why BT.cameraReset() must run before
the UI kit draws.
- applyEasing(t, 'cubic-in-out') for the speed ramps at the jump and the drop.
- Degrading motion instead of deleting it: slower, shorter, steadier - not stopped.
- overlayStyle in configure() picks the engine overlay's colors from the palette.
HOW THIS DIFFERS FROM THE STARFIELD DEMO
----------------------------------------
The Starfield demo (https://demos.blit386.dev/starfield) fakes depth: three flat layers
scrolling sideways at three speeds. This demo computes depth instead. Each star has a real
distance, and the screen position is worked out by dividing by it, so a star speeds up on
its own as it gets closer. Nothing in the code adds that acceleration - it falls out of
the division.
TRY THIS
--------
- Press M (or tap "M Force reduced") to force reduced motion on without touching the OS.
The checkbox can only add reduced motion on top of the system setting, never remove it.
- Press J (or tap "J Jump / drop") to jump to warp or drop back out right now. The
button does nothing while a jump or drop is already in progress.
- Press the backquote key (`) or click the small icon in the bottom-left corner to open
the engine overlay with the frame rate and timings.
- Turn the system's reduce-motion setting on or off while the page is open and watch the
"Hook fired" counter and the "Last hook" line change. No reload is needed.
- Add ?reducedmotion or ?noreducedmotion to the URL to start in either state. A URL flag
pins the answer, so the change hook stays quiet while it is present.
The engine's own startup splash follows the same preference by itself: it holds a still
frame instead of fading and dissolving.
*/
import { function applyEasing(t: number, easing: EasingFunction): numberApplies an easing curve to a normalized time value.applyEasing, function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>One-liner bootstrap function for BLIT386 demos.
Handles canvas retrieval and engine initialization. Backend selection
(WebGPU or software fallback) is managed internally by BTAPI.
This function provides a streamlined way to start a demo with sensible defaults
while allowing customization through options.bootstrap, const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT, class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32, class Rect2iInteger 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, 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 } from 'blit386';
import { import applyThemeapplyTheme, import THEME_OVERLAY_STYLETHEME_OVERLAY_STYLE, import uiui } from './shared/ui.js';
/** @typedef {import('blit386').Palette} Palette */
/** @typedef {{ x: number, y: number, z: number, prevZ: number, speedScale: number }} Star */
// Logical screen size in game pixels, and the vanishing point everything flies out of.
const const DISPLAY_W: 320DISPLAY_W = 320;
const const DISPLAY_H: 240DISPLAY_H = 240;
const const CENTER_X: numberCENTER_X = const DISPLAY_W: 320DISPLAY_W / 2;
const const CENTER_Y: numberCENTER_Y = const DISPLAY_H: 240DISPLAY_H / 2;
// Palette slots for the scene. The shared UI kit owns slots 240-251 (see applyTheme in
// init()), so scene colors live down at the bottom where they can never collide.
const const C_BG: 2C_BG = 2; // deep space: almost black, faintly blue
const const C_STAR_FAR: 3C_STAR_FAR = 3; // dim gray: stars still a long way off
const const C_STAR_MID: 4C_STAR_MID = 4; // middle distance
const const C_STAR_NEAR: 5C_STAR_NEAR = 5; // near white: about to fly past the window
const const C_FRAME: 6C_FRAME = 6; // cockpit window frame, lit edge
const const C_FRAME_DARK: 7C_FRAME_DARK = 7; // cockpit window frame, shadowed inner edge
const const C_HULL: 8C_HULL = 8; // the instrument housing the lamp sits in
const const C_LAMP_ON: 9C_LAMP_ON = 9; // lamp lit: amber, never a saturated red (see the flash note below)
const const C_LAMP_DIM: 10C_LAMP_DIM = 10; // lamp unlit: a dark amber so the bulb is still visible
const const C_LAMP_RIM: 11C_LAMP_RIM = 11; // lamp bezel and the small "WARP" label
// How many stars fly at once. Enough to feel dense at warp, few enough that every one can
// afford to draw a line instead of a dot.
const const STAR_COUNT: 140STAR_COUNT = 140;
// The depth range. Stars are born at Z_FAR and rush toward the cockpit; Z_NEAR is the
// point where they have flown past and get recycled to the back of the queue.
const const Z_NEAR: 0.08Z_NEAR = 0.08;
const const Z_FAR: 1.6Z_FAR = 1.6;
// How strongly depth spreads stars apart on screen. Bigger means a wider, faster tunnel.
const const PROJECT_SCALE: 150PROJECT_SCALE = 150;
// How far off-center a star is born, in world units. Never zero: a star sitting exactly
// on the vanishing point would stay a single motionless pixel in the middle forever.
const const SPAWN_RADIUS_MIN: 0.06SPAWN_RADIUS_MIN = 0.06;
const const SPAWN_RADIUS_MAX: 0.9SPAWN_RADIUS_MAX = 0.9;
// Per-star speed variety, as a multiplier on the ship's speed.
const const SPEED_SCALE_MIN: 0.85SPEED_SCALE_MIN = 0.85;
const const SPEED_SCALE_MAX: 1.15SPEED_SCALE_MAX = 1.15;
// How much depth the ship eats per tick while cruising, and while at warp. The whole
// "speed" of the demo is this one number; everything else is derived from it.
const const SPEED_CRUISE: 0.004SPEED_CRUISE = 0.004;
const const SPEED_WARP: 0.055SPEED_WARP = 0.055;
// The four phases of the flight, and how many ticks (1/60 second each) each one lasts.
const const PHASE_CRUISE: "cruise"PHASE_CRUISE = 'cruise';
const const PHASE_JUMP: "jump"PHASE_JUMP = 'jump';
const const PHASE_WARP: "warp"PHASE_WARP = 'warp';
const const PHASE_DROP: "drop"PHASE_DROP = 'drop';
const const PHASE_TICKS: {
cruise: number;
jump: number;
warp: number;
drop: number;
}
PHASE_TICKS = {
[const PHASE_CRUISE: "cruise"PHASE_CRUISE]: 150,
[const PHASE_JUMP: "jump"PHASE_JUMP]: 45,
[const PHASE_WARP: "warp"PHASE_WARP]: 120,
[const PHASE_DROP: "drop"PHASE_DROP]: 45,
};
// A streak is the same star drawn twice: once where it is, once where it was a moment
// ago. This is how many ticks "a moment ago" means. Because the gap is measured in ticks
// of the current speed, cruising gives a dot and warp gives a long line - one number,
// both looks.
const const STREAK_TICKS: 2STREAK_TICKS = 2;
// Shorter than this on screen, the "line" is not worth drawing - use a single pixel.
const const STREAK_MIN_PX: 1.5STREAK_MIN_PX = 1.5;
// When reduced motion is on, the ship still flies, at a fifth of the speed.
const const REDUCED_SPEED_SCALE: 0.2REDUCED_SPEED_SCALE = 0.2;
// Screen shake: a kick this big, multiplied down each tick until it is small enough to
// stop.
const const SHAKE_START_PX: 5SHAKE_START_PX = 5;
const const SHAKE_DECAY: 0.86SHAKE_DECAY = 0.86;
const const SHAKE_STOP_PX: 0.35SHAKE_STOP_PX = 0.35;
// The warp lamp blinks once every 30 ticks: on for 15, off for 15. At 60 ticks a second
// that is 2 flashes a second. The accessibility limit is 3 flashes a second (WCAG 2.1,
// "Three Flashes or Below Threshold"), because faster flashing can trigger seizures. Two
// is comfortably under it, and the lamp is a 12x12 patch of a 320x240 screen. Never flash
// the whole screen, and never flash saturated red: both have stricter limits still.
const const LAMP_PERIOD_TICKS: 30LAMP_PERIOD_TICKS = 30;
const const LAMP_ON_TICKS: 15LAMP_ON_TICKS = 15;
// Screen layout. The engine overlay (toggled with the backquote key) covers the top 42
// rows and the bottom 14 when it is open, so everything the reader needs sits between
// those bands, MARGIN pixels in from the cockpit frame's sides. The left column holds the
// status panel with the controls under it; the right column holds the instrument housing
// at the top and the legend at the bottom. The starfield shows through the middle.
const const BAND_TOP: 46BAND_TOP = 46;
const const BAND_BOTTOM: 223BAND_BOTTOM = 223;
const const MARGIN: 8MARGIN = 8;
const const PANEL_GAP: 6PANEL_GAP = 6;
const const LEFT_PANEL_W: 150LEFT_PANEL_W = 150;
const const STATUS_PANEL_H: 87STATUS_PANEL_H = 87;
const const CONTROLS_Y: numberCONTROLS_Y = const BAND_TOP: 46BAND_TOP + const STATUS_PANEL_H: 87STATUS_PANEL_H + const PANEL_GAP: 6PANEL_GAP;
const const LEGEND_W: 120LEGEND_W = 120;
const const LEGEND_H: 73LEGEND_H = 73;
const const LEGEND_X: numberLEGEND_X = const DISPLAY_W: 320DISPLAY_W - const MARGIN: 8MARGIN - const LEGEND_W: 120LEGEND_W;
const const LEGEND_Y: numberLEGEND_Y = const BAND_BOTTOM: 223BAND_BOTTOM - const LEGEND_H: 73LEGEND_H;
// The status panel's value column starts after this many characters, so the longest key,
// "Hook fired", keeps a space after it.
const const STATUS_KV_COLS: 11STATUS_KV_COLS = 11;
// The instrument housing in the top-right corner: the warp lamp with its "WARP" label,
// and under them the phase and the jump count, printed on the hull like cockpit readouts.
const const HOUSING_RECT: Rect2iHOUSING_RECT = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(const DISPLAY_W: 320DISPLAY_W - const MARGIN: 8MARGIN - 74, const BAND_TOP: 46BAND_TOP, 74, 36);
const const LAMP_RECT: Rect2iLAMP_RECT = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(const HOUSING_RECT: Rect2iHOUSING_RECT.Rect2i.x: numberLeft edge X coordinate (defaults to 0).x + 6, const HOUSING_RECT: Rect2iHOUSING_RECT.Rect2i.y: numberTop edge Y coordinate (defaults to 0).y + 5, 12, 12);
const const LAMP_LABEL_X: numberLAMP_LABEL_X = const LAMP_RECT: Rect2iLAMP_RECT.Rect2i.x: numberLeft edge X coordinate (defaults to 0).x + 18;
const const LAMP_LABEL_Y: numberLAMP_LABEL_Y = const LAMP_RECT: Rect2iLAMP_RECT.Rect2i.y: numberTop edge Y coordinate (defaults to 0).y + 2;
const const READOUT_X: numberREADOUT_X = const LAMP_RECT: Rect2iLAMP_RECT.Rect2i.x: numberLeft edge X coordinate (defaults to 0).x;
const const READOUT_Y: numberREADOUT_Y = const LAMP_RECT: Rect2iLAMP_RECT.Rect2i.y: numberTop edge Y coordinate (defaults to 0).y + 18;
const const READOUT_RIGHT_X: numberREADOUT_RIGHT_X = const HOUSING_RECT: Rect2iHOUSING_RECT.Rect2i.x: numberLeft edge X coordinate (defaults to 0).x + const HOUSING_RECT: Rect2iHOUSING_RECT.Rect2i.width: numberWidth in pixels (defaults to 0).width - 6;
// Every character of the system font is 6 pixels wide; its letters fill 5 of them.
const const FONT_W: 6FONT_W = 6;
// Depth thresholds picking which of the three star colors to draw with.
const const BAND_FAR_Z: 1BAND_FAR_Z = 1.0;
const const BAND_MID_Z: 0.4BAND_MID_Z = 0.4;
// A star is recycled once it has flown this far past the edge of the window.
const const CULL_MARGIN_PX: 12CULL_MARGIN_PX = 12;
// A warp-speed cockpit whose motion damps itself when reduced motion is preferred.
class class DemoDemo {
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = null;
// Slot numbers of the shared UI kit colors, filled in init() by applyTheme().
Demo.theme: nulltheme = null;
// One plain object per star: `x` and `y` are the sideways and up/down offsets in
// world units, fixed until the star respawns; `z` is the distance from the cockpit,
// shrinking every tick; `prevZ` is where z was at the start of this tick, for
// interpolated drawing; `speedScale` is a small per-star multiplier so the field does
// not move in lockstep.
/** @type {Star[]} */
Demo.stars: {}stars = [];
// Where the flight is in the cruise / jump / warp / drop cycle, and how long it has
// been there.
Demo.phase: stringphase = const PHASE_CRUISE: "cruise"PHASE_CRUISE;
Demo.phaseTicks: numberphaseTicks = 0;
// Depth eaten per tick right now. Recomputed every tick from the phase.
Demo.warpSpeed: numberwarpSpeed = const SPEED_CRUISE: 0.004SPEED_CRUISE;
// How many times the ship has jumped to warp, so the reader can see the button
// worked.
Demo.jumpCount: numberjumpCount = 0;
// The operating system's answer, read ONCE in init() and kept current by the change
// hook below. It is deliberately not read every frame: each read of
// BT.isReducedMotionPreferred re-parses the URL flags and re-queries the media query,
// which is wasted work sixty times a second.
Demo.osPrefersReduced: booleanosPrefersReduced = false;
// The M key / checkbox. It can only force reduced motion ON, never off: no demo
// should offer a control that overrides someone's accessibility setting against them.
Demo.forceReduced: booleanforceReduced = false;
// osPrefersReduced OR forceReduced. Recomputed at the top of every update().
Demo.reducedNow: booleanreducedNow = false;
// Proof that onReducedMotionChange() really fires. Starting at zero is correct: the
// engine only reports changes, so nothing is delivered until the setting actually
// moves.
Demo.hookCallCount: numberhookCallCount = 0;
/** @type {boolean | null} */
Demo.lastHookValue: boolean | nulllastHookValue = null;
/** @type {number | null} */
Demo.lastHookTick: number | nulllastHookTick = null;
// Shake state. shakeAmount is the fading strength; shakeX/shakeY is the whole-number
// pixel offset actually handed to the camera this tick.
Demo.shakeAmount: numbershakeAmount = 0;
Demo.shakeX: numbershakeX = 0;
Demo.shakeY: numbershakeY = 0;
// Counts 0..LAMP_PERIOD_TICKS-1 forever; the lamp is lit for the first half.
Demo.lampTick: numberlampTick = 0;
// Reused objects. Creating a new Vector2i per star per frame would make hundreds of
// throwaway objects a second, which is exactly what a retro engine cannot afford.
Demo.cameraVec: Vector2icameraVec = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(0, 0);
Demo.headVec: Vector2iheadVec = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(0, 0);
Demo.tailVec: Vector2itailVec = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(0, 0);
Demo.tempRect: Rect2itempRect = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(0, 0, 0, 0);
// Colors the engine overlay (toggled with the backquote key) from the shared UI
// theme. Its default colors live in palette slots 1 and 2, which here are empty and
// deep space - black bars with black text.
Demo.configure(): {
overlayStyle: any;
}
configure() {
return {
overlayStyle: anyoverlayStyle: import THEME_OVERLAY_STYLETHEME_OVERLAY_STYLE,
};
}
// Builds the palette, fills the sky, and asks once whether this viewer wants less
// motion.
async Demo.init(): unknowninit() {
this.Demo.palette: Palette | nullpalette = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteCreate: (size?: number) => PaletteCreates a standalone palette instance.paletteCreate(256);
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_BG: 2C_BG, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(3, 5, 14));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_STAR_FAR: 3C_STAR_FAR, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(96, 104, 124));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_STAR_MID: 4C_STAR_MID, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(168, 180, 200));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_STAR_NEAR: 5C_STAR_NEAR, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(236, 242, 255));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_FRAME: 6C_FRAME, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(92, 104, 132));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_FRAME_DARK: 7C_FRAME_DARK, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(38, 46, 64));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_HULL: 8C_HULL, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(52, 60, 80));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_LAMP_ON: 9C_LAMP_ON, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(255, 176, 58));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_LAMP_DIM: 10C_LAMP_DIM, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(74, 44, 16));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_LAMP_RIM: 11C_LAMP_RIM, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(126, 102, 70));
// Fill the sky. Each star gets its own starting depth spread across the whole
// range, so the very first frame already looks like a tunnel rather than a wall
// of stars all arriving together.
this.Demo.stars: {}stars = [];
for (let let i: numberi = 0; let i: numberi < const STAR_COUNT: 140STAR_COUNT; let i: numberi++) {
this.Demo.stars: {}stars.push(this.Demo.createStar(z: any): {
x: number;
y: number;
z: any;
prevZ: any;
speedScale: number;
}
createStar(const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.float(min: number, max: number): numberReturns the next pseudo-random float in [min, max).float(const Z_NEAR: 0.08Z_NEAR, const Z_FAR: 1.6Z_FAR)));
}
// Install the shared UI kit colors in slots 240-251, then activate the palette.
// applyTheme() must run BEFORE paletteSet(), or the kit would draw with whatever
// happened to be in those slots.
this.Demo.theme: nulltheme = import applyThemeapplyTheme(this.Demo.palette: Palettepalette);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteSet: (palette: Palette) => voidStores the active engine palette.
Use this to swap the **entire palette** (e.g. switch between a day and night
theme). After this call the renderer uploads the new palette uniform on the
next frame.
**Palette-value swap (change what a slot looks like):** mutate the live
{@link
BT.palette
}
in place with `palette.set(slot, newColor)`. The renderer
uploads dirty slots on the next frame; no `paletteSet()` or
{@link
BT.spritesRefresh
}
needed.
**Palette-layout swap (same colors, different slot positions):** build a new
palette with the same colors at new indices, call `paletteSet()`, then call
{@link
BT.spritesRefresh
}
so every sprite sheet re-maps its original RGBA
pixels against the new slot layout.paletteSet(this.Demo.palette: Palettepalette);
// Ask the browser once. From here on, onReducedMotionChange() keeps this current
// - the engine installs its media-query listener right after this init() resolves
// true.
this.Demo.osPrefersReduced: booleanosPrefersReduced = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.isReducedMotionPreferred: booleanWhether reduced motion is currently preferred.
Resolves `window.matchMedia('(prefers-reduced-motion: reduce)')`, or the
`?reducedmotion` / `?noreducedmotion` URL flags when either is present. Pair with
{@link
IBTDemo.onReducedMotionChange
}
to react when the preference changes at runtime -
it is not fixed for the session, the same way
{@link
BT.screenOrientation
}
is not.isReducedMotionPreferred;
this.Demo.reducedNow: booleanreducedNow = this.Demo.osPrefersReduced: booleanosPrefersReduced;
this.Demo.enterPhase(phase: any): voidenterPhase(const PHASE_CRUISE: "cruise"PHASE_CRUISE);
return true;
}
// Fixed-step logic: advance the flight, move every star, fade the shake, blink the
// lamp.
Demo.update(): voidupdate() {
// First line, always: this is what lets the kit see key presses and taps.
import uiui.tick();
// The OS setting and the local override are combined here, once, and every piece
// of motion below reads the result. One switch, one place to change it.
this.Demo.reducedNow: booleanreducedNow = this.Demo.osPrefersReduced: booleanosPrefersReduced || this.Demo.forceReduced: booleanforceReduced;
this.Demo.advancePhase(): voidadvancePhase();
this.Demo.moveStars(): voidmoveStars();
this.Demo.updateShake(): voidupdateShake();
// % wraps the counter back to 0 when it reaches the period, like a clock hand.
this.Demo.lampTick: numberlampTick = (this.Demo.lampTick: numberlampTick + 1) % const LAMP_PERIOD_TICKS: 30LAMP_PERIOD_TICKS;
}
// Draws the sky and hull under the shake, then the interface at rest on top.
Demo.render(): voidrender() {
// clear() paints the whole display and ignores the camera, so shaking the scene
// can never expose an unpainted strip along an edge.
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.clear: (paletteIndex: number) => voidSets the frame clear color using a palette index.
The renderer uses this color when clearing the full display at the start
of the next frame.clear(const C_BG: 2C_BG);
// Screen shake done properly: nudge the camera, not every single draw call. A
// camera offset of -3 moves everything drawn after it 3 pixels the other way, so
// nothing else in this method has to know the shake exists.
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.cameraSet: (offset: Vector2i) => voidSets the global camera offset applied to subsequent draw calls.cameraSet(this.Demo.cameraVec: Vector2icameraVec.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(-this.Demo.shakeX: numbershakeX, -this.Demo.shakeY: numbershakeY));
this.Demo.drawStars(): voiddrawStars();
this.Demo.drawHull(): voiddrawHull();
this.Demo.drawLamp(): voiddrawLamp();
// Put the camera back BEFORE any ui.* call. The kit draws its panels through the
// same camera-aware calls, but it decides what a finger hit using raw screen
// positions - so a shaking camera would slide the buttons away from their own tap
// targets.
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.cameraReset: () => voidResets the global camera offset to `(0, 0)`.cameraReset();
this.Demo.drawStatusPanel(): voiddrawStatusPanel();
this.Demo.drawControlPanel(): voiddrawControlPanel();
this.Demo.drawLegendPanel(): voiddrawLegendPanel();
}
// The engine calls this when the system's reduce-motion setting changes while the
// page is open. It fires only on a change - never once at startup with the current
// value - which is why init() reads BT.isReducedMotionPreferred for the starting
// answer. `prefersReduced` is true when the viewer now wants less motion.
Demo.onReducedMotionChange(prefersReduced: any): voidonReducedMotionChange(prefersReduced: anyprefersReduced) {
this.Demo.osPrefersReduced: booleanosPrefersReduced = prefersReduced: anyprefersReduced;
// Everything below is only here so the demo can show that the hook really fired.
// A real game would just keep the line above.
this.Demo.hookCallCount: numberhookCallCount += 1;
this.Demo.lastHookValue: boolean | nulllastHookValue = prefersReduced: anyprefersReduced;
this.Demo.lastHookTick: number | nulllastHookTick = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.ticks: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks;
}
// Makes one star at depth `z`: a random direction out from the middle, a random
// distance along it, and a slightly random speed.
Demo.createStar(z: any): {
x: number;
y: number;
z: any;
prevZ: any;
speedScale: number;
}
createStar(z: anyz) {
// BT.random.angle() picks a direction anywhere around a full turn. Cosine and
// sine turn that direction plus a distance into a sideways and an up/down offset
// - the standard way to scatter points evenly around a circle instead of in a
// square.
const const angle: numberangle = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.angle(): numberReturns a uniform angle in [0, 2π) radians.angle();
const const radius: numberradius = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.float(min: number, max: number): numberReturns the next pseudo-random float in [min, max).float(const SPAWN_RADIUS_MIN: 0.06SPAWN_RADIUS_MIN, const SPAWN_RADIUS_MAX: 0.9SPAWN_RADIUS_MAX);
return {
x: numberx: Math.cos(const angle: numberangle) * const radius: numberradius,
y: numbery: Math.sin(const angle: numberangle) * const radius: numberradius,
z: anyz,
prevZ: anyprevZ: z: anyz,
speedScale: numberspeedScale: const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.float(min: number, max: number): numberReturns the next pseudo-random float in [min, max).float(const SPEED_SCALE_MIN: 0.85SPEED_SCALE_MIN, const SPEED_SCALE_MAX: 1.15SPEED_SCALE_MAX),
};
}
// Sends one star back to the far end of the tunnel with a fresh direction.
Demo.respawnStar(star: any): voidrespawnStar(star: anystar) {
const const angle: numberangle = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.angle(): numberReturns a uniform angle in [0, 2π) radians.angle();
const const radius: numberradius = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.float(min: number, max: number): numberReturns the next pseudo-random float in [min, max).float(const SPAWN_RADIUS_MIN: 0.06SPAWN_RADIUS_MIN, const SPAWN_RADIUS_MAX: 0.9SPAWN_RADIUS_MAX);
star: anystar.x = Math.cos(const angle: numberangle) * const radius: numberradius;
star: anystar.y = Math.sin(const angle: numberangle) * const radius: numberradius;
star: anystar.z = const Z_FAR: 1.6Z_FAR;
// Snap the remembered depth to the new one as well. Without this, the
// interpolated drawing in drawStars() would blend from the star's old position
// right next to the cockpit all the way back to the far distance, painting a
// streak across the entire window on the frame it respawns.
star: anystar.prevZ = const Z_FAR: 1.6Z_FAR;
}
// Walks the flight through cruise -> jump -> warp -> drop and back, and sets the
// speed.
Demo.advancePhase(): voidadvancePhase() {
this.Demo.phaseTicks: numberphaseTicks += 1;
if (this.Demo.phaseTicks: numberphaseTicks >= const PHASE_TICKS: {
cruise: number;
jump: number;
warp: number;
drop: number;
}
PHASE_TICKS[this.Demo.phase: stringphase]) {
if (this.Demo.phase: stringphase === const PHASE_CRUISE: "cruise"PHASE_CRUISE) {
this.Demo.enterPhase(phase: any): voidenterPhase(const PHASE_JUMP: "jump"PHASE_JUMP);
} else if (this.Demo.phase: stringphase === const PHASE_JUMP: "jump"PHASE_JUMP) {
this.Demo.enterPhase(phase: any): voidenterPhase(const PHASE_WARP: "warp"PHASE_WARP);
} else if (this.Demo.phase: stringphase === const PHASE_WARP: "warp"PHASE_WARP) {
this.Demo.enterPhase(phase: any): voidenterPhase(const PHASE_DROP: "drop"PHASE_DROP);
} else {
this.Demo.enterPhase(phase: any): voidenterPhase(const PHASE_CRUISE: "cruise"PHASE_CRUISE);
}
}
// Base speed for this phase. The two transitions slide between the two speeds
// instead of snapping: t counts 0 to 1 across the phase, and applyEasing() bends
// that straight count into a slow start, fast middle, slow finish.
let let speed: numberspeed = this.Demo.phase: stringphase === const PHASE_WARP: "warp"PHASE_WARP ? const SPEED_WARP: 0.055SPEED_WARP : const SPEED_CRUISE: 0.004SPEED_CRUISE;
if (this.Demo.phase: stringphase === const PHASE_JUMP: "jump"PHASE_JUMP || this.Demo.phase: stringphase === const PHASE_DROP: "drop"PHASE_DROP) {
const const t: numbert = this.Demo.phaseTicks: numberphaseTicks / const PHASE_TICKS: {
cruise: number;
jump: number;
warp: number;
drop: number;
}
PHASE_TICKS[this.Demo.phase: "jump" | "drop"phase];
const const eased: numbereased = function applyEasing(t: number, easing: EasingFunction): numberApplies an easing curve to a normalized time value.applyEasing(const t: numbert, 'cubic-in-out');
let speed: numberspeed =
this.Demo.phase: "jump" | "drop"phase === const PHASE_JUMP: "jump"PHASE_JUMP
? const SPEED_CRUISE: 0.004SPEED_CRUISE + (const SPEED_WARP: 0.055SPEED_WARP - const SPEED_CRUISE: 0.004SPEED_CRUISE) * const eased: numbereased
: const SPEED_WARP: 0.055SPEED_WARP + (const SPEED_CRUISE: 0.004SPEED_CRUISE - const SPEED_WARP: 0.055SPEED_WARP) * const eased: numbereased;
}
// The first degradation: reduced motion keeps the whole flight, at a fifth of the
// speed. Slowed, not stopped - a frozen starfield looks like the demo has
// crashed.
this.Demo.warpSpeed: numberwarpSpeed = this.Demo.reducedNow: booleanreducedNow ? let speed: numberspeed * const REDUCED_SPEED_SCALE: 0.2REDUCED_SPEED_SCALE : let speed: numberspeed;
}
// Switches to `phase` (one of PHASE_CRUISE, PHASE_JUMP, PHASE_WARP, PHASE_DROP), and
// kicks the hull on the two moments where that makes sense.
Demo.enterPhase(phase: any): voidenterPhase(phase: anyphase) {
this.Demo.phase: stringphase = phase: anyphase;
this.Demo.phaseTicks: numberphaseTicks = 0;
// Crossing into or out of warp is a jolt; cruising along and holding at warp are
// not.
if (phase: anyphase === const PHASE_JUMP: "jump"PHASE_JUMP || phase: anyphase === const PHASE_DROP: "drop"PHASE_DROP) {
this.Demo.shakeAmount: numbershakeAmount = const SHAKE_START_PX: 5SHAKE_START_PX;
}
if (phase: anyphase === const PHASE_JUMP: "jump"PHASE_JUMP) {
this.Demo.jumpCount: numberjumpCount += 1;
}
}
// Pulls every star closer by this tick's speed, recycling the ones that have flown
// past.
Demo.moveStars(): voidmoveStars() {
for (let let i: numberi = 0; let i: numberi < this.Demo.stars: {}stars.length; let i: numberi++) {
const const star: anystar = this.Demo.stars: {}stars[let i: numberi];
// Remember where the star was, so render() can draw the in-between position.
const star: anystar.prevZ = const star: anystar.z;
// Smaller z means closer. This is the only movement in the demo: while a star
// is alive nothing changes its x or y. The sideways rush you see is entirely
// the division by a shrinking depth over in drawStars().
const star: anystar.z -= this.Demo.warpSpeed: numberwarpSpeed * const star: anystar.speedScale;
if (const star: anystar.z <= const Z_NEAR: 0.08Z_NEAR || this.Demo.isStarOffScreen(star: any): booleanisStarOffScreen(const star: anystar)) {
this.Demo.respawnStar(star: any): voidrespawnStar(const star: anystar);
}
}
}
// Returns true when the star has flown out of the window. Checked so no star is ever
// drawn - or worse, drawn as a very long line - far outside the screen.
Demo.isStarOffScreen(star: any): booleanisStarOffScreen(star: anystar) {
const const sx: numbersx = const CENTER_X: numberCENTER_X + (star: anystar.x / star: anystar.z) * const PROJECT_SCALE: 150PROJECT_SCALE;
const const sy: numbersy = const CENTER_Y: numberCENTER_Y + (star: anystar.y / star: anystar.z) * const PROJECT_SCALE: 150PROJECT_SCALE;
return (
const sx: numbersx < -const CULL_MARGIN_PX: 12CULL_MARGIN_PX ||
const sx: numbersx > const DISPLAY_W: 320DISPLAY_W + const CULL_MARGIN_PX: 12CULL_MARGIN_PX ||
const sy: numbersy < -const CULL_MARGIN_PX: 12CULL_MARGIN_PX ||
const sy: numbersy > const DISPLAY_H: 240DISPLAY_H + const CULL_MARGIN_PX: 12CULL_MARGIN_PX
);
}
// Fades the shake toward nothing, then turns it into whole pixels - or zero, if
// damped.
Demo.updateShake(): voidupdateShake() {
// Multiplying by a number just under 1 every tick makes the kick die away quickly
// at first and gently later, which is how a real jolt settles. Below about a
// third of a pixel it is invisible, so snap it to nothing and stop the wobble
// cleanly.
this.Demo.shakeAmount: numbershakeAmount *= const SHAKE_DECAY: 0.86SHAKE_DECAY;
if (this.Demo.shakeAmount: numbershakeAmount < const SHAKE_STOP_PX: 0.35SHAKE_STOP_PX) {
this.Demo.shakeAmount: numbershakeAmount = 0;
}
// The second degradation: no shake at all. The jump still happens, and the panel
// still counts it - only the camera stops moving.
if (this.Demo.reducedNow: booleanreducedNow) {
this.Demo.shakeX: numbershakeX = 0;
this.Demo.shakeY: numbershakeY = 0;
return;
}
this.Demo.shakeX: numbershakeX = Math.round(const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.float(min: number, max: number): numberReturns the next pseudo-random float in [min, max).float(-this.Demo.shakeAmount: numbershakeAmount, this.Demo.shakeAmount: numbershakeAmount));
this.Demo.shakeY: numbershakeY = Math.round(const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.float(min: number, max: number): numberReturns the next pseudo-random float in [min, max).float(-this.Demo.shakeAmount: numbershakeAmount, this.Demo.shakeAmount: numbershakeAmount));
}
// Draws every star as a streak from where it is back toward where it just was.
//
// THE ONE IDEA IN THIS DEMO: dividing by depth. A star two units away is drawn half
// as far from the middle as the same star one unit away. So as depth shrinks, the
// drawn position races outward faster and faster all by itself. Think of standing
// between railway tracks: sleepers far ahead barely seem to move, the one at your
// feet whips past.
Demo.drawStars(): voiddrawStars() {
// BT.renderAlpha says how far this frame is between the last tick and the next
// one, from 0 to just under 1. Blending the old and new depth by it keeps the
// flight smooth even though the logic above only runs at a fixed 60 steps a
// second.
const const alpha: numberalpha = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.renderAlpha: numberFractional progress between the last completed fixed update and the next.
Intended for interpolating render state between fixed-update steps.renderAlpha;
// The third degradation: reduced motion drops the trail length to zero, so every
// star stays a dot at every speed. The long radial smear is the part that hurts.
const const streakTicks: 2 | 0streakTicks = this.Demo.reducedNow: booleanreducedNow ? 0 : const STREAK_TICKS: 2STREAK_TICKS;
for (let let i: numberi = 0; let i: numberi < this.Demo.stars: {}stars.length; let i: numberi++) {
const const star: anystar = this.Demo.stars: {}stars[let i: numberi];
const const z: anyz = const star: anystar.prevZ + (const star: anystar.z - const star: anystar.prevZ) * const alpha: numberalpha;
// Guard against a depth of zero: dividing by it would give infinity.
if (const z: anyz <= 0) {
continue;
}
const const scale: numberscale = const PROJECT_SCALE: 150PROJECT_SCALE / const z: anyz;
const const hx: numberhx = const CENTER_X: numberCENTER_X + const star: anystar.x * const scale: numberscale;
const const hy: numberhy = const CENTER_Y: numberCENTER_Y + const star: anystar.y * const scale: numberscale;
// Where the star was streakTicks ticks ago: the same star, further away.
// Because the gap is this tick's speed times a fixed number of ticks, the
// streak is short when the ship crawls and long when it is at warp. One
// number, two looks.
const const zTail: anyzTail = const z: anyz + this.Demo.warpSpeed: numberwarpSpeed * const star: anystar.speedScale * const streakTicks: 2 | 0streakTicks;
const const tailScale: numbertailScale = const PROJECT_SCALE: 150PROJECT_SCALE / const zTail: anyzTail;
const const tx: numbertx = const CENTER_X: numberCENTER_X + const star: anystar.x * const tailScale: numbertailScale;
const const ty: numberty = const CENTER_Y: numberCENTER_Y + const star: anystar.y * const tailScale: numbertailScale;
const const slot: 3 | 4 | 5slot = this.Demo.starSlot(z: any): 3 | 4 | 5starSlot(const z: anyz);
// A "line" shorter than a pixel and a half is just a dot with extra work.
if (Math.abs(const hx: numberhx - const tx: numbertx) < const STREAK_MIN_PX: 1.5STREAK_MIN_PX && Math.abs(const hy: numberhy - const ty: numberty) < const STREAK_MIN_PX: 1.5STREAK_MIN_PX) {
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawPixel: (posOrX: Vector2i | number, yOrColor: number, maybeColor?: number) => voidDraws a single pixel.
Accepts either:
- `(posOrX: Vector2i, yOrColor: number)` where `yOrColor` is the palette index.
- `(posOrX: number, yOrColor: number, maybeColor: number)` for `(x, y, paletteIndex)`.drawPixel(this.Demo.headVec: Vector2iheadVec.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(Math.round(const hx: numberhx), Math.round(const hy: numberhy)), const slot: 3 | 4 | 5slot);
} else {
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawLine: (p0: Vector2i, p1: Vector2i, paletteIndex: number) => voidDraws a pixel-perfect line between two points.
Uses rasterized line drawing without antialiasing.drawLine(
this.Demo.tailVec: Vector2itailVec.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(Math.round(const tx: numbertx), Math.round(const ty: numberty)),
this.Demo.headVec: Vector2iheadVec.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(Math.round(const hx: numberhx), Math.round(const hy: numberhy)),
const slot: 3 | 4 | 5slot,
);
}
}
}
// Picks a palette slot for a star from its blended depth `z`: distant stars are dim,
// close ones glare.
Demo.starSlot(z: any): 3 | 4 | 5starSlot(z: anyz) {
if (z: anyz > const BAND_FAR_Z: 1BAND_FAR_Z) {
return const C_STAR_FAR: 3C_STAR_FAR;
}
if (z: anyz > const BAND_MID_Z: 0.4BAND_MID_Z) {
return const C_STAR_MID: 4C_STAR_MID;
}
return const C_STAR_NEAR: 5C_STAR_NEAR;
}
// The cockpit window frame and the instrument housing. This is not decoration: a
// shake against a plain starfield is nearly invisible, because there is no straight
// edge to measure it against. Give the eye a rigid frame and the same 5 pixels read
// as a jolt.
Demo.drawHull(): voiddrawHull() {
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRect: (rect: Rect2i, paletteIndex: number) => voidDraws an unfilled rectangle outline.drawRect(this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(2, 2, const DISPLAY_W: 320DISPLAY_W - 4, const DISPLAY_H: 240DISPLAY_H - 4), const C_FRAME: 6C_FRAME);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRect: (rect: Rect2i, paletteIndex: number) => voidDraws an unfilled rectangle outline.drawRect(this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(4, 4, const DISPLAY_W: 320DISPLAY_W - 8, const DISPLAY_H: 240DISPLAY_H - 8), const C_FRAME_DARK: 7C_FRAME_DARK);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(const HOUSING_RECT: Rect2iHOUSING_RECT, const C_HULL: 8C_HULL);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRect: (rect: Rect2i, paletteIndex: number) => voidDraws an unfilled rectangle outline.drawRect(const HOUSING_RECT: Rect2iHOUSING_RECT, const C_FRAME: 6C_FRAME);
}
// The warp lamp. Lit while the ship is jumping or at warp, dark while cruising.
//
// The fourth degradation: normally the lamp blinks twice a second, but under reduced
// motion it simply stays lit for the same phases. Same color, same meaning, no
// flicker - which is also why the lamp is never the ONLY thing telling you the state.
// Blinking alone is a poor way to say anything: the phase readout under the lamp says
// it in words.
Demo.drawLamp(): voiddrawLamp() {
const const isWarping: booleanisWarping = this.Demo.phase: stringphase === const PHASE_JUMP: "jump"PHASE_JUMP || this.Demo.phase: stringphase === const PHASE_WARP: "warp"PHASE_WARP;
const const isLit: booleanisLit = const isWarping: booleanisWarping && (this.Demo.reducedNow: booleanreducedNow || this.Demo.lampTick: numberlampTick < const LAMP_ON_TICKS: 15LAMP_ON_TICKS);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(const LAMP_RECT: Rect2iLAMP_RECT, const isLit: booleanisLit ? const C_LAMP_ON: 9C_LAMP_ON : const C_LAMP_DIM: 10C_LAMP_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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRect: (rect: Rect2i, paletteIndex: number) => voidDraws an unfilled rectangle outline.drawRect(const LAMP_RECT: Rect2iLAMP_RECT, const C_LAMP_RIM: 11C_LAMP_RIM);
// Drawn inside the camera offset with the rest of the hull, so the labels shake
// along with the housing they are printed on instead of floating free of 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;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.systemPrint: (pos: Vector2i, paletteIndex: number, text: string) => voidDraws text using the built-in 6x14 system font.
The system font covers printable ASCII (characters 32-126). For custom
bitmap fonts with proportional glyphs, use
{@link
BT.printFont
}
instead.systemPrint(this.Demo.headVec: Vector2iheadVec.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(const LAMP_LABEL_X: numberLAMP_LABEL_X, const LAMP_LABEL_Y: numberLAMP_LABEL_Y), const C_LAMP_RIM: 11C_LAMP_RIM, 'WARP');
// The phase in capitals on the left, and the jump count right-aligned under the
// label: its letters end on READOUT_RIGHT_X.
const const jumps: stringjumps = `x${this.Demo.jumpCount: numberjumpCount}`;
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.systemPrint: (pos: Vector2i, paletteIndex: number, text: string) => voidDraws text using the built-in 6x14 system font.
The system font covers printable ASCII (characters 32-126). For custom
bitmap fonts with proportional glyphs, use
{@link
BT.printFont
}
instead.systemPrint(this.Demo.headVec: Vector2iheadVec.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(const READOUT_X: numberREADOUT_X, const READOUT_Y: numberREADOUT_Y), const C_LAMP_RIM: 11C_LAMP_RIM, this.Demo.phase: stringphase.toUpperCase());
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.systemPrint: (pos: Vector2i, paletteIndex: number, text: string) => voidDraws text using the built-in 6x14 system font.
The system font covers printable ASCII (characters 32-126). For custom
bitmap fonts with proportional glyphs, use
{@link
BT.printFont
}
instead.systemPrint(
this.Demo.headVec: Vector2iheadVec.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(const READOUT_RIGHT_X: numberREADOUT_RIGHT_X - (const jumps: stringjumps.length * const FONT_W: 6FONT_W - 1) + 1, const READOUT_Y: numberREADOUT_Y),
const C_LAMP_RIM: 11C_LAMP_RIM,
const jumps: stringjumps,
);
}
// Top-left panel: what the system said, what the override says, and proof the hook
// fires. The phase and the jump count are on the instrument housing (see drawLamp()).
Demo.drawStatusPanel(): voiddrawStatusPanel() {
import uiui.begin('topLeft', { x: numberx: const MARGIN: 8MARGIN, y: numbery: const BAND_TOP: 46BAND_TOP, width: numberwidth: const LEFT_PANEL_W: 150LEFT_PANEL_W, kvCols: numberkvCols: const STATUS_KV_COLS: 11STATUS_KV_COLS });
import uiui.panel('Reduced motion');
import uiui.kv('OS pref', this.Demo.osPrefersReduced: booleanosPrefersReduced ? 'reduce' : 'no-pref');
import uiui.kv('Override', this.Demo.forceReduced: booleanforceReduced ? 'on' : 'off');
import uiui.kv('Effective', this.Demo.reducedNow: booleanreducedNow ? 'REDUCED' : 'FULL');
import uiui.spacer(4);
// "0" here is the right answer at startup, not a bug: the hook reports changes
// only.
import uiui.kv('Hook fired', this.Demo.hookCallCount: numberhookCallCount);
import uiui.kv('Last hook', this.Demo.formatLastHook(): stringformatLastHook());
import uiui.end();
}
// Puts the most recent hook call into one short line.
Demo.formatLastHook(): stringformatLastHook() {
if (this.Demo.lastHookValue: boolean | nulllastHookValue === null) {
return 'never';
}
return `${this.Demo.lastHookValue: booleanlastHookValue ? 'reduce' : 'no-pref'} @${this.Demo.lastHookTick: number | nulllastHookTick}`;
}
// Left panel under the status: the two controls, both usable by key and by touch.
Demo.drawControlPanel(): voiddrawControlPanel() {
import uiui.begin('topLeft', { x: numberx: const MARGIN: 8MARGIN, y: numbery: const CONTROLS_Y: numberCONTROLS_Y, width: numberwidth: const LEFT_PANEL_W: 150LEFT_PANEL_W });
import uiui.panel('Controls');
// A checkbox, not a button, because this is a lasting on/off state. The { key }
// option gives it a keyboard shortcut and a tap target at once, so the demo works
// on a phone with no keyboard attached.
//
// Note the direction: this can only ADD reduced motion on top of the system
// setting. Nothing here can take it away from someone whose OS asked for it.
this.Demo.forceReduced: booleanforceReduced = import uiui.checkbox('M Force reduced', this.Demo.forceReduced: booleanforceReduced, { key: stringkey: 'KeyM' });
// A little breathing room: buttons are taller than text rows, and without a gap
// the button's top edge clips the descenders of the checkbox label above it.
import uiui.spacer(4);
if (import uiui.button('J Jump / drop', { key: stringkey: 'KeyJ', width: numberwidth: const LEFT_PANEL_W: 150LEFT_PANEL_W - 20 })) {
this.Demo.toggleWarp(): voidtoggleWarp();
}
import uiui.label('Or use the OS setting.', { color: stringcolor: 'dim' });
import uiui.end();
}
// Bottom-right panel: exactly what changes when reduced motion is on.
Demo.drawLegendPanel(): voiddrawLegendPanel() {
import uiui.begin('topLeft', { x: numberx: const LEGEND_X: numberLEGEND_X, y: numbery: const LEGEND_Y: numberLEGEND_Y, width: numberwidth: const LEGEND_W: 120LEGEND_W });
import uiui.panel('When reduced');
import uiui.label('Stars 20% speed', { color: stringcolor: 'dim' });
import uiui.label('Streaks dots only', { color: stringcolor: 'dim' });
import uiui.label('Shake none', { color: stringcolor: 'dim' });
import uiui.label('Lamp steady', { color: stringcolor: 'dim' });
import uiui.end();
}
// Jumps to warp now, or drops out now, so nobody has to wait for the cycle to come
// round. Mid-transition (jump or drop) the button does nothing: the ship is already
// busy changing speed, and interrupting it would snap the starfield.
Demo.toggleWarp(): voidtoggleWarp() {
if (this.Demo.phase: stringphase === const PHASE_CRUISE: "cruise"PHASE_CRUISE) {
this.Demo.enterPhase(phase: any): voidenterPhase(const PHASE_JUMP: "jump"PHASE_JUMP);
} else if (this.Demo.phase: stringphase === const PHASE_WARP: "warp"PHASE_WARP) {
this.Demo.enterPhase(phase: any): voidenterPhase(const PHASE_DROP: "drop"PHASE_DROP);
}
}
}
// bootstrap finds the canvas, builds the Demo, and runs the loop.
function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>One-liner bootstrap function for BLIT386 demos.
Handles canvas retrieval and engine initialization. Backend selection
(WebGPU or software fallback) is managed internally by BTAPI.
This function provides a streamlined way to start a demo with sensible defaults
while allowing customization through options.bootstrap(class DemoDemo);