/*

Audio Basics: load sounds, play them with pitch, volume, and pan, and unlock audio first.
@description Load a clip, play it with volume, pitch, and pan variation, and handle the first-gesture audio unlock.

Part of the BLIT386 demo series.

Prerequisites:
  - Pointer Basics: https://demos.blit386.dev/pointer-basics
  - Keyboard Input: https://demos.blit386.dev/keyboard-input

Live version: https://demos.blit386.dev/audio-basics

Guides:
  - Audio: https://blit386.dev/docs/guides/audio#playing-sfx

Reference:
  - Audio: https://blit386.dev/docs/api/audio#playback-sfx
  - Overlay: https://blit386.dev/docs/api/overlay#audio-meters-optional

---

WHAT YOU WILL SEE
-----------------

The engine overlay is open from the start: white bars along the top with live audio level
meters, and a hint bar along the bottom. Between them, one centered line of text reads
"Click or press a key to enable sound" in warm orange until you click or press a key;
after that it turns into a dim reminder: "Tap a button for a blip. Click the pad for a
pop."

Two panels of the same height sit side by side under that line. "Keyboard SFX (pitch)" on
the left has three buttons, "1 - Low (0.75x)", "2 - Normal (1.00x)", and "3 - High
(1.50x)", plus a "Last pitch" row. "Pointer SFX (volume/pan)" on the right holds a dark
pad labeled "Loud" at the top, "Quiet" at the bottom, and "L" and "R" on the sides, with a
faint cross through its middle. Under the pad are a "Volume" row with a meter bar and a
"Pan" row.

Press and hold inside the pad and the pop starts repeating. Drag around and it follows
you: up is louder, down is quieter, left and right move it between the speakers. A note
glyph marks where the sound sits, with a green square ring around it while you hold, and
the volume meter lights up green. Let go and the sound fades out; the note stays behind in
amber.

WHAT YOU WILL LEARN
-------------------

- AudioClip.load(url) downloads a sound file and decodes it into a ready-to-play clip. It
  works before sound is unlocked - only hearing a sound has to wait.
- BT.soundPlay(clip, { pitch }) plays a clip faster or slower than recorded.
- BT.soundPlay(clip, { loop: true }) keeps a clip repeating, and returns a handle for that
  one sound.
- BT.soundVolumeSet(handle, volume) and BT.soundPanSet(handle, pan) change a sound while
  it plays; BT.soundStop(handle, { fadeOutMs }) fades it out and stops it.
- BT.isAudioUnlocked reports whether the first click or key press has happened yet.
- BT.isPressed(BT.BTN_POINTER_A, 0) catches the moment a press starts, BT.isDown(...) is
  true for as long as it is held, and BT.pointerPos(0) says where the pointer is.
- Turning a screen position into a number in a range: clamp it into the pad, turn it into
  a fraction from 0 to 1, then stretch that over the range you want.
- isOverlayAudioMetersEnabled: true adds live bus level bars to the engine overlay, and
  overlayStyle / overlayAudioMeterStyle pick its colors from the palette.

WHY DOES SOUND WAIT FOR A CLICK?
--------------------------------

Every web browser refuses to make any sound at all until you click, tap, or press a key on
the page. This is called the "autoplay policy", and it exists so a page you just opened
cannot suddenly blast noise at you. The engine listens for that first gesture and unlocks
its audio for the rest of the session; BT.isAudioUnlocked flips to true once it has.

Not every audio call waits the same way. A sound played with BT.soundPlay() before the
unlock is simply dropped (the overlay's "drop" counter goes up by one). Music started with
BT.musicPlay() is remembered and starts the instant sound unlocks - the Music demo shows
that. Volume and mute changes are kept too, and apply as soon as sound unlocks.

PITCH, VOLUME, AND PAN
----------------------

- pitch: how fast the sound plays back. 1 is the recorded speed; 1.5 sounds faster and
  higher, like speeding up a cassette tape, and 0.75 sounds slower and deeper.
- volume: how loud the sound is, from 0 (silent) to 1 (full volume).
- pan: which speaker the sound favors, from -1 (only the left speaker) through 0
  (centered) to +1 (only the right speaker).

Here the pointer's spot in the pad picks the numbers: the top of the pad is volume 1.0 and
the bottom is 0.2, the left edge is pan -1 and the right edge is pan +1.

TRY THIS
--------

- Click anywhere, or press any key, to unlock sound - watch the line under the overlay
  change once you do.
- Press 1, 2, or 3 (or tap the matching button) to play a short blip at a low, normal, or
  high pitch. The "Last pitch" row shows which one you played.
- Press and hold near the top of the pad for a loud pop, then drag down and listen to it
  get quieter. Drag left and right to hear it move between the speakers (headphones or
  stereo speakers make this easiest to hear).
- Watch the overlay while the pop repeats: the main and sfx level bars bounce, and the
  "voices" readout counts the sounds playing at once. Press the backquote key (`) or click
  the small icon in the bottom-left corner to hide or show the overlay.

*/

import { class AudioClip
Decoded audio asset with its winning source URL and buffer-derived metadata. Construct instances with {@link AudioClip.load } , {@link AudioClip.loadAll } , or {@link AudioClip.synth } ; there is no public constructor.
@since1.3.0
AudioClip
, 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.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
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 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.
@since0.1.0
Rect2i
, class Vector2i
Integer 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.
@since0.1.0
Vector2i
} from 'blit386';
import { import applyThemeapplyTheme, import FONT_WFONT_W, import THEME_OVERLAY_AUDIO_METER_STYLETHEME_OVERLAY_AUDIO_METER_STYLE, import THEME_OVERLAY_STYLETHEME_OVERLAY_STYLE, import uiui } from './shared/ui.js'; /** @typedef {import('blit386').Palette} Palette */ /** @typedef {import('blit386').SoundRef} SoundRef */ // How many ticks the click flash stays lit. A tick is one fixed update; at the default 60 // ticks per second, 12 ticks is about 200 ms. const const FLASH_TICKS: 12FLASH_TICKS = 12; // The three pitch-preset buttons and the pitch each one plays the blip at. 1.0 is the // sound's natural pitch; smaller numbers sound lower and slower, bigger numbers higher // and faster. `label` is the button text (it doubles as the keyboard hint) and `code` is // the bound key. const const KEY_PITCH_PRESETS: {}KEY_PITCH_PRESETS = [ { code: stringcode: 'Digit1', label: stringlabel: '1 - Low (0.75x)', pitch: numberpitch: 0.75 }, { code: stringcode: 'Digit2', label: stringlabel: '2 - Normal (1.00x)', pitch: numberpitch: 1.0 }, { code: stringcode: 'Digit3', label: stringlabel: '3 - High (1.50x)', pitch: numberpitch: 1.5 }, ]; // All pitch buttons share one width so the left panel reads as a tidy keypad. const const PITCH_BUTTON_W: 120PITCH_BUTTON_W = 120; // Screen layout, in display pixels. The engine overlay is open from the start: its bars // cover the top 56 rows and its hint bar the bottom 14, so everything sits in the band // between them. The status line comes first, then two panels side by side, equally tall. const const STATUS_Y: 64STATUS_Y = 64; const const PANEL_Y: 76PANEL_Y = 76; const const LEFT_PANEL_X: 4LEFT_PANEL_X = 4; const const LEFT_PANEL_W: 140LEFT_PANEL_W = 140; const const RIGHT_PANEL_X: 148RIGHT_PANEL_X = 148; const const RIGHT_PANEL_W: 168RIGHT_PANEL_W = 168; // Panel metrics from the UI kit: content starts 22 pixels below a panel's top edge // (under its title) and 9 pixels in from its left edge, and is 20 pixels narrower than // the panel. const const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP = 22; const const PANEL_CONTENT_X: 9PANEL_CONTENT_X = 9; const const PANEL_CONTENT_SHRINK: 20PANEL_CONTENT_SHRINK = 20; // The pad inside the right panel: a dark box you press and hold to play the pop. Where // the pointer sits inside it picks the volume (top to bottom) and the pan (left to // right). const const PAD_H: 74PAD_H = 74; const const PAD_RECT: Rect2iPAD_RECT = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(
const RIGHT_PANEL_X: 148RIGHT_PANEL_X + const PANEL_CONTENT_X: 9PANEL_CONTENT_X, const PANEL_Y: 76PANEL_Y + const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP, const RIGHT_PANEL_W: 168RIGHT_PANEL_W - const PANEL_CONTENT_SHRINK: 20PANEL_CONTENT_SHRINK, const PAD_H: 74PAD_H, ); // Gap between the pad and the rows under it. const const PAD_GAP: 4PAD_GAP = 4; // Empty space between the pad's border and its edge labels, and between a label and the // end of the center cross line pointing at it. const const PAD_LABEL_GAP: 5PAD_LABEL_GAP = 5; const const PAD_CROSS_GAP: 3PAD_CROSS_GAP = 3; // A system-font capital is 5 pixels wide and 7 tall. const const GLYPH_W: 5GLYPH_W = 5; const const GLYPH_H: 7GLYPH_H = 7; // The left panel's rows are shorter than the right panel's, so this much empty space // before its "Last pitch" row makes both panels end on the same line. const const LEFT_PANEL_FILLER: 6LEFT_PANEL_FILLER = 6; // A click at the top of the pad plays the pop at POINTER_VOLUME_MAX, a click at the // bottom at POINTER_VOLUME_MIN. Everything in between fades smoothly. const const POINTER_VOLUME_MAX: 1POINTER_VOLUME_MAX = 1.0; const const POINTER_VOLUME_MIN: 0.2POINTER_VOLUME_MIN = 0.2; // A click at the pad's left edge pans fully left (-1), a click at its right edge fully // right (+1). const const POINTER_PAN_MIN: -1POINTER_PAN_MIN = -1; const const POINTER_PAN_MAX: 1POINTER_PAN_MAX = 1; // Half the width of the small square ring drawn where you last clicked, in pixels. The // ring reaches this far out from the click point on every side. const const CLICK_MARKER_HALF_SIZE: 6CLICK_MARKER_HALF_SIZE = 6; // The system font's eighth-note glyph marks where the sound sits in the pad. const const NOTE_GLYPH: "♪"NOTE_GLYPH = '\u266A'; // How quickly the looping pop fades out after you let go, in milliseconds. Stopping it // instantly can click. const const RELEASE_FADE_MS: 40RELEASE_FADE_MS = 40; // Keeps `value` from going below `min` or above `max`. function function clamp(value: any, min: any, max: any): anyclamp(value: anyvalue, min: anymin, max: anymax) { return Math.max(min: anymin, Math.min(max: anymax, value: anyvalue)); } // The x to print `text` at so its letters are centered on column `centerX`. The last // character's empty spacing column is left out, so the visible letters are what centers. function function centeredTextX(centerX: any, text: any): numbercenteredTextX(centerX: anycenterX, text: anytext) { return centerX: anycenterX - Math.floor((text: anytext.length * import FONT_WFONT_W - 1) / 2); } // Two sound effects - a blip for the pitch buttons and a pop for the pad - plus the // "click to enable sound" prompt every browser makes necessary. class class DemoDemo { /** @type {Palette | null} */ Demo.palette: Palette | null
@type{Palette | null}
palette
= null;
// Palette slots of the shared UI theme colors, filled by applyTheme() in init(). Demo.theme: nulltheme = null; // Short blip sound played by the pitch buttons. /** @type {AudioClip | null} */ Demo.blipClip: AudioClip | null
@type{AudioClip | null}
blipClip
= null;
// Short pop sound played by clicking the pad. /** @type {AudioClip | null} */ Demo.popClip: AudioClip | null
@type{AudioClip | null}
popClip
= null;
// Pitch of the most recently played blip, or null before the first press. /** @type {number | null} */ Demo.lastKeyPitch: number | null
@type{number | null}
lastKeyPitch
= null;
// Flash countdown, in ticks, for the click ring and the pointer panel's volume meter. // It is held full while the pad is pressed, so both stay lit until you let go. Demo.pointerFlashTimer: numberpointerFlashTimer = 0; // Handle of the looping pop while the pad is held down, or null when nothing plays. // BT.soundPlay() returns it; BT.soundVolumeSet(), BT.soundPanSet(), and // BT.soundStop() take it to change or stop that one sound. /** @type {SoundRef | null} */ Demo.padSound: SoundRef | null
@type{SoundRef | null}
padSound
= null;
// Where the sound sits in the pad: the last pointer position, kept inside the pad. /** @type {Vector2i | null} */ Demo.lastClickPos: Vector2i | null
@type{Vector2i | null}
lastClickPos
= null;
// Volume used for the most recent pop. /** @type {number | null} */ Demo.lastClickVolume: number | null
@type{number | null}
lastClickVolume
= null;
// Pan used for the most recent pop. /** @type {number | null} */ Demo.lastClickPan: number | null
@type{number | null}
lastClickPan
= null;
// Opens the engine overlay with its audio meters, colored from the shared UI theme.
Demo.configure(): {
    isOverlayAudioMetersEnabled: boolean;
    isOverlayVisibleAtStart: boolean;
    overlayStyle: any;
    overlayAudioMeterStyle: any;
}
configure
() {
return { // Live per-bus level bars and a voices/steal/drop readout in the overlay. Off // by default. isOverlayAudioMetersEnabled: booleanisOverlayAudioMetersEnabled: true, isOverlayVisibleAtStart: booleanisOverlayVisibleAtStart: true, // The overlay's default colors live in palette slots 1 and 2, which this demo // never fills - they would draw solid black. These two shared styles point // the bars, text, and meters at the theme's colors instead. overlayStyle: anyoverlayStyle: import THEME_OVERLAY_STYLETHEME_OVERLAY_STYLE, overlayAudioMeterStyle: anyoverlayAudioMeterStyle: import THEME_OVERLAY_AUDIO_METER_STYLETHEME_OVERLAY_AUDIO_METER_STYLE, }; } // Loads the two sound clips and sets up the palette. async Demo.init(): unknowninit() { // AudioClip.load() downloads a sound file and decodes it into a ready-to-play // buffer. That works right away, even before the page is unlocked for sound - // only actually hearing a sound (BT.soundPlay, below) waits for that first click. this.Demo.blipClip: AudioClip | null
@type{AudioClip | null}
blipClip
= await class AudioClip
Decoded audio asset with its winning source URL and buffer-derived metadata. Construct instances with {@link AudioClip.load } , {@link AudioClip.loadAll } , or {@link AudioClip.synth } ; there is no public constructor.
@since1.3.0
AudioClip
.AudioClip.load(url: string | string[], options?: AudioClipLoadOptions): Promise<AudioClip>
Loads an audio clip from a single URL, or from an ordered list of candidate URLs. A single URL runs the download+decode pipeline directly, sharing the per-URL cache and in-flight dedup described on {@link AudioClip } . A URL array tries each candidate in order and resolves with the first one that downloads and decodes successfully - useful for offering a browser-friendly fallback (for example `['music.ogg', 'music.mp3']`) when a container or codec isn't universally supported. If every candidate fails, the error from the last candidate is thrown.
@paramurl - Single audio URL, or an ordered list of fallback URLs.@paramoptions - Optional load options (progress reporting).@returnsLoaded clip, cached under its winning source URL.@throwsError if the URL (or every URL in the list) fails to load or decode.
load
('/audio/blip.wav');
this.Demo.popClip: AudioClip | null
@type{AudioClip | null}
popClip
= await class AudioClip
Decoded audio asset with its winning source URL and buffer-derived metadata. Construct instances with {@link AudioClip.load } , {@link AudioClip.loadAll } , or {@link AudioClip.synth } ; there is no public constructor.
@since1.3.0
AudioClip
.AudioClip.load(url: string | string[], options?: AudioClipLoadOptions): Promise<AudioClip>
Loads an audio clip from a single URL, or from an ordered list of candidate URLs. A single URL runs the download+decode pipeline directly, sharing the per-URL cache and in-flight dedup described on {@link AudioClip } . A URL array tries each candidate in order and resolves with the first one that downloads and decodes successfully - useful for offering a browser-friendly fallback (for example `['music.ogg', 'music.mp3']`) when a container or codec isn't universally supported. If every candidate fails, the error from the last candidate is thrown.
@paramurl - Single audio URL, or an ordered list of fallback URLs.@paramoptions - Optional load options (progress reporting).@returnsLoaded clip, cached under its winning source URL.@throwsError if the URL (or every URL in the list) fails to load or decode.
load
('/audio/pop.wav');
this.Demo.palette: Palette | null
@type{Palette | null}
palette
=
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) => Palette
Creates a standalone palette instance.
@since1.0.3@paramsize - Palette size. Defaults to 256 colors.@returnsNew mutable palette.
paletteCreate
(256);
// applyTheme() installs the twelve shared UI colors (panels, text, buttons, ...) // and reports their slots, so render() can clear the screen with the theme's // background color and draw the pad and click marker in theme colors. this.Demo.theme: nulltheme = import applyThemeapplyTheme(this.Demo.palette: Palette
@type{Palette | null}
palette
);
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) => void
Stores 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.
@since1.0.3@parampalette - Palette to make active.
paletteSet
(this.Demo.palette: Palette
@type{Palette | null}
palette
);
return true; } // Runs the UI kit's once-per-tick housekeeping, then starts, moves, or stops the // pad's looping pop. // // The pitch keys (1/2/3) are bound to the buttons declared in render() via their // { key } option. ui.tick() is where the kit safely catches those presses - keyboard // "was it just pressed?" flags can only be read reliably here in update(), never in // render() (keyboard-input explains why in detail). Demo.update(): voidupdate() { import uiui.tick(); // BT.BTN_POINTER_A is the primary click (left mouse button, or a touchscreen // tap). BT.isPressed fires only once, on the tick the press happens; BT.isDown // stays true for as long as the button is held. const const pos: Vector2ipos =
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.pointerPos: (pointerIndex?: number) => Vector2i
Returns the position of the pointer in the given slot, in display coordinates. Slot 0 is the mouse; slots 1 through 3 are touch / pen contacts assigned in arrival order. Returns `Vector2i.zero()` when the engine has not been initialized, the slot index is out of `[0, 3]`, or the slot has no live pointer.
@since1.0.3@parampointerIndex - Pointer slot (defaults to 0 = mouse).@returnsPointer position in display coordinates.
pointerPos
(0);
// A press inside the pad starts the pop, looping. A press anywhere still unlocks // sound, because the engine listens for that on its own. if (
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.isPressed: (button: number, player?: number, repeatRate?: number) => boolean
Checks whether a button was pressed on the current frame. Same parameter semantics as {@link isDown } ; returns `true` only on the frame the button transitions from up to down. Call from `update()`, not `render()`, for reliable detection: for keyboard-mapped face buttons (players 0 and 1), the press edge clears once per fixed-update tick, which always runs before that frame's `render()`. `BTN_L2`, `BTN_R2` and `BTN_TRIGGER` are gamepad only (no keyboard mapping).
@since1.1.1@changed1.8.0 Also accepts BTN_L2 / BTN_R2 / BTN_TRIGGER (gamepad only).@parambutton - Button constant from the `BTN_*` set.@paramplayer - Zero-based player index for gamepads, or pointer slot (0-3) for `BTN_POINTER_*`.@paramrepeatRate - Optional repeat interval in fixed ticks (`0`/omitted = edge only).@returns`true` on the transition frame.
isPressed
(
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.type BTN_POINTER_A: number
Primary pointer button code. Maps to mouse left for slot 0; touch contact for slots 1-3.
@since0.1.0
BTN_POINTER_A
, 0) && const PAD_RECT: Rect2iPAD_RECT.Rect2i.isContainingXY(px: number, py: number): boolean
Tests raw coordinates against this rectangle without allocating a vector.
@parampx - X coordinate to test.@parampy - Y coordinate to test.@returnsTrue if point is inside the rectangle.
isContainingXY
(const pos: Vector2ipos.Vector2i.x: number
Horizontal component (defaults to 0).
x
, const pos: Vector2ipos.Vector2i.y: number
Vertical component (defaults to 0).
y
)) {
this.Demo.startPadSound(pos: any): voidstartPadSound(const pos: Vector2ipos); } else if (this.Demo.padSound: SoundRef | null
@type{SoundRef | null}
padSound
!== null) {
if (
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.isDown: (button: number, player?: number) => boolean
Checks whether a button is currently held. For pointer buttons (`BTN_POINTER_A..D`), the second parameter is the pointer slot index (0 = mouse, 1-3 = touch / pen). For mouse slot 0: `A` is left, `B` is right, `C` is middle, `D` is back / forward (matches RetroBlit canonical, not DOM `PointerEvent.button` index). Touch / pen slots only support `A`; B/C/D return `false`. `button` accepts one or more bit flags from the `BTN_*` set (for example `BT.BTN_A | BT.BTN_B`). Matching uses ANY semantics: returns `true` when any selected button is held. For face buttons (`BTN_UP`…`BTN_SELECT`), players `0` and `1` merge keyboard and gamepad input (logical OR). Players `2` and `3` use gamepad only. Pointer flags (`BTN_POINTER_*`) use the `player` argument as pointer slot. `BTN_L2`, `BTN_R2` and `BTN_TRIGGER` are gamepad only (no keyboard mapping).
@since1.1.1@changed1.8.0 Also accepts BTN_L2 / BTN_R2 / BTN_TRIGGER (gamepad only).@parambutton - Button constant from the `BTN_*` set.@paramplayer - Zero-based player index for gamepads / keyboard, or pointer slot (0-3) for `BTN_POINTER_*`.@returns`true` while the button remains pressed.
isDown
(
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.type BTN_POINTER_A: number
Primary pointer button code. Maps to mouse left for slot 0; touch contact for slots 1-3.
@since0.1.0
BTN_POINTER_A
, 0)) {
// Still held: the sound follows the pointer, even outside the pad. this.Demo.movePadSound(pos: any): voidmovePadSound(const pos: Vector2ipos); } else { // Let go: fade the loop out and forget its handle.
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
.soundStop: (ref: SoundRef, options?: SoundStopOptions) => void
Stops a playing sound, optionally fading it out.
@since1.3.0@paramref - Sound to stop, from {@link BT.soundPlay}.@paramoptions - Optional fade behavior.@paramoptions.fadeOutMs - Fade-out duration in milliseconds. Omit to stop immediately.
soundStop
(this.Demo.padSound: SoundRef
@type{SoundRef | null}
padSound
, { SoundStopOptions.fadeOutMs?: number | undefined
Optional linear fade-out duration in milliseconds before the voice actually stops.
fadeOutMs
: const RELEASE_FADE_MS: 40RELEASE_FADE_MS });
this.Demo.padSound: SoundRef | null
@type{SoundRef | null}
padSound
= null;
} } // Count the click flash down toward zero, one tick at a time. if (this.Demo.pointerFlashTimer: numberpointerFlashTimer > 0) { this.Demo.pointerFlashTimer: numberpointerFlashTimer -= 1; } } // Clears the screen and declares the whole UI: the status line, then the two panels // and the pad drawn on top of the right one. Demo.render(): voidrender() {
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) => void
Sets 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.
@since0.1.0@parampaletteIndex - Palette index for the full-screen clear pass.
clear
(this.Demo.theme: nulltheme.bg);
this.Demo.renderStatusLine(): voidrenderStatusLine(); this.Demo.renderKeyboardPanel(): voidrenderKeyboardPanel(); this.Demo.renderPointerPanel(): voidrenderPointerPanel(); this.Demo.renderPad(): voidrenderPad(); } // One centered line just under the overlay bars: the shared unlock reminder in warm // orange until sound is unlocked, then a dim reminder of the controls. Demo.renderStatusLine(): voidrenderStatusLine() { const const isLocked: booleanisLocked = !
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
.isAudioUnlocked: boolean
Whether the audio context has been unlocked by a user gesture. Browsers require a user gesture (pointer, key, or touch press) before allowing audio playback. Starts `false`; flips to `true` for the rest of the session after the first gesture successfully resumes the audio context.
@since1.3.0@returns`true` once unlocked; `false` when locked or before initialization.
isAudioUnlocked
;
const const text: "Click or press a key to enable sound" | "Tap a button for a blip. Click the pad for a pop."text = const isLocked: booleanisLocked ? 'Click or press a key to enable sound' : 'Tap a button for a blip. Click the pad for a pop.'; const const x: numberx = function centeredTextX(centerX: any, text: any): numbercenteredTextX(Math.floor(
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.displaySize: Vector2i
Active logical render resolution in pixels. This is the game/simulation coordinate space configured by the demo, not the canvas element's CSS size. Each read returns a clone.
@since1.0.4@returnsConfigured logical size, or `Vector2i.zero()` before initialization.
displaySize
.Vector2i.x: number
Horizontal component (defaults to 0).
x
/ 2), const text: "Click or press a key to enable sound" | "Tap a button for a blip. Click the pad for a pop."text);
import uiui.caption(const x: numberx, const STATUS_Y: 64STATUS_Y, const text: "Click or press a key to enable sound" | "Tap a button for a blip. Click the pad for a pop."text, { color: stringcolor: const isLocked: booleanisLocked ? 'warm' : 'dim' }); } // Left-hand panel: one button per pitch preset, plus the pitch last played. Each // button fires on click, tap, or its number key - ui.button() treats all three the // same, which is what makes this demo playable on a touchscreen. Demo.renderKeyboardPanel(): voidrenderKeyboardPanel() { import uiui.begin('topLeft', { x: numberx: const LEFT_PANEL_X: 4LEFT_PANEL_X, y: numbery: const PANEL_Y: 76PANEL_Y, width: numberwidth: const LEFT_PANEL_W: 140LEFT_PANEL_W }); import uiui.panel('Keyboard SFX (pitch)'); import uiui.label('Keys 1, 2, 3 or tap', { color: stringcolor: 'dim' }); import uiui.spacer(4); for (const const preset: anypreset of const KEY_PITCH_PRESETS: {}KEY_PITCH_PRESETS) { if (import uiui.button(const preset: anypreset.label, { key: anykey: const preset: anypreset.code, width: numberwidth: const PITCH_BUTTON_W: 120PITCH_BUTTON_W })) { // Play the blip at this preset's speed and remember it for the row below.
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
.soundPlay: (clip: AudioClip, options?: SoundPlayOptions) => SoundRef
Plays a loaded audio clip through the SFX voice pool. Returns an inert {@link SoundRef } without allocating a voice when the clip hasn't finished loading yet (or was already unloaded), when the pool has no free or stealable voice at this priority, or before the engine has unlocked audio playback.
@since1.3.0@paramclip - Loaded audio clip to play.@paramoptions - Playback options.@returnsA handle identifying the new voice; pass it to {@link BT.soundStop} and the other per-sound controls. Safe to use even when playback was silently dropped - every accessor on an inert handle is a no-op.
soundPlay
(this.Demo.blipClip: AudioClip | null
@type{AudioClip | null}
blipClip
, { VoicePlayOptions.pitch?: number | undefined
Initial `playbackRate`. Defaults to `1`.
pitch
: const preset: anypreset.pitch });
this.Demo.lastKeyPitch: number | null
@type{number | null}
lastKeyPitch
= const preset: anypreset.pitch;
} } const const lastPitchLabel: stringlastPitchLabel = this.Demo.lastKeyPitch: number | null
@type{number | null}
lastKeyPitch
=== null ? '-' : `${this.Demo.lastKeyPitch: number
@type{number | null}
lastKeyPitch
.toFixed(2)}x`;
import uiui.spacer(const LEFT_PANEL_FILLER: 6LEFT_PANEL_FILLER); import uiui.kv('Last pitch', const lastPitchLabel: stringlastPitchLabel); import uiui.end(); } // Right-hand panel: room for the pad, then the volume and pan of the most recent pop. // The volume gets a meter bar that fills left-to-right (and flashes green right after // a click); the pan is a plain number row, since -1 means left, 0 center, and +1 // right. Demo.renderPointerPanel(): voidrenderPointerPanel() { const const hasClicked: booleanhasClicked = this.Demo.lastClickVolume: number | null
@type{number | null}
lastClickVolume
!== null && this.Demo.lastClickPan: number | null
@type{number | null}
lastClickPan
!== null;
const const flashing: booleanflashing = this.Demo.pointerFlashTimer: numberpointerFlashTimer > 0; import uiui.begin('topLeft', { x: numberx: const RIGHT_PANEL_X: 148RIGHT_PANEL_X, y: numbery: const PANEL_Y: 76PANEL_Y, width: numberwidth: const RIGHT_PANEL_W: 168RIGHT_PANEL_W }); import uiui.panel('Pointer SFX (volume/pan)'); // An empty row as tall as the pad: renderPad() paints the pad into this gap after // the panel is drawn, so it lands on top. import uiui.spacer(const PAD_H: 74PAD_H + const PAD_GAP: 4PAD_GAP); // Volume row plus a read-only bar showing the same value ('-' before any click). import uiui.kv('Volume', const hasClicked: booleanhasClicked ? this.Demo.lastClickVolume: number | null
@type{number | null}
lastClickVolume
.toFixed(2) : '-');
import uiui.meter(null, const hasClicked: booleanhasClicked ? this.Demo.lastClickVolume: number | null
@type{number | null}
lastClickVolume
: 0, {
color: stringcolor: const flashing: booleanflashing ? 'accent' : 'info', width: numberwidth: const PAD_RECT: Rect2iPAD_RECT.Rect2i.width: number
Width in pixels (defaults to 0).
width
,
}); // Pan as a signed number: -1.00 is fully left, 0.00 centered, +1.00 fully right. const const panSign: "+" | ""panSign = const hasClicked: booleanhasClicked && this.Demo.lastClickPan: number | null
@type{number | null}
lastClickPan
> 0 ? '+' : '';
import uiui.kv('Pan', const hasClicked: booleanhasClicked ? `${const panSign: "+" | ""panSign}${this.Demo.lastClickPan: number | null
@type{number | null}
lastClickPan
.toFixed(2)}` : '-');
import uiui.end(); } // The pad: a dark box with its four edges labeled with what they do to the // sound, a faint center cross between the labels, and a marker where you last // clicked. The ui.draw* helpers draw in call order like the panel itself, so the pad // lands on top of the panel. Demo.renderPad(): voidrenderPad() { const { const x: number
Left edge X coordinate (defaults to 0).
x
, const y: number
Top edge Y coordinate (defaults to 0).
y
, const width: number
Width in pixels (defaults to 0).
width
, const height: number
Height in pixels (defaults to 0).
height
} = const PAD_RECT: Rect2iPAD_RECT;
const const right: numberright = const x: number
Left edge X coordinate (defaults to 0).
x
+ const width: number
Width in pixels (defaults to 0).
width
- 1;
const const bottom: numberbottom = const y: number
Top edge Y coordinate (defaults to 0).
y
+ const height: number
Height in pixels (defaults to 0).
height
- 1;
const const midX: anymidX = const x: number
Left edge X coordinate (defaults to 0).
x
+ Math.floor(const width: number
Width in pixels (defaults to 0).
width
/ 2);
const const midY: anymidY = const y: number
Top edge Y coordinate (defaults to 0).
y
+ Math.floor(const height: number
Height in pixels (defaults to 0).
height
/ 2);
import uiui.drawRectFill(const PAD_RECT: Rect2iPAD_RECT, this.Demo.theme: nulltheme.shadow); import uiui.drawRect(const PAD_RECT: Rect2iPAD_RECT, this.Demo.theme: nulltheme.border); // Where each edge label's letters start and end, PAD_LABEL_GAP inside the border. const const labelTop: numberlabelTop = const y: number
Top edge Y coordinate (defaults to 0).
y
+ 1 + const PAD_LABEL_GAP: 5PAD_LABEL_GAP;
const const labelBottom: numberlabelBottom = const bottom: numberbottom - 1 - const PAD_LABEL_GAP: 5PAD_LABEL_GAP; const const labelLeft: numberlabelLeft = const x: number
Left edge X coordinate (defaults to 0).
x
+ 1 + const PAD_LABEL_GAP: 5PAD_LABEL_GAP;
const const labelRight: numberlabelRight = const right: numberright - 1 - const PAD_LABEL_GAP: 5PAD_LABEL_GAP; // A caption's capitals start 1 pixel above the y it is placed at, so each y below // is the letters' top row plus 1. The Q of "Quiet" hangs its tail below that. const const sideLabelY: numbersideLabelY = const midY: anymidY - Math.floor(const GLYPH_H: 7GLYPH_H / 2) + 1; import uiui.caption(function centeredTextX(centerX: any, text: any): numbercenteredTextX(const midX: anymidX, 'Loud'), const labelTop: numberlabelTop + 1, 'Loud', { color: stringcolor: 'dim' }); import uiui.caption(function centeredTextX(centerX: any, text: any): numbercenteredTextX(const midX: anymidX, 'Quiet'), const labelBottom: numberlabelBottom - const GLYPH_H: 7GLYPH_H + 2, 'Quiet', { color: stringcolor: 'dim' }); import uiui.caption(const labelLeft: numberlabelLeft, const sideLabelY: numbersideLabelY, 'L', { color: stringcolor: 'dim' }); import uiui.caption(const labelRight: numberlabelRight - const GLYPH_W: 5GLYPH_W + 1, const sideLabelY: numbersideLabelY, 'R', { color: stringcolor: 'dim' }); // Center cross, stopping short of the labels: the middle row is half volume, the // middle column centered pan. const const crossLeft: numbercrossLeft = const labelLeft: numberlabelLeft + const GLYPH_W: 5GLYPH_W + const PAD_CROSS_GAP: 3PAD_CROSS_GAP; const const crossRight: numbercrossRight = const labelRight: numberlabelRight - const GLYPH_W: 5GLYPH_W - const PAD_CROSS_GAP: 3PAD_CROSS_GAP; const const crossTop: numbercrossTop = const labelTop: numberlabelTop + const GLYPH_H: 7GLYPH_H + const PAD_CROSS_GAP: 3PAD_CROSS_GAP; const const crossBottom: numbercrossBottom = const labelBottom: numberlabelBottom - const GLYPH_H: 7GLYPH_H - const PAD_CROSS_GAP: 3PAD_CROSS_GAP; import uiui.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(const crossLeft: numbercrossLeft, const midY: anymidY, const crossRight: numbercrossRight - const crossLeft: numbercrossLeft + 1, 1), this.Demo.theme: nulltheme.panel);
import uiui.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(const midX: anymidX, const crossTop: numbercrossTop, 1, const crossBottom: numbercrossBottom - const crossTop: numbercrossTop + 1), this.Demo.theme: nulltheme.panel);
this.Demo.renderClickMarker(): voidrenderClickMarker(); } // Marks where the sound sits: a note glyph that stays after you let go, plus a square // ring that stays lit while the pad is held and for FLASH_TICKS after. Both are kept // inside the pad. Demo.renderClickMarker(): voidrenderClickMarker() { if (this.Demo.lastClickPos: Vector2i | null
@type{Vector2i | null}
lastClickPos
=== null) {
return; } const { const x: number
Horizontal component (defaults to 0).
x
, const y: number
Vertical component (defaults to 0).
y
} = this.Demo.lastClickPos: Vector2i
@type{Vector2i | null}
lastClickPos
;
const const isLit: booleanisLit = this.Demo.pointerFlashTimer: numberpointerFlashTimer > 0; // Center the 5x7 glyph on the spot, without letting it poke out of the pad. A // caption's letters start 1 pixel above its y. const const glyphX: anyglyphX = function clamp(value: any, min: any, max: any): anyclamp(const x: number
Horizontal component (defaults to 0).
x
- Math.floor(const GLYPH_W: 5GLYPH_W / 2), const PAD_RECT: Rect2iPAD_RECT.Rect2i.x: number
Left edge X coordinate (defaults to 0).
x
+ 1, const PAD_RECT: Rect2iPAD_RECT.Rect2i.x: number
Left edge X coordinate (defaults to 0).
x
+ const PAD_RECT: Rect2iPAD_RECT.Rect2i.width: number
Width in pixels (defaults to 0).
width
- 1 - const GLYPH_W: 5GLYPH_W);
const const glyphTop: anyglyphTop = function clamp(value: any, min: any, max: any): anyclamp(const y: number
Vertical component (defaults to 0).
y
- Math.floor(const GLYPH_H: 7GLYPH_H / 2), const PAD_RECT: Rect2iPAD_RECT.Rect2i.y: number
Top edge Y coordinate (defaults to 0).
y
+ 1, const PAD_RECT: Rect2iPAD_RECT.Rect2i.y: number
Top edge Y coordinate (defaults to 0).
y
+ const PAD_RECT: Rect2iPAD_RECT.Rect2i.height: number
Height in pixels (defaults to 0).
height
- 1 - const GLYPH_H: 7GLYPH_H);
import uiui.caption(const glyphX: anyglyphX, const glyphTop: anyglyphTop + 1, const NOTE_GLYPH: "♪"NOTE_GLYPH, { color: stringcolor: const isLit: booleanisLit ? 'accent' : 'header' }); if (!const isLit: booleanisLit) { return; } const const size: numbersize = const CLICK_MARKER_HALF_SIZE: 6CLICK_MARKER_HALF_SIZE * 2 + 1; const const left: anyleft = function clamp(value: any, min: any, max: any): anyclamp(const x: number
Horizontal component (defaults to 0).
x
- const CLICK_MARKER_HALF_SIZE: 6CLICK_MARKER_HALF_SIZE, const PAD_RECT: Rect2iPAD_RECT.Rect2i.x: number
Left edge X coordinate (defaults to 0).
x
, const PAD_RECT: Rect2iPAD_RECT.Rect2i.x: number
Left edge X coordinate (defaults to 0).
x
+ const PAD_RECT: Rect2iPAD_RECT.Rect2i.width: number
Width in pixels (defaults to 0).
width
- const size: numbersize);
const const top: anytop = function clamp(value: any, min: any, max: any): anyclamp(const y: number
Vertical component (defaults to 0).
y
- const CLICK_MARKER_HALF_SIZE: 6CLICK_MARKER_HALF_SIZE, const PAD_RECT: Rect2iPAD_RECT.Rect2i.y: number
Top edge Y coordinate (defaults to 0).
y
, const PAD_RECT: Rect2iPAD_RECT.Rect2i.y: number
Top edge Y coordinate (defaults to 0).
y
+ const PAD_RECT: Rect2iPAD_RECT.Rect2i.height: number
Height in pixels (defaults to 0).
height
- const size: numbersize);
import uiui.drawRect(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(const left: anyleft, const top: anytop, const size: numbersize, const size: numbersize), this.Demo.theme: nulltheme.accent);
} // Starts the pop looping at the pressed spot. `pos` is the pointer in display pixels. Demo.startPadSound(pos: any): voidstartPadSound(pos: anypos) { this.Demo.padSound: SoundRef | null
@type{SoundRef | null}
padSound
=
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
.soundPlay: (clip: AudioClip, options?: SoundPlayOptions) => SoundRef
Plays a loaded audio clip through the SFX voice pool. Returns an inert {@link SoundRef } without allocating a voice when the clip hasn't finished loading yet (or was already unloaded), when the pool has no free or stealable voice at this priority, or before the engine has unlocked audio playback.
@since1.3.0@paramclip - Loaded audio clip to play.@paramoptions - Playback options.@returnsA handle identifying the new voice; pass it to {@link BT.soundStop} and the other per-sound controls. Safe to use even when playback was silently dropped - every accessor on an inert handle is a no-op.
soundPlay
(this.Demo.popClip: AudioClip | null
@type{AudioClip | null}
popClip
, { VoicePlayOptions.loop?: boolean | undefined
Whether the buffer loops. Defaults to `false`.
loop
: true });
this.Demo.movePadSound(pos: any): voidmovePadSound(pos: anypos); } // Turns a pointer position into a volume and a pan, hands both to the looping pop, // and remembers everything the pointer panel and the marker show. `pos` is the // pointer in display pixels; it may be outside the pad while you drag, so it is // clamped in. Demo.movePadSound(pos: any): voidmovePadSound(pos: anypos) { const const x: anyx = function clamp(value: any, min: any, max: any): anyclamp(pos: anypos.x, const PAD_RECT: Rect2iPAD_RECT.Rect2i.x: number
Left edge X coordinate (defaults to 0).
x
, const PAD_RECT: Rect2iPAD_RECT.Rect2i.x: number
Left edge X coordinate (defaults to 0).
x
+ const PAD_RECT: Rect2iPAD_RECT.Rect2i.width: number
Width in pixels (defaults to 0).
width
- 1);
const const y: anyy = function clamp(value: any, min: any, max: any): anyclamp(pos: anypos.y, const PAD_RECT: Rect2iPAD_RECT.Rect2i.y: number
Top edge Y coordinate (defaults to 0).
y
, const PAD_RECT: Rect2iPAD_RECT.Rect2i.y: number
Top edge Y coordinate (defaults to 0).
y
+ const PAD_RECT: Rect2iPAD_RECT.Rect2i.height: number
Height in pixels (defaults to 0).
height
- 1);
// Turn the vertical position into a fraction: 0 at the top of the pad, 1 at the // bottom. const const verticalFraction: numberverticalFraction = (const y: anyy - const PAD_RECT: Rect2iPAD_RECT.Rect2i.y: number
Top edge Y coordinate (defaults to 0).
y
) / (const PAD_RECT: Rect2iPAD_RECT.Rect2i.height: number
Height in pixels (defaults to 0).
height
- 1);
// Loud at the top, quiet at the bottom, so we count down from the maximum. const const volume: numbervolume = const POINTER_VOLUME_MAX: 1POINTER_VOLUME_MAX - const verticalFraction: numberverticalFraction * (const POINTER_VOLUME_MAX: 1POINTER_VOLUME_MAX - const POINTER_VOLUME_MIN: 0.2POINTER_VOLUME_MIN); // Turn the horizontal position into a fraction: 0 at the pad's left edge, 1 at // its right edge, then stretch it out to the -1..+1 range the pan expects. const const horizontalFraction: numberhorizontalFraction = (const x: anyx - const PAD_RECT: Rect2iPAD_RECT.Rect2i.x: number
Left edge X coordinate (defaults to 0).
x
) / (const PAD_RECT: Rect2iPAD_RECT.Rect2i.width: number
Width in pixels (defaults to 0).
width
- 1);
const const pan: numberpan = const POINTER_PAN_MIN: -1POINTER_PAN_MIN + const horizontalFraction: numberhorizontalFraction * (const POINTER_PAN_MAX: 1POINTER_PAN_MAX - const POINTER_PAN_MIN: -1POINTER_PAN_MIN); // Change the sound that is already playing instead of starting a new one.
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
.soundVolumeSet: (ref: SoundRef, value: number, options?: SoundParamSetOptions) => void
Sets a sound's gain, optionally fading to it.
@since1.3.0@paramref - Sound to update.@paramvalue - Target gain.@paramoptions - Optional fade behavior.@paramoptions.fadeMs - Fade duration in milliseconds. Omit for an immediate change.
soundVolumeSet
(this.Demo.padSound: SoundRef | null
@type{SoundRef | null}
padSound
, const volume: numbervolume);
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
.soundPanSet: (ref: SoundRef, value: number, options?: SoundParamSetOptions) => void
Sets a sound's stereo pan, optionally fading to it.
@since1.3.0@paramref - Sound to update.@paramvalue - Target pan.@paramoptions - Optional fade behavior.@paramoptions.fadeMs - Fade duration in milliseconds. Omit for an immediate change.
soundPanSet
(this.Demo.padSound: SoundRef | null
@type{SoundRef | null}
padSound
, const pan: numberpan);
this.Demo.lastClickPos: Vector2i | null
@type{Vector2i | null}
lastClickPos
= new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const x: anyx, const y: anyy);
this.Demo.lastClickVolume: number | null
@type{number | null}
lastClickVolume
= const volume: numbervolume;
this.Demo.lastClickPan: number | null
@type{number | null}
lastClickPan
= const pan: numberpan;
this.Demo.pointerFlashTimer: numberpointerFlashTimer = const FLASH_TICKS: 12FLASH_TICKS; } } 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.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
(class DemoDemo);