/*

Music Playback: crossfade between two tracks and loop a third from a chosen point.
@description Crossfade two looping tracks with different fade profiles, plus a third with a seamless loop point.
@pageTitle BLIT386 Demo – Music Playback
@ogScale integer

Part of the BLIT386 demo series.

Prerequisites:
  - Audio Basics: https://demos.blit386.dev/audio-basics

Live version: https://demos.blit386.dev/music

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

Reference:
  - Audio: https://blit386.dev/docs/api/audio#playback-music
  - 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: "Press 1, 2, or 3 to switch tracks."

Two panels of the same height sit side by side under that line. "Tracks" on the left has
three buttons, "1 - A: calm", "2 - B: upbeat", and "3 - Intro + loop", the one playing
lit green, and a "Playing" row naming it.

"Crossfade" on the right draws the switch that brought the current track in: a dark chart
where time runs left to right and volume bottom to top. The old track's dim "out" line
falls from full volume to silence, and the new track's green "in" line rises. Every
profile uses the same time scale, so a longer fade really looks longer. While a switch is
still fading, an amber line moves across the chart to show how far along it is. Under the
chart, "Fade", "Overlap", "Curve", and "Loop" rows list the numbers behind it.

Track A starts playing on its own the moment sound is unlocked - you never have to press
its button first.

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

- BT.musicPlay(clip, options) drives one music player. Calling it again crossfades from
  the current track into the new one instead of layering the two.
- fadeMs sets how long each side of a crossfade takes, and overlap sets how the two sides
  line up: 1 fades both at the same time, 0 starts the fade-in exactly when the fade-out
  ends, and -1 leaves a silent gap of fadeMs between them.
- easeIn and easeOut pick an easing curve for each side ('linear' or 'ease-in-out' here).
  applyEasing(t, curve) gives the same curve as a number, which is how the chart draws it.
- loop: true repeats the whole track; loopStart and loopEnd (in seconds) repeat only a
  region, so an intro plays once and the rest loops forever.
- BT.musicPlay() called before sound is unlocked is remembered and starts the instant the
  first click or key press arrives - unlike BT.soundPlay(), which drops early sounds.
- Promise.all() loads three clips at once instead of one after another.
- isOverlayAudioMetersEnabled: true adds live bus level bars to the engine overlay, and
  overlayStyle / overlayAudioMeterStyle pick its colors from the palette.

HOW THE THREE PROFILES DIFFER
-----------------------------

Sound effects are short one-shot clips: press a key, hear a blip, done. Music is meant to
loop in the background, and switching tracks should not just cut off with a click. Each
button here uses a different crossfade recipe:

  1. To Track A: fadeMs 800, overlap -1, linear curves. The old track fades out over
     800 ms, there is 800 ms of silence, then Track A fades in over 800 ms. Listen for
     the gap in the middle.
  2. To Track B: fadeMs 1200, overlap 1, ease-in-out curves. The old track fades out
     while Track B fades in, both at the same time, so the music blends with no gap. The
     curve starts and ends gently instead of moving at one constant speed.
  3. To Loop Demo: fadeMs 600, overlap 1, linear curves - a quick, plain overlap. This
     track also sets loopStart 1.5 and loopEnd 7.9, so its intro plays once and then the
     1.5-7.9 second region repeats, like the opening jingle of a game level settling into
     its main tune.

TRY THIS
--------

- Click anywhere, or press any key, to unlock sound. Track A starts by itself.
- Press 1, 2, or 3 (or tap a button) to switch tracks. Pressing the button of the track
  that is already playing does nothing.
- Switch from Track B back to Track A: the chart shows the gap between the two lines, and
  you hear the short silence in the middle. Switch to Track B again and both happen at
  once - no gap.
- Play Loop Demo and wait: the intro only ever plays once, then the loop region repeats
  without a seam.
- Watch the overlay's music level bar during a crossfade: it dips and rises just like the
  lines in the chart. Press the backquote key (`) or click the small icon in the
  bottom-left corner to hide or show the overlay.

*/

import { function applyEasing(t: number, easing: EasingFunction): number
Applies an easing curve to a normalized time value.
@since1.0.3@changed1.5.0 Added sine, cubic, quartic, quintic, expo, circ, back, elastic, and bounce families.@paramt - Normalized time in [0, 1]. Values outside this range are not clamped.@parameasing - Easing curve to apply.@returnsEased value. Guaranteed to return 0 for `t = 0` and 1 for `t = 1`.
applyEasing
, class AudioClip
Decoded audio asset with its winning source URL and buffer-derived metadata. Construct instances with {@link AudioClip.load } , {@link AudioClip.loadAll } , or {@link AudioClip.synth } ; there is no public constructor.
@since1.3.0
AudioClip
, function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
,
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
, class Color32
Mutable 32-bit RGBA color value with 8-bit channels.
@since0.1.0
Color32
, class Rect2i
Integer rectangle for pixel-perfect bounds and regions. Used throughout the engine for sprite regions, display-space bounds, and zero-allocation geometry helpers. Both convenience getters and allocation-free `*To()` helpers are provided so callers can choose between readability and hot-path efficiency.
@since0.1.0
Rect2i
, class Vector2i
Integer 2D vector for pixel-perfect positioning. Used for points, sizes, directions, and camera offsets throughout the engine. The API includes both allocation-free `*To()` / `*InPlace()` variants and convenience methods that return new vectors.
@since0.1.0
Vector2i
} from 'blit386';
import { import applyThemeapplyTheme, import FONT_WFONT_W, import litRimColorlitRimColor, 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 */ const const DISPLAY_W: 320DISPLAY_W = 320; const const DISPLAY_H: 240DISPLAY_H = 240; // These two numbers come straight out of public/audio/music-intro-loop.loop.json, // generated by scripts/generate-audio-loops.mjs. They mark where the short intro ends and // where the repeating loop section begins and ends inside that one audio file, in // seconds. const const INTRO_LOOP_START_SECONDS: 1.5INTRO_LOOP_START_SECONDS = 1.5; const const INTRO_LOOP_END_SECONDS: 7.9INTRO_LOOP_END_SECONDS = 7.9; // Fading to Track A: the old track fades all the way out first, then - after a silent gap // as long as the fade itself - Track A fades in. `overlap: -1` is what creates that gap. const
const PROFILE_TO_A: {
    fadeMs: number;
    overlap: number;
    easeIn: string;
    easeOut: string;
}
PROFILE_TO_A
= { fadeMs: numberfadeMs: 800, overlap: numberoverlap: -1, easeIn: stringeaseIn: 'linear', easeOut: stringeaseOut: 'linear' };
// Fading to Track B: the new track fades in at the same time as the old one fades out // (`overlap: 1`), and both fades use an ease-in-out curve so the volume change starts and // ends gently instead of moving at one constant speed the whole way through. const
const PROFILE_TO_B: {
    fadeMs: number;
    overlap: number;
    easeIn: string;
    easeOut: string;
}
PROFILE_TO_B
= { fadeMs: numberfadeMs: 1200, overlap: numberoverlap: 1, easeIn: stringeaseIn: 'ease-in-out', easeOut: stringeaseOut: 'ease-in-out' };
// Fading into the loop-point track: a plain, fairly quick overlapping crossfade. const
const PROFILE_TO_LOOP: {
    fadeMs: number;
    overlap: number;
    easeIn: string;
    easeOut: string;
}
PROFILE_TO_LOOP
= { fadeMs: numberfadeMs: 600, overlap: numberoverlap: 1, easeIn: stringeaseIn: 'linear', easeOut: stringeaseOut: 'linear' };
// One entry per track button: which track it plays, the button text (it starts with the // number key the kit binds to it), the name shown once it plays, its crossfade profile, // and a short word for that profile's overlap. const const TRACKS: {}TRACKS = [ { trackId: stringtrackId: 'A', label: stringlabel: '1 - A: calm', name: stringname: 'A: calm', keyCode: stringkeyCode: 'Digit1',
profile: {
    fadeMs: number;
    overlap: number;
    easeIn: string;
    easeOut: string;
}
profile
:
const PROFILE_TO_A: {
    fadeMs: number;
    overlap: number;
    easeIn: string;
    easeOut: string;
}
PROFILE_TO_A
,
overlapNote: stringoverlapNote: 'gap', }, { trackId: stringtrackId: 'B', label: stringlabel: '2 - B: upbeat', name: stringname: 'B: upbeat', keyCode: stringkeyCode: 'Digit2',
profile: {
    fadeMs: number;
    overlap: number;
    easeIn: string;
    easeOut: string;
}
profile
:
const PROFILE_TO_B: {
    fadeMs: number;
    overlap: number;
    easeIn: string;
    easeOut: string;
}
PROFILE_TO_B
,
overlapNote: stringoverlapNote: 'blend', }, { trackId: stringtrackId: 'loop', label: stringlabel: '3 - Intro + loop', name: stringname: 'Intro + loop', keyCode: stringkeyCode: 'Digit3',
profile: {
    fadeMs: number;
    overlap: number;
    easeIn: string;
    easeOut: string;
}
profile
:
const PROFILE_TO_LOOP: {
    fadeMs: number;
    overlap: number;
    easeIn: string;
    easeOut: string;
}
PROFILE_TO_LOOP
,
overlapNote: stringoverlapNote: 'blend', }, ]; // 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 track buttons fill the panel's width. The one playing is lit in the same green the // Keyboard Diagnostic demo lights a held key with, rimmed with a lighter shade of it // (written in init()). The shared UI kit owns slots 240-251, so these sit far below. const const TRACK_BUTTON_W: numberTRACK_BUTTON_W = const LEFT_PANEL_W: 140LEFT_PANEL_W - const PANEL_CONTENT_SHRINK: 20PANEL_CONTENT_SHRINK; const const C_LIT: 1C_LIT = 1; const const C_LIT_RIM: 2C_LIT_RIM = 2; const
const LIT_BUTTON: {
    fill: number;
    rim: number;
}
LIT_BUTTON
= { fill: numberfill: const C_LIT: 1C_LIT, rim: numberrim: const C_LIT_RIM: 2C_LIT_RIM };
// The left panel's rows are shorter than the right panel's, so this much empty space // before its "Playing" row makes both panels end on the same line. const const LEFT_PANEL_FILLER: 4LEFT_PANEL_FILLER = 4; // The crossfade chart at the top of the right panel: two volume lines over time, the old // track fading out and the new one fading in, drawn for the profile in use. const const CHART_H: 65CHART_H = 65; const const CHART_RECT: Rect2iCHART_RECT = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(
const RIGHT_PANEL_X: 148RIGHT_PANEL_X + const PANEL_CONTENT_X: 9PANEL_CONTENT_X, const PANEL_Y: 76PANEL_Y + const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP, const RIGHT_PANEL_W: 168RIGHT_PANEL_W - const PANEL_CONTENT_SHRINK: 20PANEL_CONTENT_SHRINK, const CHART_H: 65CHART_H, ); // Gap between the chart and the rows under it. const const CHART_GAP: 4CHART_GAP = 4; // The plot inside the chart's border: full volume on the top row, silence on the bottom // row, and room above it for the "out" and "in" labels. const const PLOT_INSET_X: 6PLOT_INSET_X = 6; const const PLOT_TOP: numberPLOT_TOP = const CHART_RECT: Rect2iCHART_RECT.Rect2i.y: number
Top edge Y coordinate (defaults to 0).
y
+ 14;
const const PLOT_BOTTOM: numberPLOT_BOTTOM = const CHART_RECT: Rect2iCHART_RECT.Rect2i.y: number
Top edge Y coordinate (defaults to 0).
y
+ const CHART_H: 65CHART_H - 5;
const const PLOT_LEFT: numberPLOT_LEFT = const CHART_RECT: Rect2iCHART_RECT.Rect2i.x: number
Left edge X coordinate (defaults to 0).
x
+ const PLOT_INSET_X: 6PLOT_INSET_X;
const const PLOT_RIGHT: numberPLOT_RIGHT = const CHART_RECT: Rect2iCHART_RECT.Rect2i.x: number
Left edge X coordinate (defaults to 0).
x
+ const CHART_RECT: Rect2iCHART_RECT.Rect2i.width: number
Width in pixels (defaults to 0).
width
- 1 - const PLOT_INSET_X: 6PLOT_INSET_X;
// Every profile is drawn on the same time scale, so a longer fade really looks longer. // The chart spans this many milliseconds: a short stretch at full volume before the fade, // the slowest profile's crossfade (800 ms out, 800 ms gap, 800 ms in), and the same // stretch after it. const const CHART_LEAD_MS: 200CHART_LEAD_MS = 200; const const CHART_SPAN_MS: numberCHART_SPAN_MS = 2400 + 2 * const CHART_LEAD_MS: 200CHART_LEAD_MS; // The crossfade panel's value column starts after this many characters, so the longest // key, "Overlap", keeps a space after it. const const FADE_KV_COLS: 9FADE_KV_COLS = 9; // 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); } // How long a profile's whole crossfade takes, in milliseconds. The fade-in starts // fadeMs * (1 - overlap) after the fade-out does: right away for overlap 1, when the // fade-out ends for 0, and one fadeMs after that (a silent gap) for -1. function function crossfadeMs(profile: any): numbercrossfadeMs(profile: anyprofile) { return profile: anyprofile.fadeMs * (2 - profile: anyprofile.overlap); } // The volumes of the old and the new track `ms` milliseconds after a switch, from 0 to 1, // using the same easing curves BT.musicPlay() uses. Writes them into `out` so the chart // can ask about every column without making new objects. function function crossfadeVolumes(profile: any, ms: any, out: any): voidcrossfadeVolumes(profile: anyprofile, ms: anyms, out: anyout) { const const inStartMs: numberinStartMs = profile: anyprofile.fadeMs * (1 - profile: anyprofile.overlap); const const outT: anyoutT = function clamp(value: any, min: any, max: any): anyclamp(ms: anyms / profile: anyprofile.fadeMs, 0, 1); const const inT: anyinT = function clamp(value: any, min: any, max: any): anyclamp((ms: anyms - const inStartMs: numberinStartMs) / profile: anyprofile.fadeMs, 0, 1); out: anyout.old = 1 - function applyEasing(t: number, easing: EasingFunction): number
Applies an easing curve to a normalized time value.
@since1.0.3@changed1.5.0 Added sine, cubic, quartic, quintic, expo, circ, back, elastic, and bounce families.@paramt - Normalized time in [0, 1]. Values outside this range are not clamped.@parameasing - Easing curve to apply.@returnsEased value. Guaranteed to return 0 for `t = 0` and 1 for `t = 1`.
applyEasing
(const outT: anyoutT, profile: anyprofile.easeOut);
out: anyout.new = function applyEasing(t: number, easing: EasingFunction): number
Applies an easing curve to a normalized time value.
@since1.0.3@changed1.5.0 Added sine, cubic, quartic, quintic, expo, circ, back, elastic, and bounce families.@paramt - Normalized time in [0, 1]. Values outside this range are not clamped.@parameasing - Easing curve to apply.@returnsEased value. Guaranteed to return 0 for `t = 0` and 1 for `t = 1`.
applyEasing
(const inT: anyinT, profile: anyprofile.easeIn);
} // Three buttons, each starting a different music track with a different crossfade // profile, and a chart that draws what that crossfade does to both tracks' volume. class class DemoDemo { /** @type {Palette | null} */ Demo.palette: Palette | null
@type{Palette | null}
palette
= null;
// Palette slots of the shared UI theme colors, filled by applyTheme() in init(). Demo.theme: nulltheme = null; /** @type {AudioClip | null} */ Demo.calmClip: AudioClip | null
@type{AudioClip | null}
calmClip
= null;
/** @type {AudioClip | null} */ Demo.upbeatClip: AudioClip | null
@type{AudioClip | null}
upbeatClip
= null;
/** @type {AudioClip | null} */ Demo.introLoopClip: AudioClip | null
@type{AudioClip | null}
introLoopClip
= null;
// Which button's track is playing: 'A', 'B', 'loop', or null before the first play. /** @type {string | null} */ Demo.activeTrackId: string | null
@type{string | null}
activeTrackId
= null;
// The tick of the last track switch, so the chart can move a marker along the // crossfade while it plays. Null for the first track, which has nothing to fade from. /** @type {number | null} */ Demo.switchTick: number | null
@type{number | null}
switchTick
= null;
// Scratch object crossfadeVolumes() writes into, reused for every chart column.
Demo.volumes: {
    old: number;
    new: number;
}
volumes
= { old: numberold: 0, new: numbernew: 0 };
// Sets the logical display size and opens the overlay with its audio meters, colored // from the shared UI theme.
Demo.configure(): {
    displaySize: Vector2i;
    isOverlayAudioMetersEnabled: boolean;
    isOverlayVisibleAtStart: boolean;
    overlayStyle: any;
    overlayAudioMeterStyle: any;
}
configure
() {
return { displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const DISPLAY_W: 320DISPLAY_W, const DISPLAY_H: 240DISPLAY_H),
// 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 all three music clips, sets up the shared UI theme, and starts Track A. async Demo.init(): unknowninit() { // Load all three tracks at once - Promise.all waits for the slowest fetch, not // the sum of three downloads one after another. [this.Demo.calmClip: AudioClip | null
@type{AudioClip | null}
calmClip
, this.Demo.upbeatClip: AudioClip | null
@type{AudioClip | null}
upbeatClip
, this.Demo.introLoopClip: AudioClip | null
@type{AudioClip | null}
introLoopClip
] = await Promise.all([
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/music-calm.wav'),
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/music-upbeat.wav'),
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/music-intro-loop.wav'),
]); this.Demo.palette: Palette | null
@type{Palette | null}
palette
=
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.paletteCreate: (size?: number) => Palette
Creates a standalone palette instance.
@since1.0.3@paramsize - Palette size. Defaults to 256 colors.@returnsNew mutable palette.
paletteCreate
(256);
// applyTheme() installs the twelve shared UI colors (into high palette slots, far // from any scene colors) and hands back where they landed, so render() can clear // the screen and draw the chart in theme colors. this.Demo.theme: nulltheme = import applyThemeapplyTheme(this.Demo.palette: Palette
@type{Palette | null}
palette
);
// The playing track's button: green, with a rim worked out from the theme so it // stands out as much as a resting button's rim does. this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_LIT: 1C_LIT, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(75, 210, 120));
this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_LIT_RIM: 2C_LIT_RIM, import litRimColorlitRimColor(this.Demo.palette: Palette
@type{Palette | null}
palette
, this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.get(index: number): Color32
Returns a copy of the color stored at a palette index. Returns a clone so callers cannot mutate internal state without going through {@link set } , which keeps the dirty flag accurate.
@paramindex - Palette index to read.@returnsClone of the stored color entry.@throwsError if the index is invalid.
get
(const C_LIT: 1C_LIT)));
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.paletteSet: (palette: Palette) => void
Stores the active engine palette. Use this to swap the **entire palette** (e.g. switch between a day and night theme). After this call the renderer uploads the new palette uniform on the next frame. **Palette-value swap (change what a slot looks like):** mutate the live {@link BT.palette } in place with `palette.set(slot, newColor)`. The renderer uploads dirty slots on the next frame; no `paletteSet()` or {@link BT.spritesRefresh } needed. **Palette-layout swap (same colors, different slot positions):** build a new palette with the same colors at new indices, call `paletteSet()`, then call {@link BT.spritesRefresh } so every sprite sheet re-maps its original RGBA pixels against the new slot layout.
@since1.0.3@parampalette - Palette to make active.
paletteSet
(this.Demo.palette: Palette
@type{Palette | null}
palette
);
// Start with Track A so there is always music, even before you press anything. // BT.musicPlay() called before the page is unlocked is remembered and starts for // real the instant you click or press a key - unlike BT.soundPlay(), which drops // sounds played too early. this.Demo.playTrack(trackId: any): voidplayTrack('A'); return true; } // Once-per-tick housekeeping for the UI kit. // // ui.tick() is what safely catches the number-key shortcuts bound to the buttons in // render(). Keyboard presses can only be read reliably here in update(), never in // render() - the engine clears "was this just pressed?" flags once per tick, and that // tick always finishes before this frame's render() runs (keyboard-input explains // this in more detail). The kit latches the presses now so the buttons can answer // later, during render(). Demo.update(): voidupdate() { import uiui.tick(); } // Clears the screen and declares the whole UI: the status line, the track panel, and // the crossfade panel with its chart. // // With the immediate-mode kit there is no separate "handle input" step: ui.button() // returns true on the frame it was clicked, tapped, or its number key was pressed, so // the track switch happens right where the button is declared. Demo.render(): voidrender() {
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.clear: (paletteIndex: number) => void
Sets the frame clear color using a palette index. The renderer uses this color when clearing the full display at the start of the next frame.
@since0.1.0@parampaletteIndex - Palette index for the full-screen clear pass.
clear
(this.Demo.theme: nulltheme.bg);
this.Demo.renderStatusLine(): voidrenderStatusLine(); // The track panel goes first: its buttons may switch tracks this very frame, and // everything drawn after them should already show the new one. this.Demo.renderTrackPanel(): voidrenderTrackPanel(); const const active: anyactive = this.Demo.activeTrack(): anyactiveTrack(); this.Demo.renderFadePanel(active: any): voidrenderFadePanel(const active: anyactive); this.Demo.renderChart(profile: any): voidrenderChart(const active: anyactive.profile); } // The TRACKS entry of the track playing now. Array.find() walks the list and returns // the first entry the test function says yes to. Demo.activeTrack(): anyactiveTrack() { return const TRACKS: {}TRACKS.find((track: anytrack) => track: anytrack.trackId === this.Demo.activeTrackId: string | null
@type{string | null}
activeTrackId
);
} // One centered line just under the overlay bars: the shared unlock reminder in warm // orange until sound is unlocked, then a dim reminder of the controls. Demo.renderStatusLine(): voidrenderStatusLine() { const const isLocked: booleanisLocked = !
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.isAudioUnlocked: boolean
Whether the audio context has been unlocked by a user gesture. Browsers require a user gesture (pointer, key, or touch press) before allowing audio playback. Starts `false`; flips to `true` for the rest of the session after the first gesture successfully resumes the audio context.
@since1.3.0@returns`true` once unlocked; `false` when locked or before initialization.
isAudioUnlocked
;
const const text: "Click or press a key to enable sound" | "Press 1, 2, or 3 to switch tracks."text = const isLocked: booleanisLocked ? 'Click or press a key to enable sound' : 'Press 1, 2, or 3 to switch tracks.'; const const x: numberx = function centeredTextX(centerX: any, text: any): numbercenteredTextX(Math.floor(const DISPLAY_W: 320DISPLAY_W / 2), const text: "Click or press a key to enable sound" | "Press 1, 2, or 3 to switch tracks."text); import uiui.caption(const x: numberx, const STATUS_Y: 64STATUS_Y, const text: "Click or press a key to enable sound" | "Press 1, 2, or 3 to switch tracks."text, { color: stringcolor: const isLocked: booleanisLocked ? 'warm' : 'dim' }); } // Left-hand panel: one button per track, the one playing lit green, and its name in // the last row. Demo.renderTrackPanel(): voidrenderTrackPanel() { 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('Tracks'); import uiui.label('Keys 1, 2, 3 or tap', { color: stringcolor: 'dim' }); import uiui.spacer(4); // Each button fires on a click, a tap, or its number key - the kit binds the key // and treats all three the same. for (const const track: anytrack of const TRACKS: {}TRACKS) { const
const lit: {
    fill: number;
    rim: number;
} | undefined
lit
= const track: anytrack.trackId === this.Demo.activeTrackId: string | null
@type{string | null}
activeTrackId
?
const LIT_BUTTON: {
    fill: number;
    rim: number;
}
LIT_BUTTON
: var undefinedundefined;
if (import uiui.button(const track: anytrack.label, { key: anykey: const track: anytrack.keyCode, width: numberwidth: const TRACK_BUTTON_W: numberTRACK_BUTTON_W,
lit: {
    fill: number;
    rim: number;
} | undefined
lit
})) {
this.Demo.playTrack(trackId: any): voidplayTrack(const track: anytrack.trackId); } } // Looked up after the buttons, so a switch made just now already shows. const const active: anyactive = this.Demo.activeTrack(): anyactiveTrack(); import uiui.spacer(const LEFT_PANEL_FILLER: 4LEFT_PANEL_FILLER); import uiui.kv('Playing', const active: anyactive.name); import uiui.end(); } // Right-hand panel: room for the chart, then the numbers behind it - the fade length, // the overlap, the easing curve, and what part of the track loops. Demo.renderFadePanel(active: any): voidrenderFadePanel(active: anyactive) { const { const profile: anyprofile } = active: anyactive; const const overlapSign: "+" | ""overlapSign = const profile: anyprofile.overlap > 0 ? '+' : ''; const const loopText: "1.5s - 7.9s" | "whole track"loopText = active: anyactive.trackId === 'loop' ? `${const INTRO_LOOP_START_SECONDS: 1.5INTRO_LOOP_START_SECONDS}s - ${const INTRO_LOOP_END_SECONDS: 7.9INTRO_LOOP_END_SECONDS}s` : 'whole track'; 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, kvCols: numberkvCols: const FADE_KV_COLS: 9FADE_KV_COLS }); import uiui.panel('Crossfade'); // An empty row as tall as the chart: renderChart() paints the chart into this gap // after the panel is drawn, so it lands on top. import uiui.spacer(const CHART_H: 65CHART_H + const CHART_GAP: 4CHART_GAP); import uiui.kv('Fade', `${const profile: anyprofile.fadeMs}ms`); import uiui.kv('Overlap', `${const overlapSign: "+" | ""overlapSign}${const profile: anyprofile.overlap} (${active: anyactive.overlapNote})`); import uiui.kv('Curve', const profile: anyprofile.easeIn); import uiui.kv('Loop', const loopText: "1.5s - 7.9s" | "whole track"loopText); import uiui.end(); } // The crossfade chart: time runs left to right, volume bottom to top. The old track's // line (dim) falls from full to silent, the new track's line (green) rises. While a // switch is still fading, a marker shows how far along it is. Demo.renderChart(profile: any): voidrenderChart(profile: anyprofile) { import uiui.drawRectFill(const CHART_RECT: Rect2iCHART_RECT, this.Demo.theme: nulltheme.shadow); import uiui.drawRect(const CHART_RECT: Rect2iCHART_RECT, this.Demo.theme: nulltheme.border); // Faint guides for full volume and silence. import uiui.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(const PLOT_LEFT: numberPLOT_LEFT, const PLOT_TOP: numberPLOT_TOP, const PLOT_RIGHT: numberPLOT_RIGHT - const PLOT_LEFT: numberPLOT_LEFT + 1, 1), this.Demo.theme: nulltheme.panel);
import uiui.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(const PLOT_LEFT: numberPLOT_LEFT, const PLOT_BOTTOM: numberPLOT_BOTTOM, const PLOT_RIGHT: numberPLOT_RIGHT - const PLOT_LEFT: numberPLOT_LEFT + 1, 1), this.Demo.theme: nulltheme.panel);
// The marker goes first, so the two lines draw over it. this.Demo.renderChartMarker(profile: any): voidrenderChartMarker(profile: anyprofile); this.Demo.renderChartLine(profile: any, which: any, slot: any): voidrenderChartLine(profile: anyprofile, 'old', this.Demo.theme: nulltheme.dim); this.Demo.renderChartLine(profile: any, which: any, slot: any): voidrenderChartLine(profile: anyprofile, 'new', this.Demo.theme: nulltheme.accent); // "out" above the old line where it starts, "in" above the new line where it // ends. A caption's letters start 1 pixel above the y it is placed at. const const labelY: numberlabelY = const CHART_RECT: Rect2iCHART_RECT.Rect2i.y: number
Top edge Y coordinate (defaults to 0).
y
+ 5;
import uiui.caption(const PLOT_LEFT: numberPLOT_LEFT, const labelY: numberlabelY, 'out', { color: stringcolor: 'dim' }); import uiui.caption(const PLOT_RIGHT: numberPLOT_RIGHT - (2 * import FONT_WFONT_W - 1) + 1, const labelY: numberlabelY, 'in', { color: stringcolor: 'accent' }); } // One volume line across the chart, `which` being 'old' or 'new'. Each column gets a // 1-pixel-wide bar from its own height to the previous column's, so the line stays // connected even where it drops steeply. Flat stretches are gathered into one wide // bar instead of one draw per column. Demo.renderChartLine(profile: any, which: any, slot: any): voidrenderChartLine(profile: anyprofile, which: anywhich, slot: anyslot) { let let prevY: anyprevY = null; let let runStart: numberrunStart = const PLOT_LEFT: numberPLOT_LEFT; for (let let col: numbercol = const PLOT_LEFT: numberPLOT_LEFT; let col: numbercol <= const PLOT_RIGHT: numberPLOT_RIGHT + 1; let col: numbercol++) { const const lineY: anylineY = let col: numbercol <= const PLOT_RIGHT: numberPLOT_RIGHT ? this.Demo.lineY(profile: any, which: any, col: any): anylineY(profile: anyprofile, which: anywhich, let col: numbercol) : null; // Still flat: keep growing the current run. if (const lineY: anylineY !== null && const lineY: anylineY === let prevY: anyprevY) { continue; } // The line moved (or the chart ended): draw the finished flat run... if (let prevY: anyprevY !== null) { import uiui.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(let runStart: numberrunStart, let prevY: anyprevY, let col: numbercol - let runStart: numberrunStart, 1), slot: anyslot);
} // ...and a bar joining it to the new height, so a steep drop has no holes. if (const lineY: anylineY !== null && let prevY: anyprevY !== null) { const const top: anytop = Math.min(const lineY: anylineY, let prevY: anyprevY); import uiui.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(let col: numbercol, const top: anytop, 1, Math.abs(const lineY: anylineY - let prevY: anyprevY) + 1), slot: anyslot);
} let runStart: numberrunStart = let col: numbercol; let prevY: anyprevY = const lineY: anylineY; } } // Which row the 'old' or 'new' volume line sits on in chart column `col`. Demo.lineY(profile: any, which: any, col: any): anylineY(profile: anyprofile, which: anywhich, col: anycol) { function crossfadeVolumes(profile: any, ms: any, out: any): voidcrossfadeVolumes(profile: anyprofile, this.Demo.columnMs(col: any): numbercolumnMs(col: anycol) - const CHART_LEAD_MS: 200CHART_LEAD_MS, this.
Demo.volumes: {
    old: number;
    new: number;
}
volumes
);
return Math.round(const PLOT_BOTTOM: numberPLOT_BOTTOM - this.
Demo.volumes: {
    old: number;
    new: number;
}
volumes
[which: anywhich] * (const PLOT_BOTTOM: numberPLOT_BOTTOM - const PLOT_TOP: numberPLOT_TOP));
} // A vertical line at how far the current crossfade has got, shown only while it is // still fading. Nothing fades before sound is unlocked, so no marker then either. Demo.renderChartMarker(profile: any): voidrenderChartMarker(profile: anyprofile) { if (this.Demo.switchTick: number | null
@type{number | null}
switchTick
=== null || !
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.isAudioUnlocked: boolean
Whether the audio context has been unlocked by a user gesture. Browsers require a user gesture (pointer, key, or touch press) before allowing audio playback. Starts `false`; flips to `true` for the rest of the session after the first gesture successfully resumes the audio context.
@since1.3.0@returns`true` once unlocked; `false` when locked or before initialization.
isAudioUnlocked
) {
return; } const const elapsedMs: numberelapsedMs = ((
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
- this.Demo.switchTick: number
@type{number | null}
switchTick
) /
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
.targetFPS: number
Target fixed-update rate in frames per second. Mirrors {@link HardwareSettings.targetFPS } from `configure()`. `update()` runs at this frequency; rendering may occur at a different cadence.
@since1.0.4@returnsTarget updates per second, or `60` before initialization.
targetFPS
) * 1000;
if (const elapsedMs: numberelapsedMs >= function crossfadeMs(profile: any): numbercrossfadeMs(profile: anyprofile)) { return; } const const col: anycol = const PLOT_LEFT: numberPLOT_LEFT + Math.round(((const elapsedMs: numberelapsedMs + const CHART_LEAD_MS: 200CHART_LEAD_MS) / const CHART_SPAN_MS: numberCHART_SPAN_MS) * (const PLOT_RIGHT: numberPLOT_RIGHT - const PLOT_LEFT: numberPLOT_LEFT)); import uiui.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(const col: anycol, const PLOT_TOP: numberPLOT_TOP, 1, const PLOT_BOTTOM: numberPLOT_BOTTOM - const PLOT_TOP: numberPLOT_TOP + 1), this.Demo.theme: nulltheme.header);
} // The time, in milliseconds from the chart's left edge, that chart column `col` // shows. Demo.columnMs(col: any): numbercolumnMs(col: anycol) { return ((col: anycol - const PLOT_LEFT: numberPLOT_LEFT) / (const PLOT_RIGHT: numberPLOT_RIGHT - const PLOT_LEFT: numberPLOT_LEFT)) * const CHART_SPAN_MS: numberCHART_SPAN_MS; } // Starts the given track ('A', 'B', or 'loop') with its matching crossfade profile, // unless it is already playing - pressing the same button twice does nothing new. Demo.playTrack(trackId: any): voidplayTrack(trackId: anytrackId) { if (trackId: anytrackId === this.Demo.activeTrackId: string | null
@type{string | null}
activeTrackId
) {
return; } if (trackId: anytrackId === 'A') {
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
.musicPlay: (clip: AudioClip, options?: MusicPlayOptions) => void
Plays a loaded audio clip through the music player, crossfading out whatever is currently playing. Silently does nothing when the clip hasn't finished loading yet (or was already unloaded with `clip.unload()`), or before the engine has initialized. While the audio context is still locked (before the first unlock gesture), the request is remembered instead of dropped - it starts automatically the instant the context unlocks, unlike {@link BT.soundPlay } .
@since1.3.0@paramclip - Loaded audio clip to play.@paramoptions - Crossfade, volume, and loop options; see {@link MusicPlayOptions}.
musicPlay
(this.Demo.calmClip: AudioClip | null
@type{AudioClip | null}
calmClip
, { MusicPlayOptions.volume?: number | undefined
Target gain for the incoming track in `[0, 1]` (unclamped). Defaults to `1`.
volume
: 1, MusicPlayOptions.loop?: boolean | undefined
Whether the whole track loops. Ignored when `loopStart`/`loopEnd` are given. Defaults to `true`.
loop
: true, ...
const PROFILE_TO_A: {
    fadeMs: number;
    overlap: number;
    easeIn: string;
    easeOut: string;
}
PROFILE_TO_A
});
} else if (trackId: anytrackId === 'B') {
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
.musicPlay: (clip: AudioClip, options?: MusicPlayOptions) => void
Plays a loaded audio clip through the music player, crossfading out whatever is currently playing. Silently does nothing when the clip hasn't finished loading yet (or was already unloaded with `clip.unload()`), or before the engine has initialized. While the audio context is still locked (before the first unlock gesture), the request is remembered instead of dropped - it starts automatically the instant the context unlocks, unlike {@link BT.soundPlay } .
@since1.3.0@paramclip - Loaded audio clip to play.@paramoptions - Crossfade, volume, and loop options; see {@link MusicPlayOptions}.
musicPlay
(this.Demo.upbeatClip: AudioClip | null
@type{AudioClip | null}
upbeatClip
, { MusicPlayOptions.volume?: number | undefined
Target gain for the incoming track in `[0, 1]` (unclamped). Defaults to `1`.
volume
: 1, MusicPlayOptions.loop?: boolean | undefined
Whether the whole track loops. Ignored when `loopStart`/`loopEnd` are given. Defaults to `true`.
loop
: true, ...
const PROFILE_TO_B: {
    fadeMs: number;
    overlap: number;
    easeIn: string;
    easeOut: string;
}
PROFILE_TO_B
});
} else { // No `loop: true` here: giving loopStart and loopEnd is what makes the region // repeat, and the engine ignores `loop` whenever both of them are set.
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
.musicPlay: (clip: AudioClip, options?: MusicPlayOptions) => void
Plays a loaded audio clip through the music player, crossfading out whatever is currently playing. Silently does nothing when the clip hasn't finished loading yet (or was already unloaded with `clip.unload()`), or before the engine has initialized. While the audio context is still locked (before the first unlock gesture), the request is remembered instead of dropped - it starts automatically the instant the context unlocks, unlike {@link BT.soundPlay } .
@since1.3.0@paramclip - Loaded audio clip to play.@paramoptions - Crossfade, volume, and loop options; see {@link MusicPlayOptions}.
musicPlay
(this.Demo.introLoopClip: AudioClip | null
@type{AudioClip | null}
introLoopClip
, {
MusicPlayOptions.volume?: number | undefined
Target gain for the incoming track in `[0, 1]` (unclamped). Defaults to `1`.
volume
: 1,
MusicPlayOptions.loopStart?: number | undefined
Loop region start in seconds. Requires `loopEnd`; see {@link MusicPlayer.play } .
loopStart
: const INTRO_LOOP_START_SECONDS: 1.5INTRO_LOOP_START_SECONDS,
MusicPlayOptions.loopEnd?: number | undefined
Loop region end in seconds. Requires `loopStart`; see {@link MusicPlayer.play } .
loopEnd
: const INTRO_LOOP_END_SECONDS: 7.9INTRO_LOOP_END_SECONDS,
...
const PROFILE_TO_LOOP: {
    fadeMs: number;
    overlap: number;
    easeIn: string;
    easeOut: string;
}
PROFILE_TO_LOOP
,
}); } // The very first track has nothing to fade from, so its chart gets no marker. this.Demo.switchTick: number | null
@type{number | null}
switchTick
= this.Demo.activeTrackId: string | null
@type{string | null}
activeTrackId
=== null ? null :
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
;
this.Demo.activeTrackId: string | null
@type{string | null}
activeTrackId
= trackId: anytrackId;
} } function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
(class DemoDemo);