/**
 * Audio Basics Demo - loading sounds, playing them, and the "click to allow sound" rule.
 * @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
 *
 * Every web browser refuses to make any sound at all until you click 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 without asking first. BT.isAudioUnlocked
 * tells you whether that first click or key press has happened yet.
 *
 * This page loads two short sound effects with AudioClip.load() and plays them with
 * BT.soundPlay(), which can also change how a sound plays each time:
 * - pitch: how fast the sound plays back. Higher pitch sounds faster and higher, like
 *   speeding up a cassette tape. Lower pitch 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).
 *
 * The panels and buttons come from the shared UI kit in src/shared/ui.js, so each pitch
 * preset works three ways: click its button, tap it on a phone, or press its number key.
 *
 * The engine's built-in overlay can also show live audio meters: little bars that move
 * up and down with how loud each audio bus (main, music, sfx) is right now, plus a
 * count of how many sounds are playing at once. This demo turns that feature on with
 * `isOverlayAudioMetersEnabled: true` in configure() - open the overlay (see below) and
 * watch the meters jump every time a blip or pop plays.
 *
 * Try this:
 * - Click anywhere, or press any key, to unlock sound - watch the message at the top
 *   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.
 * - Click near the top of the screen for a loud "pop," or near the bottom for a quiet
 *   one. Click near the left or right edge to hear it pan toward that speaker
 *   (headphones or stereo speakers make this easiest to hear).
 * - Press the backquote key (`) or click the small icon in the bottom-left corner to
 *   open the engine overlay, then play a few sounds and watch the audio meters move.
 */

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;
    ... 106 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
} from 'blit386';
import { import applyThemeapplyTheme, import uiui, import UI_ANCHORSUI_ANCHORS } from './shared/ui.js'; /** @typedef {import('blit386').IBTDemo} IBTDemo */ /** @typedef {import('blit386').Palette} Palette */ /** @typedef {import('blit386').Vector2i} Vector2i */ /** @typedef {import('blit386').HardwareSettings} HardwareSettings */ // How many fixed ticks a click flash stays lit before fading back to normal. // The engine runs 60 ticks per second by default, so 12 ticks is about 200ms. const const FLASH_TICKS: 12FLASH_TICKS = 12; // The three pitch-preset buttons, and the pitch each one plays the blip sound at. // 1.0 is the sound's natural pitch; smaller numbers sound lower and slower, // bigger numbers sound higher and faster. `label` is the button text (which 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; // A click near the top of the screen plays at POINTER_VOLUME_MAX; a click near the // bottom plays 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 left edge pans fully left (-1); a click at the right edge pans // 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 little square ring drawn where you last clicked, in pixels. // The marker is drawn by stepping this far out from the click point on every side. const const CLICK_MARKER_HALF_SIZE: 6CLICK_MARKER_HALF_SIZE = 6; /** * Keeps a number from going below `min` or above `max`. * * @param {number} value - Number to restrict. * @param {number} min - Smallest allowed result. * @param {number} max - Largest allowed result. * @returns {number} */ function function clamp(value: number, min: number, max: number): number
Keeps a number from going below `min` or above `max`.
@paramvalue - Number to restrict.@parammin - Smallest allowed result.@parammax - Largest allowed result.@returns
clamp
(value: number
- Number to restrict.
@paramvalue - Number to restrict.
value
, min: number
- Smallest allowed result.
@parammin - Smallest allowed result.
min
, max: number
- Largest allowed result.
@parammax - Largest allowed result.
max
) {
return Math.max(min: number
- Smallest allowed result.
@parammin - Smallest allowed result.
min
, Math.min(max: number
- Largest allowed result.
@parammax - Largest allowed result.
max
, value: number
- Number to restrict.
@paramvalue - Number to restrict.
value
));
} /** * Shows AudioClip loading, BT.soundPlay volume/pitch/pan, and the audio unlock gesture. * * @implements {IBTDemo} */ class class Demo
Shows AudioClip loading, BT.soundPlay volume/pitch/pan, and the audio unlock gesture.
@implementsIBTDemo
Demo
{
/** @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: null
Palette slots of the shared UI theme colors, filled by applyTheme() in init().
theme
= null;
/** @type {AudioClip | null} Short blip sound played by the pitch buttons. */ Demo.blipClip: AudioClip | null
@type{AudioClip | null} Short blip sound played by the pitch buttons.
blipClip
= null;
/** @type {AudioClip | null} Short pop sound played by clicking. */ Demo.popClip: AudioClip | null
@type{AudioClip | null} Short pop sound played by clicking.
popClip
= null;
/** @type {number | null} Pitch of the most recently played blip, or null before the first press. */ Demo.lastKeyPitch: number | null
@type{number | null} Pitch of the most recently played blip, or null before the first press.
lastKeyPitch
= null;
// Flash countdown for the click marker and the pointer panel's volume meter. Demo.pointerFlashTimer: numberpointerFlashTimer = 0; /** @type {Vector2i | null} Where the pointer was the last time it was clicked. */ Demo.lastClickPos: Vector2i | null
@type{Vector2i | null} Where the pointer was the last time it was clicked.
lastClickPos
= null;
/** @type {number | null} Volume used for the most recent pop sound. */ Demo.lastClickVolume: number | null
@type{number | null} Volume used for the most recent pop sound.
lastClickVolume
= null;
/** @type {number | null} Pan used for the most recent pop sound. */ Demo.lastClickPan: number | null
@type{number | null} Pan used for the most recent pop sound.
lastClickPan
= null;
/** * Turns on the engine's built-in audio meters in the overlay. * * @returns {Partial<HardwareSettings>} */ Demo.configure(): Partial<HardwareSettings>
Turns on the engine's built-in audio meters in the overlay.
@returns
configure
() {
return { // Live per-bus level meters and a voice-count readout in the overlay (explained in the header above). isOverlayAudioMetersEnabled: booleanisOverlayAudioMetersEnabled: true, }; } /** * Loads the two sound clips and sets up the palette. * * @returns {Promise<boolean>} */ async Demo.init(): Promise<boolean>
Loads the two sound clips and sets up the palette.
@returns
init
() {
// AudioClip.load() downloads a sound file and decodes it into a ready-to-play // buffer. That part works right away, even before the page is "unlocked" for // sound - only actually hearing a sound (BT.soundPlay, below) waits for that. this.Demo.blipClip: AudioClip | null
@type{AudioClip | null} Short blip sound played by the pitch buttons.
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} Short pop sound played by clicking.
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;
    ... 106 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 click marker with the theme's accent green. this.Demo.theme: null
Palette slots of the shared UI theme colors, filled by applyTheme() in init().
theme
= 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;
    ... 106 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 reads pointer clicks. * * 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(): void
Runs the UI kit's once-per-tick housekeeping, then reads pointer clicks. 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).
update
() {
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 frame the click happens - the same // way pointer-basics and pointer-paint read their clicks. if (
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.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()`.
@since1.1.1@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;
    ... 106 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 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;
    ... 106 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);
// Skip clicks that land on a UI kit widget - tapping a pitch button should // only play its blip, not also fire a pop underneath the button. if (!import uiui.overWidget(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.playPopAt(pos: Vector2i): void
Plays the pop sound with a volume and pan taken from where the click landed, and remembers everything the pointer panel and click marker show.
@parampos - Where the click landed, in display pixels.
playPopAt
(const pos: Vector2ipos);
} } // 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 unlock/status message, the click * marker, and the two info panels along the bottom edge. */ Demo.render(): void
Clears the screen and declares the whole UI: the unlock/status message, the click marker, and the two info panels along the bottom edge.
render
() {
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.clear: (paletteIndex: number) => 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: null
Palette slots of the shared UI theme colors, filled by applyTheme() in init().
theme
.bg);
// The click marker is scene drawing, not a widget - draw it first so the panels // always sit on top of it. this.Demo.renderClickMarker(): void
Draws a small square ring where you last clicked, only while its flash is active.
renderClickMarker
();
this.Demo.renderStatusLine(): void
The status line in the top-left corner: the shared unlock reminder until audio is unlocked, then a plain reminder of the controls. A borderless group - just one line of text, no panel around it.
renderStatusLine
();
this.Demo.renderKeyboardPanel(): void
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.
renderKeyboardPanel
();
this.Demo.renderPointerPanel(): void
Right-hand panel: the volume and pan of the most recent click. 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.
renderPointerPanel
();
} /** * The status line in the top-left corner: the shared unlock reminder until audio is * unlocked, then a plain reminder of the controls. A borderless group - just one * line of text, no panel around it. */ Demo.renderStatusLine(): void
The status line in the top-left corner: the shared unlock reminder until audio is unlocked, then a plain reminder of the controls. A borderless group - just one line of text, no panel around it.
renderStatusLine
() {
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT); // The shared "click to enable sound" row - it draws itself only while sound is // still locked, and disappears on its own after the first click or key press. import uiui.audioUnlockHint(); // Once sound works, swap in a short reminder of what to try instead. if (
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.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
) {
import uiui.label('Tap a button for a blip. Click anywhere for a pop.', { color: stringcolor: 'dim' }); } import uiui.end(); } /** * Draws a small square ring where you last clicked, only while its flash is active. */ Demo.renderClickMarker(): void
Draws a small square ring where you last clicked, only while its flash is active.
renderClickMarker
() {
if (this.Demo.lastClickPos: Vector2i | null
@type{Vector2i | null} Where the pointer was the last time it was clicked.
lastClickPos
=== null || this.Demo.pointerFlashTimer: numberpointerFlashTimer === 0) {
return; } const const marker: Rect2imarker = 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
(
this.Demo.lastClickPos: Vector2i
@type{Vector2i | null} Where the pointer was the last time it was clicked.
lastClickPos
.Vector2i.x: number
Horizontal component (defaults to 0).
x
- const CLICK_MARKER_HALF_SIZE: 6CLICK_MARKER_HALF_SIZE,
this.Demo.lastClickPos: Vector2i
@type{Vector2i | null} Where the pointer was the last time it was clicked.
lastClickPos
.Vector2i.y: number
Vertical component (defaults to 0).
y
- const CLICK_MARKER_HALF_SIZE: 6CLICK_MARKER_HALF_SIZE,
const CLICK_MARKER_HALF_SIZE: 6CLICK_MARKER_HALF_SIZE * 2, const CLICK_MARKER_HALF_SIZE: 6CLICK_MARKER_HALF_SIZE * 2, );
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.drawRect: (rect: Rect2i, paletteIndex: number) => void
Draws an unfilled rectangle outline.
@since0.1.0@paramrect - Rectangle bounds in display coordinates.@parampaletteIndex - Palette color index.
drawRect
(const marker: Rect2imarker, this.Demo.theme: null
Palette slots of the shared UI theme colors, filled by applyTheme() in init().
theme
.accent);
} /** * 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(): void
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.
renderKeyboardPanel
() {
import uiui.begin(import UI_ANCHORSUI_ANCHORS.BOTTOM_LEFT); import uiui.panel('Keyboard SFX (pitch)'); 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;
    ... 106 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} Short blip sound played by the pitch buttons.
blipClip
, { VoicePlayOptions.pitch?: number | undefined
Initial `playbackRate`. Defaults to `1`.
pitch
: const preset: anypreset.pitch });
this.Demo.lastKeyPitch: number | null
@type{number | null} Pitch of the most recently played blip, or null before the first press.
lastKeyPitch
= const preset: anypreset.pitch;
} } const const lastPitchLabel: stringlastPitchLabel = this.Demo.lastKeyPitch: number | null
@type{number | null} Pitch of the most recently played blip, or null before the first press.
lastKeyPitch
=== null ? '-' : `${this.Demo.lastKeyPitch: number
@type{number | null} Pitch of the most recently played blip, or null before the first press.
lastKeyPitch
.toFixed(2)}x`;
import uiui.kv('Last pitch', const lastPitchLabel: stringlastPitchLabel); import uiui.end(); } /** * Right-hand panel: the volume and pan of the most recent click. 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(): void
Right-hand panel: the volume and pan of the most recent click. 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.
renderPointerPanel
() {
const const hasClicked: booleanhasClicked = this.Demo.lastClickVolume: number | null
@type{number | null} Volume used for the most recent pop sound.
lastClickVolume
!== null && this.Demo.lastClickPan: number | null
@type{number | null} Pan used for the most recent pop sound.
lastClickPan
!== null;
const const flashing: booleanflashing = this.Demo.pointerFlashTimer: numberpointerFlashTimer > 0; import uiui.begin(import UI_ANCHORSUI_ANCHORS.BOTTOM_RIGHT); import uiui.panel('Pointer SFX (volume/pan)'); // Two short reminders of how a click's position maps to sound. import uiui.label('Top = loud, bottom = quiet', { color: stringcolor: 'dim' }); import uiui.label('Left/right edge = pan', { color: stringcolor: 'dim' }); import uiui.spacer(4); // 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} Volume used for the most recent pop sound.
lastClickVolume
.toFixed(2) : '-');
import uiui.meter(null, const hasClicked: booleanhasClicked ? this.Demo.lastClickVolume: number | null
@type{number | null} Volume used for the most recent pop sound.
lastClickVolume
: 0, { color: stringcolor: const flashing: booleanflashing ? 'accent' : 'info' });
// Pan as a signed number: -1.00 is fully left, 0.00 centered, +1.00 fully right. import uiui.kv('Pan', const hasClicked: booleanhasClicked ? this.Demo.lastClickPan: number | null
@type{number | null} Pan used for the most recent pop sound.
lastClickPan
.toFixed(2) : '-');
import uiui.end(); } /** * Plays the pop sound with a volume and pan taken from where the click landed, and * remembers everything the pointer panel and click marker show. * * @param {Vector2i} pos - Where the click landed, in display pixels. */ Demo.playPopAt(pos: Vector2i): void
Plays the pop sound with a volume and pan taken from where the click landed, and remembers everything the pointer panel and click marker show.
@parampos - Where the click landed, in display pixels.
playPopAt
(pos: Vector2i
- Where the click landed, in display pixels.
@parampos - Where the click landed, in display pixels.
pos
) {
const const screen: Vector2iscreen =
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.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
;
// Turn the click's vertical position into a volume: 0 at the very top of the // screen, 1 at the very bottom. const const verticalFraction: numberverticalFraction = function clamp(value: number, min: number, max: number): number
Keeps a number from going below `min` or above `max`.
@paramvalue - Number to restrict.@parammin - Smallest allowed result.@parammax - Largest allowed result.@returns
clamp
(pos: Vector2i
- Where the click landed, in display pixels.
@parampos - Where the click landed, in display pixels.
pos
.Vector2i.y: number
Vertical component (defaults to 0).
y
/ const screen: Vector2iscreen.Vector2i.y: number
Vertical component (defaults to 0).
y
, 0, 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 click's horizontal position into a pan: 0 at the left edge of the // screen, 1 at the right edge, then stretched out to the -1..+1 range BT.soundPlay // expects. const const horizontalFraction: numberhorizontalFraction = function clamp(value: number, min: number, max: number): number
Keeps a number from going below `min` or above `max`.
@paramvalue - Number to restrict.@parammin - Smallest allowed result.@parammax - Largest allowed result.@returns
clamp
(pos: Vector2i
- Where the click landed, in display pixels.
@parampos - Where the click landed, in display pixels.
pos
.Vector2i.x: number
Horizontal component (defaults to 0).
x
/ const screen: Vector2iscreen.Vector2i.x: number
Horizontal component (defaults to 0).
x
, 0, 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);
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.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} Short pop sound played by clicking.
popClip
, { VoicePlayOptions.volume?: number | undefined
Initial gain in `[0, 1]` (unclamped). Defaults to `1`.
volume
, VoicePlayOptions.pan?: number | undefined
Initial stereo pan in `[-1, 1]` (unclamped). Defaults to `0`.
pan
});
this.Demo.lastClickPos: Vector2i | null
@type{Vector2i | null} Where the pointer was the last time it was clicked.
lastClickPos
= pos: Vector2i
- Where the click landed, in display pixels.
@parampos - Where the click landed, in display pixels.
pos
;
this.Demo.lastClickVolume: number | null
@type{number | null} Volume used for the most recent pop sound.
lastClickVolume
= const volume: numbervolume;
this.Demo.lastClickPan: number | null
@type{number | null} Pan used for the most recent pop sound.
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 Demo
Shows AudioClip loading, BT.soundPlay volume/pitch/pan, and the audio unlock gesture.
@implementsIBTDemo
Demo
);