/*
Pointer Drag Flick: grab bouncing balls, drag them around, and release to throw.
@description Grab one of three bouncing balls, drag it, and release to throw it, with synthesized sound effects.
@ogScale integer
Part of the BLIT386 demo series.
Prerequisites:
- Pointer Basics: https://demos.blit386.dev/pointer-basics
- Pointer Paint: https://demos.blit386.dev/pointer-paint
Live version: https://demos.blit386.dev/pointer-drag-flick
Guides:
- Input: https://blit386.dev/docs/guides/input#buttons
- Game Loop: https://blit386.dev/docs/guides/game-loop#smoothing-motion-between-updates
- Audio: https://blit386.dev/docs/guides/audio#design-a-sound
Reference:
- Game Loop: https://blit386.dev/docs/api/game-loop#interpolating-render-state-with-renderalpha
- Audio: https://blit386.dev/docs/api/audio#synth
- Overlay: https://blit386.dev/docs/api/overlay#event-tags
---
WHAT YOU WILL SEE
-----------------
Three colored balls (red, green, blue) bouncing around inside a closed box under gravity.
The full-width title strip along the top is the ceiling of that box; the floor and side
walls are the screen edges. The right end of the title strip holds
one small box per pointer slot (M for the mouse, T1 to T3 for fingers), filled green
while that slot holds a ball, so the whole box below is free for the balls. Each active
pointer shows a tiny crosshair, drawn on top of the strip too. Hovering the mouse over a
free ball draws a thin amber ring around it; a grabbed ball gets a white ring.
While sound is still locked, the shared "enable sound" hint sits just under the title
strip; it disappears on the first grab.
WHAT YOU WILL LEARN
-------------------
- BT.isPressed(BT.BTN_POINTER_A, slot) as a "grab" edge: true only on the tick the
button (or finger) goes down, so the drag starts exactly once.
- BT.isReleased(BT.BTN_POINTER_A, slot) as a "throw" edge: true only on the tick the
button comes up. The engine keeps a touch slot's position and delta on release, so the
release tick can still read them.
- BT.pointerDelta(slot) driving the simulation: the release-tick delta becomes the ball's
launch velocity, scaled by THROW_SCALE and capped at MAX_THROW_SPEED.
- Smooth motion between fixed updates: each ball remembers where it was before the last
physics tick, and render() blends that toward the new position by BT.renderAlpha.
- Two sounds built with AudioClip.synth() (the technique Synth Toy explores in depth): a
whoosh whose pitch and volume scale with throw speed, and a thud on hard wall hits.
Both play through BT.soundPlay(clip, { pitch, volume }).
- A tiny physics step per tick: gravity, air drag, floor friction, damped wall bounces.
- BT.assignTag() marks every throw on the overlay's timing chart.
TRY THIS
--------
- Press and hold the left mouse button on a ball to grab it, move it around, then let go
while moving to throw it. A faster flick makes a higher, louder whoosh.
- On a touchscreen, each finger can grab its own ball - up to three at once (slots 1-3).
- Drop a ball from high up and listen: hard landings thud, gentle settling is silent.
- Press the backquote key (`) to open the engine overlay. Each throw drops a "Throw slot
N" tag on the timing chart.
*/
import { class AudioClipDecoded audio asset with its winning source URL and buffer-derived metadata.
Construct instances with
{@link
AudioClip.load
}
,
{@link
AudioClip.loadAll
}
, or
{@link
AudioClip.synth
}
; there is no public constructor.AudioClip, function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>One-liner bootstrap function for BLIT386 demos.
Handles canvas retrieval and engine initialization. Backend selection
(WebGPU or software fallback) is managed internally by BTAPI.
This function provides a streamlined way to start a demo with sensible defaults
while allowing customization through options.bootstrap, const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT, class 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 uiui, import UI_ANCHORSUI_ANCHORS } from './shared/ui.js';
/** @typedef {import('blit386').Palette} Palette */
/** @typedef {{ x: number, y: number, prevX: number, prevY: number, vx: number, vy: number, color: number, grabbedBy: number }} Ball */
const const DISPLAY_W: 320DISPLAY_W = 320;
const const DISPLAY_H: 240DISPLAY_H = 240;
// Vertical strip at the top reserved for the shared UI kit's full-width title bar (a
// title-only 'topBar' is 22 pixels tall). Balls cannot enter it, and grabs are only
// registered below it, so the strip acts as the ceiling of the physics box.
const const HUD_HEIGHT: 22HUD_HEIGHT = 22;
// Scene palette slots. Index 0 is always transparent. UI colors (panel, text, borders)
// are not listed here - the shared UI kit installs them into high slots (240 and up) via
// applyTheme() in init(), so only the demo's own scene colors remain.
const const C_BALL_OUTLINE: 1C_BALL_OUTLINE = 1; // outline drawn around any grabbed ball
// Tint for the ball under the mouse cursor when no slot is grabbing it.
const const C_BALL_HIGHLIGHT: 2C_BALL_HIGHLIGHT = 2;
const const C_CHART_UPDATE: 3C_CHART_UPDATE = 3; // dim gray for the overlay timing chart's update bars
const const C_CHART_RENDER: 4C_CHART_RENDER = 4; // white for the chart's render bars and throw tags
// Three balls, each its own color so it is easy to track which is which.
const const BALL_COLORS: {}BALL_COLORS = [5, 6, 7];
// Physics parameters, all expressed in "display pixels per fixed update tick" since the
// engine runs update() at a fixed rate (here 60 ticks per second).
const const BALL_RADIUS: 10BALL_RADIUS = 10;
const const GRAVITY: 0.35GRAVITY = 0.35; // px per tick squared, downward
const const WALL_DAMPING: 0.78WALL_DAMPING = 0.78; // velocity multiplier on wall bounce (energy loss)
const const FLOOR_FRICTION: 0.985FLOOR_FRICTION = 0.985; // horizontal velocity multiplier per tick on the floor
const const AIR_DRAG: 0.999AIR_DRAG = 0.999; // gentle air drag so flicks decay over time
const const MIN_SPEED: 0.05MIN_SPEED = 0.05; // velocities below this are clamped to zero (avoid tiny jitter)
// Multiplier applied to BT.pointerDelta when a ball is released. The delta is already in
// "display pixels moved since the previous fixed update tick", which is roughly velocity
// in px/tick. Scale up slightly so easy flicks feel energetic.
const const THROW_SCALE: 1.4THROW_SCALE = 1.4;
// Maximum allowed launch speed (px/tick). Caps the velocity from a very fast flick so
// balls do not escape the box in a single tick.
const const MAX_THROW_SPEED: 16MAX_THROW_SPEED = 16;
// Throw whoosh: pitch and volume scale between these min/max values based on how fast the
// ball was thrown, so a gentle nudge sounds different from a hard flick.
const const WHOOSH_PITCH_MIN: 0.85WHOOSH_PITCH_MIN = 0.85;
const const WHOOSH_PITCH_MAX: 1.6WHOOSH_PITCH_MAX = 1.6;
const const WHOOSH_VOLUME_MIN: 0.35WHOOSH_VOLUME_MIN = 0.35;
const const WHOOSH_VOLUME_MAX: 1WHOOSH_VOLUME_MAX = 1.0;
// Wall/floor thud: bounces gentler than this speed are skipped entirely, so a ball
// settling to rest does not spam quiet thuds. THUD_VOLUME_MAX_SPEED is the impact speed
// at or above which the thud plays at full volume.
const const THUD_MIN_SPEED: 0.5THUD_MIN_SPEED = 0.5;
const const THUD_VOLUME_MAX_SPEED: 10THUD_VOLUME_MAX_SPEED = 10;
// Grab indicators in the right end of the title strip: one small box per pointer slot
// (M = mouse, T1-T3 = touch fingers) followed by its label. GRAB_BOX is the box size,
// GRAB_LABEL_GAP the space between a box and its label, and GRAB_ITEM_GAP the space
// before the next box.
const const GRAB_LABELS: {}GRAB_LABELS = ['M', 'T1', 'T2', 'T3'];
const const GRAB_BOX: 8GRAB_BOX = 8;
const const GRAB_LABEL_GAP: 3GRAB_LABEL_GAP = 3;
const const GRAB_ITEM_GAP: 8GRAB_ITEM_GAP = 8;
// Half the length of each cursor crosshair arm, in pixels.
const const CURSOR_ARM: 4CURSOR_ARM = 4;
// Keeps a number 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));
}
// Drag-and-flick physics demo. Each ball is a small object: { x, y, prevX, prevY, vx, vy,
// color, grabbedBy }. `grabbedBy` is -1 when free or a pointer slot index 0..3 when held.
// While held, physics integration is skipped and the ball is moved to the pointer
// position each tick. On release we read BT.pointerDelta(slot) and convert it to the
// ball's launch velocity.
class class DemoDemo {
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = null;
// Palette slot map returned by applyTheme() - where the shared UI colors landed. Used
// for BT.clear(this.theme.bg) and the crosshair cursor color.
/** @type {ReturnType<typeof applyTheme> | null} */
Demo.theme: anytheme = null;
// Whoosh sound played when a ball is thrown.
/** @type {AudioClip | null} */
Demo.whooshClip: AudioClip | nullwhooshClip = null;
// Thud sound played when a ball bounces off a wall or the floor.
/** @type {AudioClip | null} */
Demo.thudClip: AudioClip | nullthudClip = null;
// Active balls. Created in init(). prevX/prevY remember where each ball was at the
// START of the most recent update() tick, before physics moved it. render() blends
// between prevX/prevY and x/y using BT.renderAlpha so the ball glides smoothly across
// render frames instead of only moving once per physics tick - see the Game Loop
// guide linked in the header.
/** @type {Ball[]} */
Demo.balls: {}balls = [];
// Reused for every rectangle, line end, and pixel drawn in render(), so drawing
// allocates no new objects.
Demo.tempRect: Rect2itempRect = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(0, 0, 0, 0);
Demo.lineFrom: Vector2ilineFrom = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(0, 0);
Demo.lineTo: Vector2ilineTo = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(0, 0);
// Tells the engine the screen size and which palette slots to use for the timing
// chart overlay. The chart shows update() and render() time side by side so you can
// see when a ball throw causes a spike.
Demo.configure(): {
displaySize: Vector2i;
isWakeLockEnabled: boolean;
isOverlayEnabled: boolean;
}
configure() {
return {
// Set the logical display size (how many pixels the demo draws at).
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),
// Phones and tablets dim, then lock, the screen after 30-60 seconds without a
// touch - easy to hit while you are just watching a ball settle before
// flicking it again. This asks the browser to keep the screen on while you
// play; unsupported browsers ignore it.
isWakeLockEnabled: booleanisWakeLockEnabled: true,
isOverlayEnabled: booleanisOverlayEnabled: false,
};
}
// Synthesizes the two sounds, sets up the palette, and seeds three balls at varied
// starting positions.
async Demo.init(): unknowninit() {
// The whoosh: a sine wave sliding from 600 Hz down to 150 Hz over 0.35 seconds,
// with a pinch of noise mixed in. Rendering it once here means later throws play
// back with zero delay.
this.Demo.whooshClip: AudioClip | nullwhooshClip = await class AudioClipDecoded audio asset with its winning source URL and buffer-derived metadata.
Construct instances with
{@link
AudioClip.load
}
,
{@link
AudioClip.loadAll
}
, or
{@link
AudioClip.synth
}
; there is no public constructor.AudioClip.AudioClip.synth(params: SynthParams): Promise<AudioClip>Synthesizes a clip from deterministic procedural parameters - no source file, no
`OfflineAudioContext`, and no audio graph involved.
Rendering happens entirely on the CPU via the pure
{@link
renderSynthSamples
}
function
against an `AudioBuffer` allocated from the registered decode context. The returned clip
flows through the same
{@link
buffer
}
getter and playback path as a loaded clip, but uses
a synthetic, non-cached identifier (`synth:<waveform>`) - it is never added to the
URL-keyed resolved cache and never deduplicated, so identical `params` still render a
fresh, independent `AudioBuffer` on every call. See
{@link
SynthParams
}
for the full
parameter set.synth({
SynthParams.waveform: anyOscillator waveform shape.waveform: 'sine',
SynthParams.frequency: numberBase carrier frequency in Hz at the start of the clip (before any pitch sweep or vibrato).frequency: 600,
SynthParams.duration: numberTotal clip duration in seconds. Must be greater than 0 and no more than
{@link
MAX_SYNTH_DURATION_SECONDS
}
.duration: 0.35,
SynthParams.envelope?: SynthEnvelope | undefinedOptional attack/decay/sustain/release envelope. Defaults to a full ADSR envelope; see
{@link
SynthEnvelope
}
.envelope: { SynthEnvelope.attack?: number | undefinedTime in seconds to ramp from silence to full amplitude.
Defaults to
{@link
DEFAULT_ATTACK
}
.attack: 0.005, SynthEnvelope.decay?: number | undefinedTime in seconds to fall from full amplitude to the `sustain` level.
Defaults to
{@link
DEFAULT_DECAY
}
.decay: 0.1, SynthEnvelope.sustain?: number | undefinedGain level in [0, 1] held between the decay and release phases.
Defaults to
{@link
DEFAULT_SUSTAIN
}
.sustain: 0.2, SynthEnvelope.release?: number | undefinedTime in seconds to fall from the sustain level to silence at the end of the clip.
Defaults to
{@link
DEFAULT_RELEASE
}
.release: 0.2 },
SynthParams.pitchSweep?: SynthPitchSweep | undefinedOptional linear pitch sweep from `frequency` to a target frequency across the clip.pitchSweep: { SynthPitchSweep.toFrequency: numberFrequency in Hz the carrier linearly reaches by the end of the clip.toFrequency: 150 },
SynthParams.noiseMix?: number | undefinedFraction of white noise mixed into the oscillator output, in [0, 1] (`0` is pure tone,
`1` is pure noise). Ignored when `waveform` is already `'noise'`.
Defaults to
{@link
DEFAULT_NOISE_MIX
}
.noiseMix: 0.15,
SynthParams.seed: numberSeed for the deterministic PRNG driving noise generation - identical seeds render identical output.seed: 2,
});
// The thud: a very short, low 90 Hz sine with a fast decay and no sustain.
this.Demo.thudClip: AudioClip | nullthudClip = await class AudioClipDecoded audio asset with its winning source URL and buffer-derived metadata.
Construct instances with
{@link
AudioClip.load
}
,
{@link
AudioClip.loadAll
}
, or
{@link
AudioClip.synth
}
; there is no public constructor.AudioClip.AudioClip.synth(params: SynthParams): Promise<AudioClip>Synthesizes a clip from deterministic procedural parameters - no source file, no
`OfflineAudioContext`, and no audio graph involved.
Rendering happens entirely on the CPU via the pure
{@link
renderSynthSamples
}
function
against an `AudioBuffer` allocated from the registered decode context. The returned clip
flows through the same
{@link
buffer
}
getter and playback path as a loaded clip, but uses
a synthetic, non-cached identifier (`synth:<waveform>`) - it is never added to the
URL-keyed resolved cache and never deduplicated, so identical `params` still render a
fresh, independent `AudioBuffer` on every call. See
{@link
SynthParams
}
for the full
parameter set.synth({
SynthParams.waveform: anyOscillator waveform shape.waveform: 'sine',
SynthParams.frequency: numberBase carrier frequency in Hz at the start of the clip (before any pitch sweep or vibrato).frequency: 90,
SynthParams.duration: numberTotal clip duration in seconds. Must be greater than 0 and no more than
{@link
MAX_SYNTH_DURATION_SECONDS
}
.duration: 0.12,
SynthParams.envelope?: SynthEnvelope | undefinedOptional attack/decay/sustain/release envelope. Defaults to a full ADSR envelope; see
{@link
SynthEnvelope
}
.envelope: { SynthEnvelope.attack?: number | undefinedTime in seconds to ramp from silence to full amplitude.
Defaults to
{@link
DEFAULT_ATTACK
}
.attack: 0.001, SynthEnvelope.decay?: number | undefinedTime in seconds to fall from full amplitude to the `sustain` level.
Defaults to
{@link
DEFAULT_DECAY
}
.decay: 0.06, SynthEnvelope.sustain?: number | undefinedGain level in [0, 1] held between the decay and release phases.
Defaults to
{@link
DEFAULT_SUSTAIN
}
.sustain: 0, SynthEnvelope.release?: number | undefinedTime in seconds to fall from the sustain level to silence at the end of the clip.
Defaults to
{@link
DEFAULT_RELEASE
}
.release: 0.05 },
SynthParams.seed: numberSeed for the deterministic PRNG driving noise generation - identical seeds render identical output.seed: 3,
});
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);
// Scene colors: the ball rings, the timing chart bars, and the balls themselves.
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_BALL_OUTLINE: 1C_BALL_OUTLINE, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(255, 255, 255));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_BALL_HIGHLIGHT: 2C_BALL_HIGHLIGHT, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(255, 220, 120));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_CHART_UPDATE: 3C_CHART_UPDATE, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(150, 160, 180));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_CHART_RENDER: 4C_CHART_RENDER, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(255, 255, 255));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const BALL_COLORS: {}BALL_COLORS[0], new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(255, 100, 110));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const BALL_COLORS: {}BALL_COLORS[1], new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(120, 220, 130));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const BALL_COLORS: {}BALL_COLORS[2], new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(120, 170, 255));
// Install the shared UI theme (panel, text, border colors and so on) into high
// palette slots (240 and up), far away from the scene colors above. The returned
// map tells us which slot each theme color landed in, so the demo can reuse them
// (for example this.theme.bg as the screen clear color).
this.Demo.theme: anytheme = import applyThemeapplyTheme(this.Demo.palette: Palettepalette);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteSet: (palette: Palette) => voidStores the active engine palette.
Use this to swap the **entire palette** (e.g. switch between a day and night
theme). After this call the renderer uploads the new palette uniform on the
next frame.
**Palette-value swap (change what a slot looks like):** mutate the live
{@link
BT.palette
}
in place with `palette.set(slot, newColor)`. The renderer
uploads dirty slots on the next frame; no `paletteSet()` or
{@link
BT.spritesRefresh
}
needed.
**Palette-layout swap (same colors, different slot positions):** build a new
palette with the same colors at new indices, call `paletteSet()`, then call
{@link
BT.spritesRefresh
}
so every sprite sheet re-maps its original RGBA
pixels against the new slot layout.paletteSet(this.Demo.palette: Palettepalette);
// Hide the native OS cursor so the drawn crosshair markers are the only cursors
// visible while the pointer is over the canvas.
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.hideCursor: () => voidHides the native OS cursor while the pointer is over the canvas.
Call once from `init()` when the demo draws its own crosshair or
cursor sprite in place of the system arrow. The cursor is restored
automatically when the engine shuts down.
No-op before the engine is initialized.hideCursor();
// Stagger the balls horizontally and give each a small initial velocity so the
// simulation looks alive on the first frame.
this.Demo.balls: {}balls = [
{ x: numberx: 80, y: numbery: 60, prevX: numberprevX: 80, prevY: numberprevY: 60, vx: numbervx: 1.2, vy: numbervy: 0, color: anycolor: const BALL_COLORS: {}BALL_COLORS[0], grabbedBy: numbergrabbedBy: -1 },
{ x: numberx: 160, y: numbery: 50, prevX: numberprevX: 160, prevY: numberprevY: 50, vx: numbervx: -0.6, vy: numbervy: 0.4, color: anycolor: const BALL_COLORS: {}BALL_COLORS[1], grabbedBy: numbergrabbedBy: -1 },
{ x: numberx: 240, y: numbery: 70, prevX: numberprevX: 240, prevY: numberprevY: 70, vx: numbervx: 0.8, vy: numbervy: -0.2, color: anycolor: const BALL_COLORS: {}BALL_COLORS[2], grabbedBy: numbergrabbedBy: -1 },
];
return true;
}
// Per-tick: route press and release edges to grab and throw, follow held balls to
// their owning pointer, integrate physics for free balls.
Demo.update(): voidupdate() {
// Walk every pointer slot. The mouse (slot 0) and touches (1-3) all use
// BTN_POINTER_A as the "primary" button; for touches that is automatic ("contact
// made = A held"), for the mouse it is the left button.
for (let let slot: numberslot = 0; let slot: numberslot < 4; let slot: numberslot++) {
// Edge: pointer just went down on this slot. Try to grab a ball.
if (const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.isPressed: (button: number, player?: number, repeatRate?: number) => booleanChecks whether a button was pressed on the current frame.
Same parameter semantics as
{@link
isDown
}
; returns `true` only on
the frame the button transitions from up to down.
Call from `update()`, not `render()`, for reliable detection: for keyboard-mapped
face buttons (players 0 and 1), the press edge clears once per fixed-update tick,
which always runs before that frame's `render()`.
`BTN_L2`, `BTN_R2` and `BTN_TRIGGER` are gamepad only (no keyboard mapping).isPressed(const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.type BTN_POINTER_A: numberPrimary pointer button code.
Maps to mouse left for slot 0; touch contact for slots 1-3.BTN_POINTER_A, let slot: numberslot)) {
this.Demo.tryGrab(slot: any): voidtryGrab(let slot: numberslot);
}
// Edge: pointer just released on this slot. Throw whatever it held.
if (const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.isReleased: (button: number, player?: number) => booleanChecks whether a button was released on the current frame.
Same parameter semantics as
{@link
isDown
}
; returns `true` only on
the frame the button transitions from down to up.
Call from `update()`, not `render()`, for reliable detection: for keyboard-mapped
face buttons (players 0 and 1), the release edge clears once per fixed-update tick,
which always runs before that frame's `render()`.
`BTN_L2`, `BTN_R2` and `BTN_TRIGGER` are gamepad only (no keyboard mapping).isReleased(const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.type BTN_POINTER_A: numberPrimary pointer button code.
Maps to mouse left for slot 0; touch contact for slots 1-3.BTN_POINTER_A, let slot: numberslot)) {
this.Demo.tryThrow(slot: any): voidtryThrow(let slot: numberslot);
}
}
// Move every ball: held ones follow their pointer, free ones obey gravity, drag,
// and wall bounces.
for (const const ball: anyball of this.Demo.balls: {}balls) {
// Snapshot "where was this ball a moment ago" BEFORE moving it, so render()
// can draw a smooth in-between position instead of a pop.
const ball: anyball.prevX = const ball: anyball.x;
const ball: anyball.prevY = const ball: anyball.y;
if (const ball: anyball.grabbedBy >= 0) {
this.Demo.updateHeldBall(ball: any): voidupdateHeldBall(const ball: anyball);
} else {
this.Demo.updateFreeBall(ball: any): voidupdateFreeBall(const ball: anyball);
}
}
}
// Per-frame render: clear, draw the HUD, draw the balls, draw the cursor markers.
Demo.render(): voidrender() {
// Clear the whole screen with the shared theme's background color.
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: anytheme.bg);
this.Demo.renderHUD(): voidrenderHUD();
this.Demo.renderBalls(): voidrenderBalls();
this.Demo.renderCursors(): voidrenderCursors();
}
// Attempts to grab a ball whose disc is under this slot's pointer. Skips if there is
// no live pointer, the pointer is inside the HUD strip, or this slot already holds a
// ball.
Demo.tryGrab(slot: any): voidtryGrab(slot: anyslot) {
if (!const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.isPointerActive: (pointerIndex?: number) => booleanReports whether the given pointer slot has a live pointer.
For slot 0 (mouse) this is true while the mouse is hovering inside the
canvas; cleared on `pointerleave`. For slots 1-3 (touch / pen) this is
true while the contact is down.isPointerActive(slot: anyslot)) {
return;
}
const const pos: Vector2ipos = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.pointerPos: (pointerIndex?: number) => Vector2iReturns the position of the pointer in the given slot, in display coordinates.
Slot 0 is the mouse; slots 1 through 3 are touch / pen contacts assigned
in arrival order. Returns `Vector2i.zero()` when the engine has not been
initialized, the slot index is out of `[0, 3]`, or the slot has no live
pointer.pointerPos(slot: anyslot);
// Do not try to grab through the HUD - a press at HUD level is a miss-click
// rather than a deliberate grab.
if (const pos: Vector2ipos.Vector2i.y: numberVertical component (defaults to 0).y < const HUD_HEIGHT: 22HUD_HEIGHT) {
return;
}
// If this slot is already holding something, leave it alone.
for (const const ball: anyball of this.Demo.balls: {}balls) {
if (const ball: anyball.grabbedBy === slot: anyslot) {
return;
}
}
// Find the topmost ball whose disc covers the pointer. Iterating in reverse so
// the visually topmost ball wins when balls overlap.
for (let let i: numberi = this.Demo.balls: {}balls.length - 1; let i: numberi >= 0; let i: numberi--) {
const const ball: anyball = this.Demo.balls: {}balls[let i: numberi];
if (const ball: anyball.grabbedBy !== -1) {
continue;
}
// Inside the disc when the squared distance to the center is at most the
// squared radius - comparing squares avoids a square root.
const const dx: numberdx = const ball: anyball.x - const pos: Vector2ipos.Vector2i.x: numberHorizontal component (defaults to 0).x;
const const dy: numberdy = const ball: anyball.y - const pos: Vector2ipos.Vector2i.y: numberVertical component (defaults to 0).y;
if (const dx: numberdx * const dx: numberdx + const dy: numberdy * const dy: numberdy <= const BALL_RADIUS: 10BALL_RADIUS * const BALL_RADIUS: 10BALL_RADIUS) {
const ball: anyball.grabbedBy = slot: anyslot;
const ball: anyball.vx = 0;
const ball: anyball.vy = 0;
return;
}
}
}
// Releases whatever ball this slot is holding, launching it with the pointer's
// release-tick velocity (scaled and clamped to MAX_THROW_SPEED).
Demo.tryThrow(slot: any): voidtryThrow(slot: anyslot) {
// Mark this throw on the overlay timing chart so you can see exactly when a throw
// happened and which pointer slot caused it. The template string inserts the slot
// number so repeated throws from different fingers are distinct.
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.assignTag: (label?: string) => voidPlaces a labeled marker on the overlay timing chart at the current tick.
Requires `isOverlayTimingChartEnabled: true` in `configure()`. Tags scroll with the chart
history and are pruned when they leave the visible window. Empty labels become
`"Untitled"`. Chart width resets add an automatic `"Start"` tag.assignTag(`Throw slot ${slot: anyslot}`);
for (const const ball: anyball of this.Demo.balls: {}balls) {
if (const ball: anyball.grabbedBy !== slot: anyslot) {
continue;
}
// BT.pointerDelta is the movement since the previous tick, which is
// approximately velocity in px/tick. We can use it directly as launch
// velocity (with a small scale factor).
const const delta: Vector2idelta = 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.pointerDelta: (pointerIndex?: number) => Vector2iReturns the position delta `(pos - prevPos)` for a pointer slot since the previous frame.
Reflects movement accumulated between the previous and current frame.
Snapshotted and reset by the engine at `endFrame()`, which runs after
`update()` and `render()`. Returns `Vector2i.zero()` when the engine is
not initialized or `pointerIndex` is out of range.pointerDelta(slot: anyslot);
let let vx: numbervx = const delta: Vector2idelta.Vector2i.x: numberHorizontal component (defaults to 0).x * const THROW_SCALE: 1.4THROW_SCALE;
let let vy: numbervy = const delta: Vector2idelta.Vector2i.y: numberVertical component (defaults to 0).y * const THROW_SCALE: 1.4THROW_SCALE;
// Clamp the launch speed so a frantic flick cannot escape the box.
const const speed: anyspeed = Math.hypot(let vx: numbervx, let vy: numbervy);
if (const speed: anyspeed > const MAX_THROW_SPEED: 16MAX_THROW_SPEED) {
const const k: numberk = const MAX_THROW_SPEED: 16MAX_THROW_SPEED / const speed: anyspeed;
let vx: numbervx *= const k: numberk;
let vy: numbervy *= const k: numberk;
}
this.Demo.playWhoosh(speed: any): voidplayWhoosh(const speed: anyspeed);
const ball: anyball.vx = let vx: numbervx;
const ball: anyball.vy = let vy: numbervy;
const ball: anyball.grabbedBy = -1;
return;
}
}
// Plays the flick whoosh, using throw speed to control pitch (faster flick = higher,
// more urgent pitch) and volume (faster flick = louder). BT.soundPlay's `pitch`
// option is a playback-rate multiplier, the same trick Audio Basics uses for its blip
// sound. `speed` is the pre-clamp launch speed in px/tick.
Demo.playWhoosh(speed: any): voidplayWhoosh(speed: anyspeed) {
const const speedFraction: anyspeedFraction = function clamp(value: any, min: any, max: any): anyclamp(speed: anyspeed / const MAX_THROW_SPEED: 16MAX_THROW_SPEED, 0, 1);
const const pitch: numberpitch = const WHOOSH_PITCH_MIN: 0.85WHOOSH_PITCH_MIN + const speedFraction: anyspeedFraction * (const WHOOSH_PITCH_MAX: 1.6WHOOSH_PITCH_MAX - const WHOOSH_PITCH_MIN: 0.85WHOOSH_PITCH_MIN);
const const volume: numbervolume = const WHOOSH_VOLUME_MIN: 0.35WHOOSH_VOLUME_MIN + const speedFraction: anyspeedFraction * (const WHOOSH_VOLUME_MAX: 1WHOOSH_VOLUME_MAX - const WHOOSH_VOLUME_MIN: 0.35WHOOSH_VOLUME_MIN);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.soundPlay: (clip: AudioClip, options?: SoundPlayOptions) => SoundRefPlays a loaded audio clip through the SFX voice pool.
Returns an inert
{@link
SoundRef
}
without allocating a voice when the clip hasn't finished
loading yet (or was already unloaded), when the pool has no free or stealable voice at this
priority, or before the engine has unlocked audio playback.soundPlay(this.Demo.whooshClip: AudioClip | nullwhooshClip, { VoicePlayOptions.pitch?: number | undefinedInitial `playbackRate`. Defaults to `1`.pitch, VoicePlayOptions.volume?: number | undefinedInitial gain in `[0, 1]` (unclamped). Defaults to `1`.volume });
}
// Snaps a held ball to its owning pointer's position. If the pointer went invalid
// mid-grab (the mouse left the canvas, a touch was canceled) we release the ball
// gently with zero velocity.
Demo.updateHeldBall(ball: any): voidupdateHeldBall(ball: anyball) {
const const slot: anyslot = ball: anyball.grabbedBy;
if (!const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.isPointerActive: (pointerIndex?: number) => booleanReports whether the given pointer slot has a live pointer.
For slot 0 (mouse) this is true while the mouse is hovering inside the
canvas; cleared on `pointerleave`. For slots 1-3 (touch / pen) this is
true while the contact is down.isPointerActive(const slot: anyslot)) {
// Pointer disappeared - drop the ball where it is.
ball: anyball.grabbedBy = -1;
ball: anyball.vx = 0;
ball: anyball.vy = 0;
return;
}
const const pos: Vector2ipos = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.pointerPos: (pointerIndex?: number) => Vector2iReturns the position of the pointer in the given slot, in display coordinates.
Slot 0 is the mouse; slots 1 through 3 are touch / pen contacts assigned
in arrival order. Returns `Vector2i.zero()` when the engine has not been
initialized, the slot index is out of `[0, 3]`, or the slot has no live
pointer.pointerPos(const slot: anyslot);
ball: anyball.x = const pos: Vector2ipos.Vector2i.x: numberHorizontal component (defaults to 0).x;
ball: anyball.y = const pos: Vector2ipos.Vector2i.y: numberVertical component (defaults to 0).y;
}
// Integrates one tick of physics for a free ball: gravity, air drag, floor friction,
// and wall bounces with damping.
Demo.updateFreeBall(ball: any): voidupdateFreeBall(ball: anyball) {
// Gravity pulls the ball down each tick.
ball: anyball.vy += const GRAVITY: 0.35GRAVITY;
// Gentle air drag on both axes.
ball: anyball.vx *= const AIR_DRAG: 0.999AIR_DRAG;
ball: anyball.vy *= const AIR_DRAG: 0.999AIR_DRAG;
// Integrate position: add this tick's velocity to where the ball is.
ball: anyball.x += ball: anyball.vx;
ball: anyball.y += ball: anyball.vy;
// Bounce off the side walls: push the ball back inside, play a thud, and flip the
// horizontal velocity (scaled down so some energy is lost).
if (ball: anyball.x - const BALL_RADIUS: 10BALL_RADIUS < 0) {
ball: anyball.x = const BALL_RADIUS: 10BALL_RADIUS;
this.Demo.playThud(impactSpeed: any): voidplayThud(Math.abs(ball: anyball.vx));
ball: anyball.vx = -ball: anyball.vx * const WALL_DAMPING: 0.78WALL_DAMPING;
} else if (ball: anyball.x + const BALL_RADIUS: 10BALL_RADIUS > const DISPLAY_W: 320DISPLAY_W) {
ball: anyball.x = const DISPLAY_W: 320DISPLAY_W - const BALL_RADIUS: 10BALL_RADIUS;
this.Demo.playThud(impactSpeed: any): voidplayThud(Math.abs(ball: anyball.vx));
ball: anyball.vx = -ball: anyball.vx * const WALL_DAMPING: 0.78WALL_DAMPING;
}
// Same for the ceiling (the HUD strip) and the floor (the bottom edge).
if (ball: anyball.y - const BALL_RADIUS: 10BALL_RADIUS < const HUD_HEIGHT: 22HUD_HEIGHT) {
ball: anyball.y = const HUD_HEIGHT: 22HUD_HEIGHT + const BALL_RADIUS: 10BALL_RADIUS;
this.Demo.playThud(impactSpeed: any): voidplayThud(Math.abs(ball: anyball.vy));
ball: anyball.vy = -ball: anyball.vy * const WALL_DAMPING: 0.78WALL_DAMPING;
} else if (ball: anyball.y + const BALL_RADIUS: 10BALL_RADIUS > const DISPLAY_H: 240DISPLAY_H) {
ball: anyball.y = const DISPLAY_H: 240DISPLAY_H - const BALL_RADIUS: 10BALL_RADIUS;
this.Demo.playThud(impactSpeed: any): voidplayThud(Math.abs(ball: anyball.vy));
ball: anyball.vy = -ball: anyball.vy * const WALL_DAMPING: 0.78WALL_DAMPING;
// Touching the floor: scrub a little horizontal speed so balls come to rest
// after a few rolls.
ball: anyball.vx *= const FLOOR_FRICTION: 0.985FLOOR_FRICTION;
}
// Snap negligible velocities to zero so balls truly stop instead of creeping
// forever.
if (Math.abs(ball: anyball.vx) < const MIN_SPEED: 0.05MIN_SPEED) {
ball: anyball.vx = 0;
}
if (Math.abs(ball: anyball.vy) < const MIN_SPEED: 0.05MIN_SPEED) {
ball: anyball.vy = 0;
}
}
// Plays the wall/floor bounce thud, skipping bounces too gentle to notice and scaling
// volume with impact speed. `impactSpeed` is the absolute velocity component
// (px/tick) at the moment of impact.
Demo.playThud(impactSpeed: any): voidplayThud(impactSpeed: anyimpactSpeed) {
if (impactSpeed: anyimpactSpeed < const THUD_MIN_SPEED: 0.5THUD_MIN_SPEED) {
return;
}
const const volume: anyvolume = function clamp(value: any, min: any, max: any): anyclamp(impactSpeed: anyimpactSpeed / const THUD_VOLUME_MAX_SPEED: 10THUD_VOLUME_MAX_SPEED, 0.2, 1);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.soundPlay: (clip: AudioClip, options?: SoundPlayOptions) => SoundRefPlays a loaded audio clip through the SFX voice pool.
Returns an inert
{@link
SoundRef
}
without allocating a voice when the clip hasn't finished
loading yet (or was already unloaded), when the pool has no free or stealable voice at this
priority, or before the engine has unlocked audio playback.soundPlay(this.Demo.thudClip: AudioClip | nullthudClip, { VoicePlayOptions.volume?: number | undefinedInitial gain in `[0, 1]` (unclamped). Defaults to `1`.volume });
}
// The HUD, built from shared UI kit groups: the full-width title strip at the top (it
// doubles as the ceiling of the physics box - see HUD_HEIGHT) with the grab
// indicators in its right end, plus the "enable sound" hint just under it.
Demo.renderHUD(): voidrenderHUD() {
// The classic full-width 22 px title strip along the top edge. Nothing else goes
// into this group, so the strip stays exactly HUD_HEIGHT tall and the ceiling
// the balls bounce off matches what you see.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_BAR);
import uiui.panel('Drag a ball, release to flick');
import uiui.end();
this.Demo.renderGrabIndicators(): voidrenderGrabIndicators();
// Browsers refuse to play any sound until the page is clicked or a key is
// pressed. This shared row shows the standard warm "enable sound" prompt and
// disappears on its own the moment audio unlocks - which here happens on the very
// first grab. Balls fly behind it: kit text is drawn as sprites, and the balls
// are plain shapes, which the engine draws first.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { y: numbery: const HUD_HEIGHT: 22HUD_HEIGHT + 2 });
import uiui.audioUnlockHint();
import uiui.end();
}
// One small box per pointer slot in the right end of the title strip, filled green
// while that slot is grabbing a ball, each followed by its label (M = mouse, T1-T3 =
// touch fingers). The boxes and labels are drawn with the kit's sprite helpers so
// they land on top of the strip.
Demo.renderGrabIndicators(): voidrenderGrabIndicators() {
// Work out the row's total width first, so it can end 6 pixels from the right
// edge - the same margin the strip's title keeps on the left. Every character of
// the system font is FONT_W (6) pixels wide.
let let rowW: numberrowW = (const GRAB_LABELS: {}GRAB_LABELS.length - 1) * const GRAB_ITEM_GAP: 8GRAB_ITEM_GAP;
for (const const label: anylabel of const GRAB_LABELS: {}GRAB_LABELS) {
let rowW: numberrowW += const GRAB_BOX: 8GRAB_BOX + const GRAB_LABEL_GAP: 3GRAB_LABEL_GAP + const label: anylabel.length * import FONT_WFONT_W;
}
// Line the labels up with the strip's title: its capital letters cover rows 6 to
// 12 (the kit prints the title 3 pixels down, and capitals start 3 rows into the
// font cell). A caption's capitals start 1 row above the y it is given, so labels
// at boxY + 1 land on those same rows, and each 8-pixel box covers rows 6 to 13.
const const boxY: 6boxY = 6;
let let x: numberx = const DISPLAY_W: 320DISPLAY_W - 6 - let rowW: numberrowW;
for (let let slot: numberslot = 0; let slot: numberslot < const GRAB_LABELS: {}GRAB_LABELS.length; let slot: numberslot++) {
// .some() asks: is there at least one ball whose grabbedBy equals this slot?
const const grabbing: anygrabbing = this.Demo.balls: {}balls.some((b: anyb) => b: anyb.grabbedBy === let slot: numberslot);
this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(let x: numberx, const boxY: 6boxY, const GRAB_BOX: 8GRAB_BOX, const GRAB_BOX: 8GRAB_BOX);
if (const grabbing: anygrabbing) {
import uiui.drawRectFill(this.Demo.tempRect: Rect2itempRect, this.Demo.theme: anytheme.accent);
}
import uiui.drawRect(this.Demo.tempRect: Rect2itempRect, this.Demo.theme: anytheme.border);
let x: numberx += const GRAB_BOX: 8GRAB_BOX + const GRAB_LABEL_GAP: 3GRAB_LABEL_GAP;
import uiui.caption(let x: numberx, const boxY: 6boxY + 1, const GRAB_LABELS: {}GRAB_LABELS[let slot: numberslot], { color: stringcolor: const grabbing: anygrabbing ? 'text' : 'dim' });
let x: numberx += const GRAB_LABELS: {}GRAB_LABELS[let slot: numberslot].length * import FONT_WFONT_W + const GRAB_ITEM_GAP: 8GRAB_ITEM_GAP;
}
}
// Draws each ball as a filled disc. Highlights the ball under the mouse (for hover
// feedback) and outlines any ball currently grabbed.
Demo.renderBalls(): voidrenderBalls() {
// Determine which ball, if any, the mouse is currently hovering over. We only do
// this for the mouse (slot 0) since touch slots only have a position while in
// contact (which means they are already grabbing).
const const mousePos: Vector2i | nullmousePos = 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.isPointerActive: (pointerIndex?: number) => booleanReports whether the given pointer slot has a live pointer.
For slot 0 (mouse) this is true while the mouse is hovering inside the
canvas; cleared on `pointerleave`. For slots 1-3 (touch / pen) this is
true while the contact is down.isPointerActive(0) ? const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.pointerPos: (pointerIndex?: number) => Vector2iReturns the position of the pointer in the given slot, in display coordinates.
Slot 0 is the mouse; slots 1 through 3 are touch / pen contacts assigned
in arrival order. Returns `Vector2i.zero()` when the engine has not been
initialized, the slot index is out of `[0, 3]`, or the slot has no live
pointer.pointerPos(0) : null;
let let hoverIndex: numberhoverIndex = -1;
if (const mousePos: Vector2i | nullmousePos !== null && const mousePos: Vector2imousePos.Vector2i.y: numberVertical component (defaults to 0).y >= const HUD_HEIGHT: 22HUD_HEIGHT) {
for (let let i: numberi = this.Demo.balls: {}balls.length - 1; let i: numberi >= 0; let i: numberi--) {
const const ball: anyball = this.Demo.balls: {}balls[let i: numberi];
const const dx: numberdx = const ball: anyball.x - const mousePos: Vector2imousePos.Vector2i.x: numberHorizontal component (defaults to 0).x;
const const dy: numberdy = const ball: anyball.y - const mousePos: Vector2imousePos.Vector2i.y: numberVertical component (defaults to 0).y;
if (const dx: numberdx * const dx: numberdx + const dy: numberdy * const dy: numberdy <= const BALL_RADIUS: 10BALL_RADIUS * const BALL_RADIUS: 10BALL_RADIUS) {
let hoverIndex: numberhoverIndex = let i: numberi;
break;
}
}
}
for (let let i: numberi = 0; let i: numberi < this.Demo.balls: {}balls.length; let i: numberi++) {
const const ball: anyball = this.Demo.balls: {}balls[let i: numberi];
// BT.renderAlpha is a fraction from 0 (a physics tick just finished) to just
// under 1 (the next tick is about to happen). Blending prevX/prevY toward x/y
// by that fraction gives us the ball's position AT THIS EXACT RENDER MOMENT,
// not just its position as of the last physics tick. Picture a movie: physics
// ticks are the individual film frames, and render() is the projector running
// faster than the film advances - renderAlpha tells the projector how far to
// "tween" between the current frame and the next one so playback looks smooth
// instead of jerky.
const const drawX: anydrawX = Math.round(const ball: anyball.prevX + (const ball: anyball.x - const ball: anyball.prevX) * 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.renderAlpha: numberFractional progress between the last completed fixed update and the next.
Intended for interpolating render state between fixed-update steps.renderAlpha);
const const drawY: anydrawY = Math.round(const ball: anyball.prevY + (const ball: anyball.y - const ball: anyball.prevY) * 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.renderAlpha: numberFractional progress between the last completed fixed update and the next.
Intended for interpolating render state between fixed-update steps.renderAlpha);
// A ring is a disc 1 pixel bigger, drawn first in the ring color; the ball
// drawn on top of it leaves a 1-pixel outline showing around the edge. White
// marks a grabbed ball, amber the topmost free ball under the mouse cursor.
if (const ball: anyball.grabbedBy !== -1) {
this.Demo.drawDisc(cx: any, cy: any, r: any, color: any): voiddrawDisc(const drawX: anydrawX, const drawY: anydrawY, const BALL_RADIUS: 10BALL_RADIUS + 1, const C_BALL_OUTLINE: 1C_BALL_OUTLINE);
} else if (let i: numberi === let hoverIndex: numberhoverIndex) {
this.Demo.drawDisc(cx: any, cy: any, r: any, color: any): voiddrawDisc(const drawX: anydrawX, const drawY: anydrawY, const BALL_RADIUS: 10BALL_RADIUS + 1, const C_BALL_HIGHLIGHT: 2C_BALL_HIGHLIGHT);
}
this.Demo.drawDisc(cx: any, cy: any, r: any, color: any): voiddrawDisc(const drawX: anydrawX, const drawY: anydrawY, const BALL_RADIUS: 10BALL_RADIUS, const ball: anyball.color);
}
}
// Small crosshair at every active pointer position so users can see where each finger
// or the mouse currently is.
Demo.renderCursors(): voidrenderCursors() {
for (let let slot: numberslot = 0; let slot: numberslot < 4; let slot: numberslot++) {
if (!const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.isPointerActive: (pointerIndex?: number) => booleanReports whether the given pointer slot has a live pointer.
For slot 0 (mouse) this is true while the mouse is hovering inside the
canvas; cleared on `pointerleave`. For slots 1-3 (touch / pen) this is
true while the contact is down.isPointerActive(let slot: numberslot)) {
continue;
}
const const pos: Vector2ipos = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.pointerPos: (pointerIndex?: number) => Vector2iReturns the position of the pointer in the given slot, in display coordinates.
Slot 0 is the mouse; slots 1 through 3 are touch / pen contacts assigned
in arrival order. Returns `Vector2i.zero()` when the engine has not been
initialized, the slot index is out of `[0, 3]`, or the slot has no live
pointer.pointerPos(let slot: numberslot);
const const span: numberspan = const CURSOR_ARM: 4CURSOR_ARM * 2 + 1;
// Two 1-pixel-thick arms, drawn with ui.drawRectFill(), the sprite version of
// BT.drawRectFill(): the engine draws every BT.drawLine() and
// BT.drawRectFill() before any sprite, and the title strip is a sprite, so a
// plain line would vanish whenever the pointer moved over it.
import uiui.drawRectFill(this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(const pos: Vector2ipos.Vector2i.x: numberHorizontal component (defaults to 0).x - const CURSOR_ARM: 4CURSOR_ARM, const pos: Vector2ipos.Vector2i.y: numberVertical component (defaults to 0).y, const span: numberspan, 1), this.Demo.theme: anytheme.text);
import uiui.drawRectFill(this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(const pos: Vector2ipos.Vector2i.x: numberHorizontal component (defaults to 0).x, const pos: Vector2ipos.Vector2i.y: numberVertical component (defaults to 0).y - const CURSOR_ARM: 4CURSOR_ARM, 1, const span: numberspan), this.Demo.theme: anytheme.text);
}
}
// Filled disc using a row-by-row scan: for each row in the bounding box, draw a
// horizontal line of the pixels covered by the circle equation. Cheaper than
// per-pixel testing and produces clean edges at this scale. The equation uses r + 0.5
// instead of r: with exactly r, the top, bottom, left, and right rows each come out
// as a lone 1-pixel nub, and the extra half pixel rounds those into flat edges.
Demo.drawDisc(cx: any, cy: any, r: any, color: any): voiddrawDisc(cx: anycx, cy: anycy, r: anyr, color: anycolor) {
const const r2: numberr2 = (r: anyr + 0.5) * (r: anyr + 0.5);
for (let let dy: numberdy = -r: anyr; let dy: numberdy <= r: anyr; let dy: numberdy++) {
// Half-width of the row at this y, from x * x + y * y <= r * r solved for x.
const const dx: anydx = Math.floor(Math.sqrt(const r2: numberr2 - let dy: numberdy * let dy: numberdy));
this.Demo.lineFrom: Vector2ilineFrom.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(cx: anycx - const dx: anydx, cy: anycy + let dy: numberdy);
this.Demo.lineTo: Vector2ilineTo.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(cx: anycx + const dx: anydx, cy: anycy + let dy: numberdy);
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.drawLine: (p0: Vector2i, p1: Vector2i, paletteIndex: number) => voidDraws a pixel-perfect line between two points.
Uses rasterized line drawing without antialiasing.drawLine(this.Demo.lineFrom: Vector2ilineFrom, this.Demo.lineTo: Vector2ilineTo, color: anycolor);
}
}
}
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);