/*
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 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;
... 124 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, class Vector2iInteger 2D vector for pixel-perfect positioning.
Used for points, sizes, directions, and camera offsets throughout the engine.
The API includes both allocation-free `*To()` / `*InPlace()` variants and
convenience methods that return new vectors.Vector2i } from 'blit386';
import { import applyThemeapplyTheme, import 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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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 | nullpalette = 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 | nullblipClip = null;
// Short pop sound played by clicking the pad.
/** @type {AudioClip | null} */
Demo.popClip: AudioClip | nullpopClip = null;
// Pitch of the most recently played blip, or null before the first press.
/** @type {number | null} */
Demo.lastKeyPitch: number | nulllastKeyPitch = 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 | nullpadSound = null;
// Where the sound sits in the pad: the last pointer position, kept inside the pad.
/** @type {Vector2i | null} */
Demo.lastClickPos: Vector2i | nulllastClickPos = null;
// Volume used for the most recent pop.
/** @type {number | null} */
Demo.lastClickVolume: number | nulllastClickVolume = null;
// Pan used for the most recent pop.
/** @type {number | null} */
Demo.lastClickPan: number | nulllastClickPan = 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 | 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;
... 124 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 pad and click marker in theme colors.
this.Demo.theme: nulltheme = import applyThemeapplyTheme(this.Demo.palette: Palettepalette);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteSet: (palette: Palette) => voidStores the active engine palette.
Use this to swap the **entire palette** (e.g. switch between a day and night
theme). After this call the renderer uploads the new palette uniform on the
next frame.
**Palette-value swap (change what a slot looks like):** mutate the live
{@link
BT.palette
}
in place with `palette.set(slot, newColor)`. The renderer
uploads dirty slots on the next frame; no `paletteSet()` or
{@link
BT.spritesRefresh
}
needed.
**Palette-layout swap (same colors, different slot positions):** build a new
palette with the same colors at new indices, call `paletteSet()`, then call
{@link
BT.spritesRefresh
}
so every sprite sheet re-maps its original RGBA
pixels against the new slot layout.paletteSet(this.Demo.palette: Palettepalette);
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) => 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);
// 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) => 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()`.
`BTN_L2`, `BTN_R2` and `BTN_TRIGGER` are gamepad only (no keyboard mapping).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: numberPrimary pointer button code.
Maps to mouse left for slot 0; touch contact for slots 1-3.BTN_POINTER_A, 0) && const PAD_RECT: Rect2iPAD_RECT.Rect2i.isContainingXY(px: number, py: number): booleanTests raw coordinates against this rectangle without allocating a vector.isContainingXY(const pos: Vector2ipos.Vector2i.x: numberHorizontal component (defaults to 0).x, const pos: Vector2ipos.Vector2i.y: numberVertical component (defaults to 0).y)) {
this.Demo.startPadSound(pos: any): voidstartPadSound(const pos: Vector2ipos);
} else if (this.Demo.padSound: SoundRef | nullpadSound !== 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) => booleanChecks 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).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: numberPrimary pointer button code.
Maps to mouse left for slot 0; touch contact for slots 1-3.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) => voidStops a playing sound, optionally fading it out.soundStop(this.Demo.padSound: SoundRefpadSound, { SoundStopOptions.fadeOutMs?: number | undefinedOptional linear fade-out duration in milliseconds before the voice actually stops.fadeOutMs: const RELEASE_FADE_MS: 40RELEASE_FADE_MS });
this.Demo.padSound: SoundRef | nullpadSound = 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) => voidSets the frame clear color using a palette index.
The renderer uses this color when clearing the full display at the start
of the next frame.clear(this.Demo.theme: nulltheme.bg);
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: 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;
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: 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.Vector2i.x: numberHorizontal 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) => 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.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 | nulllastClickVolume !== null && this.Demo.lastClickPan: number | nulllastClickPan !== 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 | nulllastClickVolume.toFixed(2) : '-');
import uiui.meter(null, const hasClicked: booleanhasClicked ? this.Demo.lastClickVolume: number | nulllastClickVolume : 0, {
color: stringcolor: const flashing: booleanflashing ? 'accent' : 'info',
width: numberwidth: const PAD_RECT: Rect2iPAD_RECT.Rect2i.width: numberWidth 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 | nulllastClickPan > 0 ? '+' : '';
import uiui.kv('Pan', const hasClicked: booleanhasClicked ? `${const panSign: "+" | ""panSign}${this.Demo.lastClickPan: number | nulllastClickPan.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: numberLeft edge X coordinate (defaults to 0).x, const y: numberTop edge Y coordinate (defaults to 0).y, const width: numberWidth in pixels (defaults to 0).width, const height: numberHeight in pixels (defaults to 0).height } = const PAD_RECT: Rect2iPAD_RECT;
const const right: numberright = const x: numberLeft edge X coordinate (defaults to 0).x + const width: numberWidth in pixels (defaults to 0).width - 1;
const const bottom: numberbottom = const y: numberTop edge Y coordinate (defaults to 0).y + const height: numberHeight in pixels (defaults to 0).height - 1;
const const midX: anymidX = const x: numberLeft edge X coordinate (defaults to 0).x + Math.floor(const width: numberWidth in pixels (defaults to 0).width / 2);
const const midY: anymidY = const y: numberTop edge Y coordinate (defaults to 0).y + Math.floor(const height: numberHeight 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: numberTop 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: numberLeft 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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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 | nulllastClickPos === null) {
return;
}
const { const x: numberHorizontal component (defaults to 0).x, const y: numberVertical component (defaults to 0).y } = this.Demo.lastClickPos: Vector2ilastClickPos;
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: numberHorizontal component (defaults to 0).x - Math.floor(const GLYPH_W: 5GLYPH_W / 2), const PAD_RECT: Rect2iPAD_RECT.Rect2i.x: numberLeft edge X coordinate (defaults to 0).x + 1, const PAD_RECT: Rect2iPAD_RECT.Rect2i.x: numberLeft edge X coordinate (defaults to 0).x + const PAD_RECT: Rect2iPAD_RECT.Rect2i.width: numberWidth 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: numberVertical component (defaults to 0).y - Math.floor(const GLYPH_H: 7GLYPH_H / 2), const PAD_RECT: Rect2iPAD_RECT.Rect2i.y: numberTop edge Y coordinate (defaults to 0).y + 1, const PAD_RECT: Rect2iPAD_RECT.Rect2i.y: numberTop edge Y coordinate (defaults to 0).y + const PAD_RECT: Rect2iPAD_RECT.Rect2i.height: numberHeight 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: numberHorizontal component (defaults to 0).x - const CLICK_MARKER_HALF_SIZE: 6CLICK_MARKER_HALF_SIZE, const PAD_RECT: Rect2iPAD_RECT.Rect2i.x: numberLeft edge X coordinate (defaults to 0).x, const PAD_RECT: Rect2iPAD_RECT.Rect2i.x: numberLeft edge X coordinate (defaults to 0).x + const PAD_RECT: Rect2iPAD_RECT.Rect2i.width: numberWidth in pixels (defaults to 0).width - const size: numbersize);
const const top: anytop = function clamp(value: any, min: any, max: any): anyclamp(const y: numberVertical component (defaults to 0).y - const CLICK_MARKER_HALF_SIZE: 6CLICK_MARKER_HALF_SIZE, const PAD_RECT: Rect2iPAD_RECT.Rect2i.y: numberTop edge Y coordinate (defaults to 0).y, const PAD_RECT: Rect2iPAD_RECT.Rect2i.y: numberTop edge Y coordinate (defaults to 0).y + const PAD_RECT: Rect2iPAD_RECT.Rect2i.height: numberHeight in pixels (defaults to 0).height - const size: numbersize);
import uiui.drawRect(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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 | nullpadSound = 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) => 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.loop?: boolean | undefinedWhether 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: numberLeft edge X coordinate (defaults to 0).x, const PAD_RECT: Rect2iPAD_RECT.Rect2i.x: numberLeft edge X coordinate (defaults to 0).x + const PAD_RECT: Rect2iPAD_RECT.Rect2i.width: numberWidth 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: numberTop edge Y coordinate (defaults to 0).y, const PAD_RECT: Rect2iPAD_RECT.Rect2i.y: numberTop edge Y coordinate (defaults to 0).y + const PAD_RECT: Rect2iPAD_RECT.Rect2i.height: numberHeight 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: numberTop edge Y coordinate (defaults to 0).y) / (const PAD_RECT: Rect2iPAD_RECT.Rect2i.height: numberHeight 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: numberLeft edge X coordinate (defaults to 0).x) / (const PAD_RECT: Rect2iPAD_RECT.Rect2i.width: numberWidth 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) => voidSets a sound's gain, optionally fading to it.soundVolumeSet(this.Demo.padSound: SoundRef | nullpadSound, 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) => voidSets a sound's stereo pan, optionally fading to it.soundPanSet(this.Demo.padSound: SoundRef | nullpadSound, const pan: numberpan);
this.Demo.lastClickPos: Vector2i | nulllastClickPos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const x: anyx, const y: anyy);
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 DemoDemo);