/**
* 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 AudioClipDecoded 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.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.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 Rect2iInteger rectangle for pixel-perfect bounds and regions.
Used throughout the engine for sprite regions, display-space bounds, and
zero-allocation geometry helpers. Both convenience getters and allocation-free
`*To()` helpers are provided so callers can choose between readability and
hot-path efficiency.Rect2i } 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): numberKeeps a number from going below `min` or above `max`.clamp(value: number- Number to restrict.value, min: number- Smallest allowed result.min, max: number- Largest allowed result.max) {
return Math.max(min: number- Smallest allowed result.min, Math.min(max: number- Largest allowed result.max, value: number- Number to restrict.value));
}
/**
* Shows AudioClip loading, BT.soundPlay volume/pitch/pan, and the audio unlock gesture.
*
* @implements {IBTDemo}
*/
class class DemoShows AudioClip loading, BT.soundPlay volume/pitch/pan, and the audio unlock gesture.Demo {
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = null;
/** Palette slots of the shared UI theme colors, filled by applyTheme() in init(). */
Demo.theme: nullPalette 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 | nullblipClip = null;
/** @type {AudioClip | null} Short pop sound played by clicking. */
Demo.popClip: AudioClip | nullpopClip = null;
/** @type {number | null} Pitch of the most recently played blip, or null before the first press. */
Demo.lastKeyPitch: number | nulllastKeyPitch = 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 | nulllastClickPos = null;
/** @type {number | null} Volume used for the most recent pop sound. */
Demo.lastClickVolume: number | nulllastClickVolume = null;
/** @type {number | null} Pan used for the most recent pop sound. */
Demo.lastClickPan: number | nulllastClickPan = 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.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.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 | nullblipClip = await class AudioClipDecoded 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.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.load('/audio/blip.wav');
this.Demo.popClip: AudioClip | nullpopClip = await class AudioClipDecoded 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.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.load('/audio/pop.wav');
this.Demo.palette: Palette | nullpalette = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteCreate: (size?: number) => PaletteCreates a standalone palette instance.paletteCreate(256);
// 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: nullPalette slots of the shared UI theme colors, filled by applyTheme() in init().theme = import applyThemeapplyTheme(this.Demo.palette: Palettepalette);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteSet: (palette: Palette) => voidStores the active engine palette.
Use this to swap the **entire palette** (e.g. switch between a day and night
theme). After this call the renderer uploads the new palette uniform on the
next frame.
**Palette-value swap (change what a slot looks like):** mutate the live
{@link
BT.palette
}
in place with `palette.set(slot, newColor)`. The renderer
uploads dirty slots on the next frame; no `paletteSet()` or
{@link
BT.spritesRefresh
}
needed.
**Palette-layout swap (same colors, different slot positions):** build a new
palette with the same colors at new indices, call `paletteSet()`, then call
{@link
BT.spritesRefresh
}
so every sprite sheet re-maps its original RGBA
pixels against the new slot layout.paletteSet(this.Demo.palette: Palettepalette);
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(): voidRuns 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) => booleanChecks 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()`.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: numberPrimary pointer button code.
Maps to mouse left for slot 0; touch contact for slots 1-3.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) => Vector2iReturns 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.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: numberHorizontal component (defaults to 0).x, const pos: Vector2ipos.Vector2i.y: numberVertical component (defaults to 0).y)) {
this.Demo.playPopAt(pos: Vector2i): voidPlays the pop sound with a volume and pan taken from where the click landed, and
remembers everything the pointer panel and click marker show.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(): voidClears 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) => voidSets the frame clear color using a palette index.
The renderer uses this color when clearing the full display at the start
of the next frame.clear(this.Demo.theme: nullPalette 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(): voidDraws a small square ring where you last clicked, only while its flash is active.renderClickMarker();
this.Demo.renderStatusLine(): voidThe 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(): voidLeft-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(): voidRight-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(): voidThe 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: booleanWhether 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.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(): voidDraws a small square ring where you last clicked, only while its flash is active.renderClickMarker() {
if (this.Demo.lastClickPos: Vector2i | nulllastClickPos === null || this.Demo.pointerFlashTimer: numberpointerFlashTimer === 0) {
return;
}
const const marker: Rect2imarker = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(
this.Demo.lastClickPos: Vector2ilastClickPos.Vector2i.x: numberHorizontal component (defaults to 0).x - const CLICK_MARKER_HALF_SIZE: 6CLICK_MARKER_HALF_SIZE,
this.Demo.lastClickPos: Vector2ilastClickPos.Vector2i.y: numberVertical 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) => voidDraws an unfilled rectangle outline.drawRect(const marker: Rect2imarker, this.Demo.theme: nullPalette 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(): voidLeft-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) => SoundRefPlays 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.soundPlay(this.Demo.blipClip: AudioClip | nullblipClip, { VoicePlayOptions.pitch?: number | undefinedInitial `playbackRate`. Defaults to `1`.pitch: const preset: anypreset.pitch });
this.Demo.lastKeyPitch: number | nulllastKeyPitch = const preset: anypreset.pitch;
}
}
const const lastPitchLabel: stringlastPitchLabel = this.Demo.lastKeyPitch: number | nulllastKeyPitch === null ? '-' : `${this.Demo.lastKeyPitch: numberlastKeyPitch.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(): voidRight-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 | nulllastClickVolume !== null && this.Demo.lastClickPan: number | nulllastClickPan !== 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 | nulllastClickVolume.toFixed(2) : '-');
import uiui.meter(null, const hasClicked: booleanhasClicked ? this.Demo.lastClickVolume: number | nulllastClickVolume : 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 | nulllastClickPan.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): voidPlays the pop sound with a volume and pan taken from where the click landed, and
remembers everything the pointer panel and click marker show.playPopAt(pos: Vector2i- 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: Vector2iActive 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.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): numberKeeps a number from going below `min` or above `max`.clamp(pos: Vector2i- Where the click landed, in display pixels.pos.Vector2i.y: numberVertical component (defaults to 0).y / const screen: Vector2iscreen.Vector2i.y: numberVertical 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): numberKeeps a number from going below `min` or above `max`.clamp(pos: Vector2i- Where the click landed, in display pixels.pos.Vector2i.x: numberHorizontal component (defaults to 0).x / const screen: Vector2iscreen.Vector2i.x: numberHorizontal 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) => SoundRefPlays 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.soundPlay(this.Demo.popClip: AudioClip | nullpopClip, { VoicePlayOptions.volume?: number | undefinedInitial gain in `[0, 1]` (unclamped). Defaults to `1`.volume, VoicePlayOptions.pan?: number | undefinedInitial stereo pan in `[-1, 1]` (unclamped). Defaults to `0`.pan });
this.Demo.lastClickPos: Vector2i | nulllastClickPos = pos: Vector2i- Where the click landed, in display pixels.pos;
this.Demo.lastClickVolume: number | nulllastClickVolume = const volume: numbervolume;
this.Demo.lastClickPan: number | nulllastClickPan = 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.bootstrap(class DemoShows AudioClip loading, BT.soundPlay volume/pitch/pan, and the audio unlock gesture.Demo);