/*
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): numberApplies an easing curve to a normalized time value.applyEasing, 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 Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32, 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 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): 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 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: numberTop edge Y coordinate (defaults to 0).y + 14;
const const PLOT_BOTTOM: numberPLOT_BOTTOM = const CHART_RECT: Rect2iCHART_RECT.Rect2i.y: numberTop 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: numberLeft 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: numberLeft edge X coordinate (defaults to 0).x + const CHART_RECT: Rect2iCHART_RECT.Rect2i.width: numberWidth 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): numberApplies an easing curve to a normalized time value.applyEasing(const outT: anyoutT, profile: anyprofile.easeOut);
out: anyout.new = function applyEasing(t: number, easing: EasingFunction): numberApplies an easing curve to a normalized time value.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 | nullpalette = null;
// Palette slots of the shared UI theme colors, filled by applyTheme() in init().
Demo.theme: nulltheme = null;
/** @type {AudioClip | null} */
Demo.calmClip: AudioClip | nullcalmClip = null;
/** @type {AudioClip | null} */
Demo.upbeatClip: AudioClip | nullupbeatClip = null;
/** @type {AudioClip | null} */
Demo.introLoopClip: AudioClip | nullintroLoopClip = null;
// Which button's track is playing: 'A', 'B', 'loop', or null before the first play.
/** @type {string | null} */
Demo.activeTrackId: string | nullactiveTrackId = 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 | nullswitchTick = 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): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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 | nullcalmClip, this.Demo.upbeatClip: AudioClip | nullupbeatClip, this.Demo.introLoopClip: AudioClip | nullintroLoopClip] = await Promise.all([
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/music-calm.wav'),
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/music-upbeat.wav'),
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/music-intro-loop.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 (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: Palettepalette);
// 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: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_LIT: 1C_LIT, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(75, 210, 120));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_LIT_RIM: 2C_LIT_RIM, import litRimColorlitRimColor(this.Demo.palette: Palettepalette, this.Demo.palette: Palettepalette.Palette.get(index: number): Color32Returns 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.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) => 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);
// 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) => 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();
// 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 | nullactiveTrackId);
}
// 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" | "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 | nullactiveTrackId ? 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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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: numberTop 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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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 | nullswitchTick === 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: 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) {
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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks - this.Demo.switchTick: numberswitchTick) / 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: numberTarget 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.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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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 | nullactiveTrackId) {
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) => voidPlays 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
}
.musicPlay(this.Demo.calmClip: AudioClip | nullcalmClip, { MusicPlayOptions.volume?: number | undefinedTarget gain for the incoming track in `[0, 1]` (unclamped). Defaults to `1`.volume: 1, MusicPlayOptions.loop?: boolean | undefinedWhether 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) => voidPlays 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
}
.musicPlay(this.Demo.upbeatClip: AudioClip | nullupbeatClip, { MusicPlayOptions.volume?: number | undefinedTarget gain for the incoming track in `[0, 1]` (unclamped). Defaults to `1`.volume: 1, MusicPlayOptions.loop?: boolean | undefinedWhether 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) => voidPlays 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
}
.musicPlay(this.Demo.introLoopClip: AudioClip | nullintroLoopClip, {
MusicPlayOptions.volume?: number | undefinedTarget gain for the incoming track in `[0, 1]` (unclamped). Defaults to `1`.volume: 1,
MusicPlayOptions.loopStart?: number | undefinedLoop region start in seconds. Requires `loopEnd`; see
{@link
MusicPlayer.play
}
.loopStart: const INTRO_LOOP_START_SECONDS: 1.5INTRO_LOOP_START_SECONDS,
MusicPlayOptions.loopEnd?: number | undefinedLoop 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 | nullswitchTick = this.Demo.activeTrackId: string | nullactiveTrackId === 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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks;
this.Demo.activeTrackId: string | nullactiveTrackId = 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.bootstrap(class DemoDemo);