/*
Keyboard Diagnostic: a visual keyboard with press, hold, and release feedback.
@description A full on-screen keyboard with press, hold, and release feedback, to verify fast taps on any display.
Part of the BLIT386 demo series.
Prerequisites:
- Keyboard Input: https://demos.blit386.dev/keyboard-input
Live version: https://demos.blit386.dev/keyboard-diagnostic
Guides:
- Input: https://blit386.dev/docs/guides/input#keyboard
- Input: https://blit386.dev/docs/guides/input#frame-timing-semantics
---
WHAT YOU WILL SEE
-----------------
A 600x315 picture of a full keyboard: six rows of key caps from the function row down to
the modifier row, with an arrow cluster on the right. Every cap is the same rounded block
the System Font Glyphs demo uses for its cells (a UI kit button face), with a centered
label; the arrow keys are labeled with the system font's arrow characters. The whole cap
turns green while its key is held, flashes yellow for 12 ticks on the press edge, and
flashes red for 12 ticks on the release edge, with a lighter rim in the same color and
its label in the dark background color. A legend under the keyboard names the three
colors next to small solid squares.
A title strip runs across the top with a one-line hint under it (a warm "This demo needs
a keyboard" notice on touch devices). Two equal panels sit under the legend, lined up
with the keyboard's edges: "Last edge" shows the most recent edge, its key code, and the
engine tick it fired on; "Edge counts" shows the running press and release counts and a
"Counts match" pip that stays lit while the two are equal.
WHAT YOU WILL LEARN
-------------------
- BT.isKeyDown(code) is held state and can be read from render(); BT.isKeyPressed(code)
and BT.isKeyReleased(code) are one-tick edges that clear at the end of every fixed
update, so they are read in update() and remembered in per-key flash timers.
- Keyboard edges are buffered on the DOM events and consumed by the next fixed update, so
a tap shorter than one tick still produces one press and one release.
- KeyboardEvent.code strings (KeyQ, ShiftLeft, ArrowUp) name physical keys independent of
the OS layout.
- A keyboard layout described as data: a row table measured in key widths ("units"),
which addKeyRow() walks to place caps, with per-key overrides for width, extra gaps,
and fixed columns.
- Hardware settings: a 600x315 display drawn into a 1200x630 drawing buffer with
outputUpscaleFilter 'nearest' and targetFPS 60, plus isCapturingKeyboardScroll.
- The shared UI kit's ui.drawButtonFace() and ui.drawButtonFill() for the caps, and
BT.systemPrint with BT.systemPrintMeasure for centering a label on a cap.
TRY THIS
--------
- Tap a key as fast as you can, several times in a row. Every tap must show a yellow
flash followed by a red one, and the "Presses" and "Releases" counts must stay equal -
the "Counts match" pip goes dark the moment they differ. A mismatch means an edge was
dropped.
- Run the page on a high-refresh display (120 Hz or more). targetFPS is 60, so some drawn
frames run no update at all; the edge buffering keeps fast taps from falling into those
frames.
- Hold a key and watch it stay green; the yellow flash ends after 12 ticks while the key
is still held.
- Press Shift, Ctrl, Alt, and the OS key on both sides to see that left and right have
separate codes.
- Click the canvas if keys stop responding - focus has moved elsewhere on the page.
WHY 600 BY 315?
---------------
That is exactly half of the 1200x630 social card the demo site generates, so the card
shows the screen at a clean 2x scale with no blurry resampling - the same size the System
Font Glyphs demo uses, so the two pages' key-cap blocks match pixel for pixel.
*/
import { 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 BUTTON_LOOKBUTTON_LOOK, import FONT_WFONT_W, import litRimColorlitRimColor, import uiui, import UI_ANCHORSUI_ANCHORS } from './shared/ui.js';
/** @typedef {import('blit386').Palette} Palette */
/** @typedef {{ code: string, label: string, x: number, y: number, w: number, pressTimer: number, releaseTimer: number }} KeyDef */
/** @typedef {{ code: string, label: string, w?: number, gapBefore?: number, at?: number }} KeyRowEntry */
// The screen is 600x315 display pixels: exactly half of the 1200x630 social card, and the
// same size as the System Font Glyphs demo whose cells the key caps match.
const const DISPLAY_W: 600DISPLAY_W = 600;
const const DISPLAY_H: 315DISPLAY_H = 315;
// Scene palette slots for the three key-cap flash states. These stay demo-owned (not
// shared UI theme colors) because the legend and the key caps must show the exact same
// green / yellow / red the diagnostic is about. Everything else (cap faces, panels, text)
// comes from the shared UI theme installed in init().
const const C_LIT_HELD: 1C_LIT_HELD = 1; // key-cap fill while BT.isKeyDown is true (green)
const const C_LIT_PRESS: 2C_LIT_PRESS = 2; // key-cap fill during the press flash (yellow)
const const C_LIT_RELEASE: 3C_LIT_RELEASE = 3; // key-cap fill during the release flash (red)
// A lighter shade of each flash color for the lit cap's rim, written in init(). Each is
// the flash color brightened by exactly the step between the theme's resting button fill
// and its border, so a lit cap's rim stands out from its fill as much as a resting cap's
// does.
const const C_RIM_HELD: 4C_RIM_HELD = 4;
const const C_RIM_PRESS: 5C_RIM_PRESS = 5;
const const C_RIM_RELEASE: 6C_RIM_RELEASE = 6;
// The three flash colors, each with the slot of its lighter rim.
const const FLASH_RIMS: {}FLASH_RIMS = [
{ fill: numberfill: const C_LIT_HELD: 1C_LIT_HELD, rim: numberrim: const C_RIM_HELD: 4C_RIM_HELD },
{ fill: numberfill: const C_LIT_PRESS: 2C_LIT_PRESS, rim: numberrim: const C_RIM_PRESS: 5C_RIM_PRESS },
{ fill: numberfill: const C_LIT_RELEASE: 3C_LIT_RELEASE, rim: numberrim: const C_RIM_RELEASE: 6C_RIM_RELEASE },
];
// How many fixed ticks a press or release flash stays visible.
const const FLASH_TICKS: 12FLASH_TICKS = 12;
// Key caps match the System Font Glyphs demo's cells: 23 pixels square with a 2-pixel
// gap, so one "unit" (the width of a standard key) is 25 pixels from one cap's left edge
// to the next. A key that is w units wide is w x 25 - 2 pixels wide, so its right edge
// still leaves the 2-pixel gap before the next cap.
const const KEY_SIZE: 23KEY_SIZE = 23;
const const KEY_GAP: 2KEY_GAP = 2;
const const UNIT: numberUNIT = const KEY_SIZE: 23KEY_SIZE + const KEY_GAP: 2KEY_GAP;
// The whole keyboard picture as data: six rows, top to bottom, measured in units. Each
// key has a `code` (KeyboardEvent.code string) and a `label` drawn on the cap.
// addKeyRow() walks each row like laying tiles on a shelf: it keeps a running position,
// places a key, then moves right by the key's width. A key only needs extra fields when
// it breaks the pattern: `w` for wide or narrow caps (Backspace, Enter, Space),
// `gapBefore` for the half-unit gaps between F-key clusters, and `at` when a key jumps to
// a fixed column (the arrow cluster on the right). Every row of the main block is 15
// units wide, the standard keyboard proportions; the arrows sit half a unit to its right.
/** @type {KeyRowEntry[][]} */
const const KEYBOARD_ROWS: {}KEYBOARD_ROWS = [
// Function row: Esc, then the twelve F-keys in three clusters of four, half a unit
// apart.
[
{ code: stringcode: 'Escape', label: stringlabel: 'Esc' },
{ code: stringcode: 'F1', label: stringlabel: 'F1', gapBefore: numbergapBefore: 1 },
{ code: stringcode: 'F2', label: stringlabel: 'F2' },
{ code: stringcode: 'F3', label: stringlabel: 'F3' },
{ code: stringcode: 'F4', label: stringlabel: 'F4' },
{ code: stringcode: 'F5', label: stringlabel: 'F5', gapBefore: numbergapBefore: 0.5 },
{ code: stringcode: 'F6', label: stringlabel: 'F6' },
{ code: stringcode: 'F7', label: stringlabel: 'F7' },
{ code: stringcode: 'F8', label: stringlabel: 'F8' },
{ code: stringcode: 'F9', label: stringlabel: 'F9', gapBefore: numbergapBefore: 0.5 },
{ code: stringcode: 'F10', label: stringlabel: 'F10' },
{ code: stringcode: 'F11', label: stringlabel: 'F11' },
{ code: stringcode: 'F12', label: stringlabel: 'F12' },
],
// Number row: thirteen standard keys, then a 2-unit Backspace.
[
{ code: stringcode: 'Backquote', label: stringlabel: '`' },
{ code: stringcode: 'Digit1', label: stringlabel: '1' },
{ code: stringcode: 'Digit2', label: stringlabel: '2' },
{ code: stringcode: 'Digit3', label: stringlabel: '3' },
{ code: stringcode: 'Digit4', label: stringlabel: '4' },
{ code: stringcode: 'Digit5', label: stringlabel: '5' },
{ code: stringcode: 'Digit6', label: stringlabel: '6' },
{ code: stringcode: 'Digit7', label: stringlabel: '7' },
{ code: stringcode: 'Digit8', label: stringlabel: '8' },
{ code: stringcode: 'Digit9', label: stringlabel: '9' },
{ code: stringcode: 'Digit0', label: stringlabel: '0' },
{ code: stringcode: 'Minus', label: stringlabel: '-' },
{ code: stringcode: 'Equal', label: stringlabel: '=' },
{ code: stringcode: 'Backspace', label: stringlabel: 'Back', w: numberw: 2 },
],
// QWERTY row: a 1.5-unit Tab, standard keys, and a 1.5-unit backslash.
[
{ code: stringcode: 'Tab', label: stringlabel: 'Tab', w: numberw: 1.5 },
{ code: stringcode: 'KeyQ', label: stringlabel: 'Q' },
{ code: stringcode: 'KeyW', label: stringlabel: 'W' },
{ code: stringcode: 'KeyE', label: stringlabel: 'E' },
{ code: stringcode: 'KeyR', label: stringlabel: 'R' },
{ code: stringcode: 'KeyT', label: stringlabel: 'T' },
{ code: stringcode: 'KeyY', label: stringlabel: 'Y' },
{ code: stringcode: 'KeyU', label: stringlabel: 'U' },
{ code: stringcode: 'KeyI', label: stringlabel: 'I' },
{ code: stringcode: 'KeyO', label: stringlabel: 'O' },
{ code: stringcode: 'KeyP', label: stringlabel: 'P' },
{ code: stringcode: 'BracketLeft', label: stringlabel: '[' },
{ code: stringcode: 'BracketRight', label: stringlabel: ']' },
{ code: stringcode: 'Backslash', label: stringlabel: '\\', w: numberw: 1.5 },
],
// Home row: a 1.75-unit Caps Lock, standard letter keys, and a 2.25-unit Enter.
[
{ code: stringcode: 'CapsLock', label: stringlabel: 'Caps', w: numberw: 1.75 },
{ code: stringcode: 'KeyA', label: stringlabel: 'A' },
{ code: stringcode: 'KeyS', label: stringlabel: 'S' },
{ code: stringcode: 'KeyD', label: stringlabel: 'D' },
{ code: stringcode: 'KeyF', label: stringlabel: 'F' },
{ code: stringcode: 'KeyG', label: stringlabel: 'G' },
{ code: stringcode: 'KeyH', label: stringlabel: 'H' },
{ code: stringcode: 'KeyJ', label: stringlabel: 'J' },
{ code: stringcode: 'KeyK', label: stringlabel: 'K' },
{ code: stringcode: 'KeyL', label: stringlabel: 'L' },
{ code: stringcode: 'Semicolon', label: stringlabel: ';' },
{ code: stringcode: 'Quote', label: stringlabel: "'" },
{ code: stringcode: 'Enter', label: stringlabel: 'Enter', w: numberw: 2.25 },
],
// Bottom letter row: a 2.25-unit left Shift, standard keys, a 2.75-unit right Shift,
// and the up arrow pinned to the middle column of the arrow cluster.
[
{ code: stringcode: 'ShiftLeft', label: stringlabel: 'Shift', w: numberw: 2.25 },
{ code: stringcode: 'KeyZ', label: stringlabel: 'Z' },
{ code: stringcode: 'KeyX', label: stringlabel: 'X' },
{ code: stringcode: 'KeyC', label: stringlabel: 'C' },
{ code: stringcode: 'KeyV', label: stringlabel: 'V' },
{ code: stringcode: 'KeyB', label: stringlabel: 'B' },
{ code: stringcode: 'KeyN', label: stringlabel: 'N' },
{ code: stringcode: 'KeyM', label: stringlabel: 'M' },
{ code: stringcode: 'Comma', label: stringlabel: ',' },
{ code: stringcode: 'Period', label: stringlabel: '.' },
{ code: stringcode: 'Slash', label: stringlabel: '/' },
{ code: stringcode: 'ShiftRight', label: stringlabel: 'Shift', w: numberw: 2.75 },
{ code: stringcode: 'ArrowUp', label: stringlabel: '\u2191', at: numberat: 16.5 },
],
// Modifier row: six 1.5-unit modifiers around a 6-unit Space, filling the same 15
// units as every other row (6 x 1.5 + 6 = 15), then the left, down, and right arrows
// starting half a unit past the main block, under the up arrow.
[
{ code: stringcode: 'ControlLeft', label: stringlabel: 'Ctrl', w: numberw: 1.5 },
{ code: stringcode: 'MetaLeft', label: stringlabel: 'Win', w: numberw: 1.5 },
{ code: stringcode: 'AltLeft', label: stringlabel: 'Alt', w: numberw: 1.5 },
{ code: stringcode: 'Space', label: stringlabel: 'Space', w: numberw: 6 },
{ code: stringcode: 'AltRight', label: stringlabel: 'Alt', w: numberw: 1.5 },
{ code: stringcode: 'MetaRight', label: stringlabel: 'Win', w: numberw: 1.5 },
{ code: stringcode: 'ControlRight', label: stringlabel: 'Ctrl', w: numberw: 1.5 },
{ code: stringcode: 'ArrowLeft', label: stringlabel: '\u2190', at: numberat: 15.5 },
{ code: stringcode: 'ArrowDown', label: stringlabel: '\u2193' },
{ code: stringcode: 'ArrowRight', label: stringlabel: '\u2192' },
],
];
// The keyboard is 18.5 units wide (15 for the main block, half a unit of gap, 3 for the
// arrows) and 6 rows tall, with an extra 6 pixels between the function row and the rest.
const const KEYBOARD_UNITS_W: 18.5KEYBOARD_UNITS_W = 18.5;
const const KEYBOARD_W: numberKEYBOARD_W = const KEYBOARD_UNITS_W: 18.5KEYBOARD_UNITS_W * const UNIT: numberUNIT - const KEY_GAP: 2KEY_GAP;
const const FN_ROW_GAP: 6FN_ROW_GAP = 6;
const const KEYBOARD_H: numberKEYBOARD_H = const KEYBOARD_ROWS: {}KEYBOARD_ROWS.length * const UNIT: numberUNIT - const KEY_GAP: 2KEY_GAP + const FN_ROW_GAP: 6FN_ROW_GAP;
// Vertical layout under the 32-pixel title strip (a 22-pixel strip that grows by its hint
// row): the keyboard, a 10-pixel gap, the 12-pixel legend row, another 10-pixel gap, and
// the two 63-pixel status panels (22 title + 3 rows of 10 + 11 bottom). The whole block
// is centered in the space below the strip, and everything is centered across the
// screen.
const const TITLE_STRIP_H: 32TITLE_STRIP_H = 32;
const const LEGEND_H: 12LEGEND_H = 12;
const const STATUS_PANEL_H: numberSTATUS_PANEL_H = 22 + 3 * 10 + 11;
const const BLOCK_H: numberBLOCK_H = const KEYBOARD_H: numberKEYBOARD_H + 10 + const LEGEND_H: 12LEGEND_H + 10 + const STATUS_PANEL_H: numberSTATUS_PANEL_H;
const const KEYBOARD_X: anyKEYBOARD_X = Math.floor((const DISPLAY_W: 600DISPLAY_W - const KEYBOARD_W: numberKEYBOARD_W) / 2);
const const KEYBOARD_Y: anyKEYBOARD_Y = const TITLE_STRIP_H: 32TITLE_STRIP_H + Math.floor((const DISPLAY_H: 315DISPLAY_H - const TITLE_STRIP_H: 32TITLE_STRIP_H - const BLOCK_H: numberBLOCK_H) / 2);
const const LEGEND_Y: anyLEGEND_Y = const KEYBOARD_Y: anyKEYBOARD_Y + const KEYBOARD_H: numberKEYBOARD_H + 10;
const const STATUS_PANEL_Y: anySTATUS_PANEL_Y = const LEGEND_Y: anyLEGEND_Y + const LEGEND_H: 12LEGEND_H + 10;
// The two status panels share the keyboard's width, 4 pixels apart.
const const STATUS_PANEL_W: numberSTATUS_PANEL_W = (const KEYBOARD_W: numberKEYBOARD_W - 4) / 2;
// The legend: one small block per flash color and a dim label after it.
const const LEGEND_ITEMS: {}LEGEND_ITEMS = [
{ slot: numberslot: const C_LIT_HELD: 1C_LIT_HELD, label: stringlabel: 'Held' },
{ slot: numberslot: const C_LIT_PRESS: 2C_LIT_PRESS, label: stringlabel: 'Press flash' },
{ slot: numberslot: const C_LIT_RELEASE: 3C_LIT_RELEASE, label: stringlabel: 'Release flash' },
];
const const LEGEND_BLOCK: 8LEGEND_BLOCK = 8;
// Full keyboard layout diagnostic for edge-trigger testing.
class class DemoDemo {
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = null;
// Palette slots of the shared UI theme colors, filled by applyTheme() in init().
/** @type {ReturnType<typeof applyTheme> | null} */
Demo.theme: anytheme = null;
// One entry per on-screen key cap, built by initKeyboardLayout(): `code`, `label`,
// the cap's left edge, top edge, and width (`x`, `y`, `w`; every cap is KEY_SIZE
// tall), and two countdowns in ticks, `pressTimer` and `releaseTimer`, for the press
// and release flashes.
/** @type {KeyDef[]} */
Demo.keys: {}keys = [];
// The most recent key edge for the Last edge panel: which kind it was, the key code,
// and the engine tick it fired on (null before the first one).
Demo.lastEdge: stringlastEdge = '-';
Demo.lastCode: stringlastCode = 'press any key';
Demo.lastTick: nulllastTick = null;
// How many press edges we have seen in total. Fast-tap test: this must match
// releases.
Demo.pressCount: numberpressCount = 0;
// How many release edges we have seen in total.
Demo.releaseCount: numberreleaseCount = 0;
// Reused for every rectangle and label position 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.tempVec: Vector2itempVec = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(0, 0);
// Glyph height of the system font, measured in init() for centering labels.
Demo.glyphHeight: numberglyphHeight = 14;
// A 600x315 display rendered into a 1200x630 buffer with nearest-neighbor upscaling,
// locked to 60 ticks per second so the display refresh rate and the tick rate differ
// on fast monitors.
Demo.configure(): {
displaySize: Vector2i;
drawingBufferSize: Vector2i;
maxCanvasSize: Vector2i;
outputUpscaleFilter: string;
targetFPS: number;
isOverlayEnabled: boolean;
isCapturingKeyboardScroll: boolean;
}
configure() {
return {
displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const DISPLAY_W: 600DISPLAY_W, const DISPLAY_H: 315DISPLAY_H),
drawingBufferSize: Vector2idrawingBufferSize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const DISPLAY_W: 600DISPLAY_W * 2, const DISPLAY_H: 315DISPLAY_H * 2),
maxCanvasSize: Vector2imaxCanvasSize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const DISPLAY_W: 600DISPLAY_W * 2, const DISPLAY_H: 315DISPLAY_H * 2),
outputUpscaleFilter: stringoutputUpscaleFilter: 'nearest',
targetFPS: numbertargetFPS: 60,
isOverlayEnabled: booleanisOverlayEnabled: false,
// This page lights up arrow keys and Space on the on-screen keyboard. Opt in
// so those keys do not scroll the demo page while you are testing them.
isCapturingKeyboardScroll: booleanisCapturingKeyboardScroll: true,
};
}
// Palette setup, shared UI theme, and the key cap layout.
async Demo.init(): unknowninit() {
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);
// The three flash colors live in low scene slots; the legend and the key caps
// both draw with them so the colors always match.
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_LIT_HELD: 1C_LIT_HELD, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(75, 210, 120, 255));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_LIT_PRESS: 2C_LIT_PRESS, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(255, 230, 80, 255));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_LIT_RELEASE: 3C_LIT_RELEASE, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(240, 80, 80, 255));
// applyTheme() installs the twelve shared UI colors (background, button faces,
// text, ...) high in the palette and reports their slots, so the key caps and the
// kit widgets all draw from one consistent theme.
this.Demo.theme: anytheme = import applyThemeapplyTheme(this.Demo.palette: Palettepalette);
// The lit caps' rims: each flash color brightened by the step from the theme's
// resting button fill to its border. litRimColor() reads both from the palette,
// so the two always match. palette.get() returns a copy of the color in a slot.
for (const { const fill: anyfill, const rim: anyrim } of const FLASH_RIMS: {}FLASH_RIMS) {
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const rim: anyrim, 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 fill: anyfill)));
}
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);
// Measure the system font once so every label can be centered on its cap.
this.Demo.glyphHeight: numberglyphHeight = 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.systemPrintMeasure: (text: string) => Vector2iMeasures the pixel dimensions of a string rendered with the built-in
system font.systemPrintMeasure('M').Vector2i.y: numberVertical component (defaults to 0).y;
// Start every run from a clean slate. init() runs again whenever BT.renderAt()
// replays the demo from the start (the /shots page and the social card do that),
// and fields keep their old values across that replay - so the counters and the
// last edge are reset here, and the key list is rebuilt rather than added to.
// Without this, every key would sit in the list twice and each press would count
// twice.
this.Demo.lastEdge: stringlastEdge = '-';
this.Demo.lastCode: stringlastCode = 'press any key';
this.Demo.lastTick: nulllastTick = null;
this.Demo.pressCount: numberpressCount = 0;
this.Demo.releaseCount: numberreleaseCount = 0;
this.Demo.initKeyboardLayout(): voidinitKeyboardLayout();
return true;
}
// Reads the press and release edges for every key and runs the flash countdowns.
Demo.update(): voidupdate() {
// Let the UI kit track touch contacts first - ui.hasTouch() in render() relies on
// this housekeeping running every update tick.
import uiui.tick();
for (let let i: numberi = 0; let i: numberi < this.Demo.keys: {}keys.length; let i: numberi++) {
const const key: anykey = this.Demo.keys: {}keys[let i: numberi];
// Whichever edge fires most recently owns the flash: starting one flash
// cancels the other, so a fast tap cannot let two competing countdowns reach
// zero together and skip the release color entirely.
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.isKeyPressed: (key: string, repeatRate?: number) => booleanChecks whether a keyboard key was pressed on the current fixed-update tick.
Optional `repeatRate` is in fixed ticks between repeats (`0` or omitted =
edge only). When `repeatRate > 0`, repeats fire while held per
`(ticks - firstPressTick) > 0 && (ticks - firstPressTick) % repeatRate === 0`.
Call from `update()`, not `render()`: the press edge clears once per fixed-update
tick, which always runs before that frame's `render()`, so a press read from
`render()` can be intermittently missed under rapid input.isKeyPressed(const key: anykey.code)) {
const key: anykey.pressTimer = const FLASH_TICKS: 12FLASH_TICKS;
const key: anykey.releaseTimer = 0;
this.Demo.lastEdge: stringlastEdge = 'pressed';
this.Demo.lastCode: stringlastCode = const key: anykey.code;
this.Demo.lastTick: nulllastTick = 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.pressCount: numberpressCount += 1;
}
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.isKeyReleased: (key: string) => booleanChecks whether a keyboard key was released on the current frame.
Call from `update()`, not `render()`: the release edge clears once per fixed-update
tick, which always runs before that frame's `render()`, so a release read from
`render()` can be intermittently missed under rapid input.isKeyReleased(const key: anykey.code)) {
const key: anykey.releaseTimer = const FLASH_TICKS: 12FLASH_TICKS;
const key: anykey.pressTimer = 0;
this.Demo.lastEdge: stringlastEdge = 'released';
this.Demo.lastCode: stringlastCode = const key: anykey.code;
this.Demo.lastTick: nulllastTick = 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.releaseCount: numberreleaseCount += 1;
}
// Count both flashes down by one tick.
if (const key: anykey.pressTimer > 0) {
const key: anykey.pressTimer -= 1;
}
if (const key: anykey.releaseTimer > 0) {
const key: anykey.releaseTimer -= 1;
}
}
}
// Draws the title strip, the key caps, the legend, and the two status panels.
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: anytheme.bg);
// Full-width title strip. The second row is contextual: touch devices get a
// warning that the demo is pointless without a keyboard, everyone else gets the
// fast-tap testing hint.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_BAR);
import uiui.panel('BLIT386 Keyboard Diagnostic');
if (import uiui.hasTouch()) {
import uiui.label('This demo needs a keyboard', { color: stringcolor: 'warm' });
} else {
import uiui.label('Tap fast on a 120 Hz display - every press flash needs a release flash', { color: stringcolor: 'dim' });
}
import uiui.end();
for (let let i: numberi = 0; let i: numberi < this.Demo.keys: {}keys.length; let i: numberi++) {
this.Demo.renderKey(key: any): voidrenderKey(this.Demo.keys: {}keys[let i: numberi]);
}
this.Demo.renderLegend(): voidrenderLegend();
this.Demo.renderStatusPanels(): voidrenderStatusPanels();
}
// Draws one key cap in its current state: held wins over the press flash, which wins
// over the release flash.
Demo.renderKey(key: any): voidrenderKey(key: anykey) {
const const isDown: booleanisDown = 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.isKeyDown: (key: string) => booleanChecks whether a keyboard key is currently held.
Uses `KeyboardEvent.code` (for example `"KeyW"`, `"Space"`, `"ArrowUp"`).isKeyDown(key: anykey.code);
const const isPressed: booleanisPressed = key: anykey.pressTimer > 0;
const const isReleased: booleanisReleased = key: anykey.releaseTimer > 0;
// Which flash color the cap wears right now (0 for none), and its lighter rim.
let let fillSlot: numberfillSlot = 0;
let let rimSlot: numberrimSlot = 0;
if (const isDown: booleanisDown) {
let fillSlot: numberfillSlot = const C_LIT_HELD: 1C_LIT_HELD;
let rimSlot: numberrimSlot = const C_RIM_HELD: 4C_RIM_HELD;
} else if (const isPressed: booleanisPressed) {
let fillSlot: numberfillSlot = const C_LIT_PRESS: 2C_LIT_PRESS;
let rimSlot: numberrimSlot = const C_RIM_PRESS: 5C_RIM_PRESS;
} else if (const isReleased: booleanisReleased) {
let fillSlot: numberfillSlot = const C_LIT_RELEASE: 3C_LIT_RELEASE;
let rimSlot: numberrimSlot = const C_RIM_RELEASE: 6C_RIM_RELEASE;
}
// The cap is the same rounded block the System Font Glyphs demo draws its cells
// with: a kit button face. A resting cap uses the plain face and a dim label. A
// lit cap uses ui.drawButtonFill() - the same face, filled with the scene flash
// color and rimmed with its lighter shade - and prints its label in the dark
// background color, so it reads clearly on the bright yellow, green, or red.
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(key: anykey.x, key: anykey.y, key: anykey.w, const KEY_SIZE: 23KEY_SIZE);
if (let fillSlot: numberfillSlot === 0) {
import uiui.drawButtonFace(this.Demo.tempRect: Rect2itempRect, import BUTTON_LOOKBUTTON_LOOK.REST);
} else {
import uiui.drawButtonFill(this.Demo.tempRect: Rect2itempRect, let fillSlot: numberfillSlot, let rimSlot: numberrimSlot);
}
const const textSlot: anytextSlot = let fillSlot: numberfillSlot === 0 ? this.Demo.theme: anytheme.dim : this.Demo.theme: anytheme.bg;
// Center the label on the cap. Every character of the system font is FONT_W (6)
// pixels wide, but its pixels fill only the first 5 columns - the 6th is the gap
// before the next character - so the visible text is one pixel narrower than
// length x 6, and centering on that keeps it truly in the middle. Vertically, the
// glyph cell sits one pixel below plain centering, which balances the capital
// letters between the cap's top edge and its slightly heavier bottom rim.
// BT.systemPrint() draws text as sprites too, so it lands on top of the face.
const const textW: numbertextW = key: anykey.label.length * import FONT_WFONT_W - 1;
this.Demo.tempVec: Vector2itempVec.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(
key: anykey.x + Math.floor((key: anykey.w - const textW: numbertextW) / 2),
key: anykey.y + Math.floor((const KEY_SIZE: 23KEY_SIZE - this.Demo.glyphHeight: numberglyphHeight) / 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.systemPrint: (pos: Vector2i, paletteIndex: number, text: string) => voidDraws text using the built-in 6x14 system font.
The system font covers printable ASCII (characters 32-126). For custom
bitmap fonts with proportional glyphs, use
{@link
BT.printFont
}
instead.systemPrint(this.Demo.tempVec: Vector2itempVec, const textSlot: anytextSlot, key: anykey.label);
}
// Draws the legend under the keyboard: for each flash color, a small solid square of
// that color and a dim label, the row centered across the screen.
Demo.renderLegend(): voidrenderLegend() {
// Work out the row's width first: block, 4 pixels, label, then 16 pixels before
// the next item.
let let rowW: numberrowW = 0;
for (let let i: numberi = 0; let i: numberi < const LEGEND_ITEMS: {}LEGEND_ITEMS.length; let i: numberi++) {
let rowW: numberrowW += const LEGEND_BLOCK: 8LEGEND_BLOCK + 4 + const LEGEND_ITEMS: {}LEGEND_ITEMS[let i: numberi].label.length * import FONT_WFONT_W + (let i: numberi > 0 ? 16 : 0);
}
let let x: anyx = Math.floor((const DISPLAY_W: 600DISPLAY_W - let rowW: numberrowW) / 2);
for (const const item: anyitem of const LEGEND_ITEMS: {}LEGEND_ITEMS) {
// The square covers rows LEGEND_Y + 2 to LEGEND_Y + 9 of the 12-pixel row.
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: anyx, const LEGEND_Y: anyLEGEND_Y + 2, const LEGEND_BLOCK: 8LEGEND_BLOCK, const LEGEND_BLOCK: 8LEGEND_BLOCK);
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.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(this.Demo.tempRect: Rect2itempRect, const item: anyitem.slot);
// A caption's capital letters cover 7 rows starting 1 row above the y it is
// given, so LEGEND_Y + 4 puts them on rows LEGEND_Y + 3 to LEGEND_Y + 9,
// level with the square.
import uiui.caption(let x: anyx + const LEGEND_BLOCK: 8LEGEND_BLOCK + 4, const LEGEND_Y: anyLEGEND_Y + 4, const item: anyitem.label, { color: stringcolor: 'dim' });
let x: anyx += const LEGEND_BLOCK: 8LEGEND_BLOCK + 4 + const item: anyitem.label.length * import FONT_WFONT_W + 16;
}
}
// The two status panels under the legend, lined up with the keyboard's edges.
Demo.renderStatusPanels(): voidrenderStatusPanels() {
// Which edge fired last, on which key, and on which engine tick.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { x: anyx: const KEYBOARD_X: anyKEYBOARD_X, y: anyy: const STATUS_PANEL_Y: anySTATUS_PANEL_Y, width: numberwidth: const STATUS_PANEL_W: numberSTATUS_PANEL_W });
import uiui.panel('Last edge');
import uiui.kv('Edge', this.Demo.lastEdge: stringlastEdge);
import uiui.kv('Code', this.Demo.lastCode: stringlastCode);
import uiui.kv('Tick', this.Demo.lastTick: nulllastTick === null ? '-' : this.Demo.lastTick: neverlastTick);
import uiui.end();
// Edge counters: after a burst of fast taps both numbers must match - a mismatch
// means an edge was dropped somewhere. The pip makes that visible at a glance.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, {
x: anyx: const KEYBOARD_X: anyKEYBOARD_X + const STATUS_PANEL_W: numberSTATUS_PANEL_W + 4,
y: anyy: const STATUS_PANEL_Y: anySTATUS_PANEL_Y,
width: numberwidth: const STATUS_PANEL_W: numberSTATUS_PANEL_W,
});
import uiui.panel('Edge counts');
import uiui.kv('Presses', this.Demo.pressCount: numberpressCount);
import uiui.kv('Releases', this.Demo.releaseCount: numberreleaseCount);
import uiui.pip('Counts match', this.Demo.pressCount: numberpressCount === this.Demo.releaseCount: numberreleaseCount);
import uiui.end();
}
// Appends one key definition to `this.keys` with its flash timers reset to zero.
// `x`, `y`, `w` are the cap's left edge, top edge, and width in display pixels.
Demo.addKey(code: any, label: any, x: any, y: any, w: any): voidaddKey(code: anycode, label: anylabel, x: anyx, y: anyy, w: anyw) {
this.Demo.keys: {}keys.push({ code: anycode, label: anylabel, x: anyx, y: anyy, w: anyw, pressTimer: numberpressTimer: 0, releaseTimer: numberreleaseTimer: 0 });
}
// Lays out one row of KEYBOARD_ROWS, left to right, at the top edge `y`. The running
// position `u` (in units) starts at 0, the keyboard's left edge. For each key we
// first honor its quirks - jump to an absolute column (`at`) or skip extra space
// (`gapBefore`) - then place the cap and step right by its width, ready for the next
// key. Units become pixels by multiplying by UNIT; Math.round keeps every edge on a
// whole pixel for the quarter-unit widths.
Demo.addKeyRow(row: any, y: any): voidaddKeyRow(row: anyrow, y: anyy) {
let let u: numberu = 0;
for (const const key: anykey of row: anyrow) {
// A fixed column wins over the flowing position (used by the arrow cluster).
if (typeof const key: anykey.at === 'number') {
let u: numberu = const key: anykey.at;
}
// Extra breathing room before this key, like the gap between F-key clusters.
// The ?? operator means "use the left value unless it is missing, then 0".
let u: numberu += const key: anykey.gapBefore ?? 0;
// Wide and narrow caps say so; everyone else is one unit wide.
const const w: anyw = const key: anykey.w ?? 1;
const const x: anyx = const KEYBOARD_X: anyKEYBOARD_X + Math.round(let u: numberu * const UNIT: numberUNIT);
const const right: numberright = const KEYBOARD_X: anyKEYBOARD_X + Math.round((let u: numberu + const w: anyw) * const UNIT: numberUNIT) - const KEY_GAP: 2KEY_GAP;
this.Demo.addKey(code: any, label: any, x: any, y: any, w: any): voidaddKey(const key: anykey.code, const key: anykey.label, const x: anyx, y: anyy, const right: numberright - const x: anyx);
let u: numberu += const w: anyw;
}
}
// Builds the on-screen key cap list (positions only; state lives on each entry).
Demo.initKeyboardLayout(): voidinitKeyboardLayout() {
// A fresh, empty list every time, so a second init() replaces the caps instead of
// adding a second copy of each.
this.Demo.keys: {}keys = [];
// The layout itself lives in KEYBOARD_ROWS near the top of the file; here we just
// walk the six rows and let addKeyRow() place every cap. Every row after the
// function row sits FN_ROW_GAP pixels lower, like a real keyboard.
for (let let i: numberi = 0; let i: numberi < const KEYBOARD_ROWS: {}KEYBOARD_ROWS.length; let i: numberi++) {
const const y: anyy = const KEYBOARD_Y: anyKEYBOARD_Y + let i: numberi * const UNIT: numberUNIT + (let i: numberi > 0 ? const FN_ROW_GAP: 6FN_ROW_GAP : 0);
this.Demo.addKeyRow(row: any, y: any): voidaddKeyRow(const KEYBOARD_ROWS: {}KEYBOARD_ROWS[let i: numberi], const y: anyy);
}
}
}
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);