/*

Nine-Slice Panels: one piece of panel art that fills a box of any size.
@description Draw one nine-slice panel at any size: stretch or tile its edges, crop corners, recolor, build bars.

Part of the BLIT386 demo series.

Prerequisites:
  - Basics: https://demos.blit386.dev/basics
  - Sprites: https://demos.blit386.dev/sprites

Live version: https://demos.blit386.dev/nine-slice-panels

Guides:
  - Palette: https://blit386.dev/docs/guides/palette#per-draw-palette-offsets-zero-cost-color-variants

Reference:
  - Rendering: https://blit386.dev/docs/api/rendering#nine-slice-panels
  - Assets: https://blit386.dev/docs/api/assets#loading-assets
  - Palette: https://blit386.dev/docs/api/palette#fill-a-block-of-slots

---

WHAT YOU WILL SEE
-----------------

A 640x400 screen split into two columns of small sections, each with a dim caption:

  1. One asset, any box (top-left) - the same blue panel drawn at five sizes, from 20x20
     to 100x52, with its size printed under each one.
  2. Stretch vs tile vs mixed (left, middle) - three boxes of exactly the same size,
     filled three ways. Stretch smears the dashes into fat bars, tile keeps them crisp,
     mixed tiles the border around a stretched center.
  3. Open from 0x0 (top-right) - a panel that grows from nothing to 140x90 in one second,
     holds, and starts over every three seconds. A "Replay (R)" button restarts it.
  4. Palette offsets (right, middle) - the same panel recolored three ways, labeled Idle,
     Hover, and Alert.
  5. Three-slice bar (right, lower) - two fixed bars plus one whose width swings between
     20 and 200 pixels.
  6. Panel from public/sprites/panel.png (left, lower) - a rounded panel loaded from a
     real PNG file, slowly growing and shrinking between 40x24 and 180x64.

A bordered kit panel in the bottom-left corner shows the two calls that matter.

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

- NineSlice.fromSheet(sheet, outer, inner, options) cuts one sheet region into a 3x3 grid
  once, in init(). The options are { edges, center }, each 'stretch' (the default) or
  'tile'.
- BT.drawNineSlice(panel, destRect, paletteOffset) draws it into any box, allocating
  nothing, so the demo builds every NineSlice and every reused Rect2i in init().
- When inner touches the top and bottom of outer, there are no top or bottom edges: a
  three-slice bar with a left cap, a middle, and a right cap.
- A box smaller than its two corners crops the corners instead of squashing them, so a
  panel can animate open from 0x0 and keep its outline.
- Recoloring is paletteOffset only, exactly as for sprites. palette.fillBlock() writes the
  recolored copies of the art's colors into later slots.
- SpriteSheet.loadIndexed() loads a PNG and registers its colors in the palette in one
  call.
- Painting pixel art on an OffscreenCanvas and turning it into an indexed sprite sheet,
  the same trick as the Sprites demo.

WHAT IS A NINE-SLICE?
---------------------

Games draw a lot of boxes: dialog windows, buttons, health bars. You could paint a new
picture for every size, but there is a smarter way. A nine-slice cuts ONE small picture
into a 3x3 grid:

  +--------+--------+--------+
  | corner |  edge  | corner |  Corners are copied 1:1, so they never get squashed.
  +--------+--------+--------+  Edges and the center are stretched (blown up to fit)
  |  edge  | center |  edge  |  or tiled (repeated like bathroom tiles) to fill the box.
  +--------+--------+--------+
  | corner |  edge  | corner |
  +--------+--------+--------+

A word of warning: each tile is one quad (one little rectangle the graphics card draws).
Tiling a 1-pixel strip across a big box costs one quad per pixel and can fill the engine's
sprite buffer. So tile PATTERNED art that is a few pixels wide (like the 8-pixel strips
here), and stretch flat fills.

TRY THIS
--------

- Press R (or tap "Replay (R)") to restart the "Open from 0x0" animation and watch the
  corners share the tiny box instead of squashing.
- Compare the dashes in section 2: the Stretch box turns them into long bars, the Tile box
  keeps them crisp and cuts the last tile off at the box edge.

*/

import { function applyEasing(t: number, easing: EasingFunction): number
Applies an easing curve to a normalized time value.
@since1.0.3@changed1.5.0 Added sine, cubic, quartic, quintic, expo, circ, back, elastic, and bounce families.@paramt - Normalized time in [0, 1]. Values outside this range are not clamped.@parameasing - Easing curve to apply.@returnsEased value. Guaranteed to return 0 for `t = 0` and 1 for `t = 1`.
applyEasing
, function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
,
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
, class Color32
Mutable 32-bit RGBA color value with 8-bit channels.
@since0.1.0
Color32
, class NineSlice
A sprite region split into 9 parts for scalable UI panels: 4 corners drawn at 1:1, 4 edges and a center that stretch or tile. Draw it with `BT.drawNineSlice`. Immutable: build it once in `init()` and draw it every frame.
@since1.8.0
NineSlice
, class Rect2i
Integer rectangle for pixel-perfect bounds and regions. Used throughout the engine for sprite regions, display-space bounds, and zero-allocation geometry helpers. Both convenience getters and allocation-free `*To()` helpers are provided so callers can choose between readability and hot-path efficiency.
@since0.1.0
Rect2i
,
type SpriteSheet = SpriteSheet
class SpriteSheet
Sprite-sheet wrapper around a loaded image asset. The class keeps the original image available for CPU-side inspection while lazily creating and caching a GPU texture for rendering. When possible, `load()` also pre-decodes the source into an `ImageBitmap` so texture uploads preserve pixel-art alpha and color values more reliably. After calling `indexize()`, the sheet stores palette indices rather than RGBA data. The GPU texture becomes an `r8uint` format uploaded via `writeTexture`. The original RGBA bytes are retained so `reindexize()` can re-convert without reloading the image.
@since0.1.0
SpriteSheet
, class Vector2i
Integer 2D vector for pixel-perfect positioning. Used for points, sizes, directions, and camera offsets throughout the engine. The API includes both allocation-free `*To()` / `*InPlace()` variants and convenience methods that return new vectors.
@since0.1.0
Vector2i
} from 'blit386';
import { import canvasToImagecanvasToImage, import registerCanvasColorsregisterCanvasColors } from './shared/canvas-sprites.js'; import { import applyThemeapplyTheme, import uiui, import UI_ANCHORSUI_ANCHORS } from './shared/ui.js'; /** @typedef {import('blit386').Palette} Palette */ /** @typedef {import('blit386').SpriteSheet} SpriteSheet */ // Display size: twice the usual 320x200 each way, which leaves room for two columns of // sections. const const DISPLAY_W: 640DISPLAY_W = 640; const const DISPLAY_H: 400DISPLAY_H = 400; // Where in the palette the panel's original colors start. The recolored copies stack // right after them, and the shared UI theme lives far above at slots 240-251. const const COLOR_BASE: 10COLOR_BASE = 10; // The panel art is 18x18 pixels: a 5-pixel border on every side around an 8x8 center. The // strips are 8 pixels wide on purpose - the patterns repeat every 4 pixels, so an 8-pixel // strip tiles without a visible seam. const const PANEL_SIZE: 18PANEL_SIZE = 18; const const PANEL_CAP: 5PANEL_CAP = 5; const const PANEL_INNER: numberPANEL_INNER = const PANEL_SIZE: 18PANEL_SIZE - const PANEL_CAP: 5PANEL_CAP * 2; // The bar art sits in the same sheet, to the right of the panel: 18 pixels wide, 10 tall, // with a 5-pixel cap at each end and an 8-pixel middle. const const BAR_X: 20BAR_X = 20; const const BAR_W: 18BAR_W = 18; const const BAR_H: 10BAR_H = 10; // Total size of the sheet that holds both pieces of art. const const SHEET_W: numberSHEET_W = const BAR_X: 20BAR_X + const BAR_W: 18BAR_W; const const SHEET_H: 18SHEET_H = const PANEL_SIZE: 18PANEL_SIZE; // Section 6 loads a real PNG file instead of painting one. The file is 14x14 pixels: 6x6 // rounded, partly see-through corners around a 2x2 center. Manual-sync hazard: // src/shared/ui-theme.js PANEL_ART is the same art, copied as text for the UI kit's // panels. const const FILE_PANEL_URL: "/sprites/panel.png"FILE_PANEL_URL = '/sprites/panel.png'; const const FILE_PANEL_SIZE: 14FILE_PANEL_SIZE = 14; const const FILE_PANEL_CAP: 6FILE_PANEL_CAP = 6; // The file panel's box keeps its top-left corner in place and grows and shrinks between // the minimum and maximum sizes. One full swing takes about 12 seconds: the sine wave // repeats every 2 * PI * 120 ticks. const const FILE_BOX_X: 12FILE_BOX_X = 12; const const FILE_BOX_Y: 236FILE_BOX_Y = 236; const const FILE_MIN_W: 40FILE_MIN_W = 40; const const FILE_MAX_W: 180FILE_MAX_W = 180; const const FILE_MIN_H: 24FILE_MIN_H = 24; const const FILE_MAX_H: 64FILE_MAX_H = 64; const const FILE_SWING_TICKS: 120FILE_SWING_TICKS = 120; // The seven flat colors of the art. Every pixel uses exactly one of them, so the sheet // maps onto the palette without any blending (the demo never uses smooth, anti-aliased // canvas drawing). const
const ART_COLORS: {
    outline: Color32;
    edgeLight: Color32;
    edgeMid: Color32;
    dash: Color32;
    fill: Color32;
    fillDot: Color32;
    stud: Color32;
}
ART_COLORS
= {
outline: Color32outline: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(0x14, 0x16, 0x3a),
edgeLight: Color32edgeLight: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(0x9a, 0xa8, 0xff),
edgeMid: Color32edgeMid: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(0x4a, 0x58, 0xc8),
dash: Color32dash: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(0xd8, 0xde, 0xff),
fill: Color32fill: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(0x24, 0x2c, 0x70),
fillDot: Color32fillDot: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(0x3a, 0x46, 0xa8),
stud: Color32stud: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(0xff, 0xd0, 0x60),
}; // How long the "open from 0x0" animation runs, in ticks (one tick is 1/60 of a second): // grow for 60 ticks, hold the finished panel, then start over after 180 ticks in total. const const TWEEN_OPEN_TICKS: 60TWEEN_OPEN_TICKS = 60; const const TWEEN_CYCLE_TICKS: 180TWEEN_CYCLE_TICKS = 180; // The largest the tweened panel gets. const const TWEEN_MAX_W: 140TWEEN_MAX_W = 140; const const TWEEN_MAX_H: 90TWEEN_MAX_H = 90; // Where the right-hand column of sections starts. const const RIGHT_X: 340RIGHT_X = 340; // Sizes for section 1: the same asset in five different boxes. const const SIZES: {}SIZES = [ { w: numberw: 20, h: numberh: 20 }, { w: numberw: 36, h: numberh: 28 }, { w: numberw: 60, h: numberh: 40 }, { w: numberw: 100, h: numberh: 52 }, { w: numberw: 40, h: numberh: 52 }, ]; // The three box modes of section 2, in the order they are drawn and labeled. const const MODE_NAMES: {}MODE_NAMES = ['Stretch', 'Tile', 'Mixed']; // The three recolors of section 4. Each name sits under one panel; the offset is a // multiple of the art's color count, so offset 0 is the original and the others jump to a // recolored block. const const RECOLOR_NAMES: {}RECOLOR_NAMES = ['Idle', 'Hover', 'Alert']; // Draws one pixel of the sheet with a flat color. function function plot(ctx: any, x: any, y: any, color: any): voidplot(ctx: anyctx, x: anyx, y: anyy, color: anycolor) { ctx: anyctx.fillStyle = color: anycolor.toHex(); ctx: anyctx.fillRect(x: anyx, y: anyy, 1, 1); } // Picks the color of one panel corner pixel: a dark outline, a gold stud, and a mid-blue // body. `dx` and `dy` are the distance from the nearest left/right and top/bottom outer // edge. function function cornerColorAt(dx: any, dy: any): Color32cornerColorAt(dx: anydx, dy: anydy) { if (dx: anydx === 0 || dy: anydy === 0) { return
const ART_COLORS: {
    outline: Color32;
    edgeLight: Color32;
    edgeMid: Color32;
    dash: Color32;
    fill: Color32;
    fillDot: Color32;
    stud: Color32;
}
ART_COLORS
.outline: Color32outline;
} return dx: anydx >= 2 && dx: anydx <= 3 && dy: anydy >= 2 && dy: anydy <= 3 ?
const ART_COLORS: {
    outline: Color32;
    edgeLight: Color32;
    edgeMid: Color32;
    dash: Color32;
    fill: Color32;
    fillDot: Color32;
    stud: Color32;
}
ART_COLORS
.stud: Color32stud :
const ART_COLORS: {
    outline: Color32;
    edgeLight: Color32;
    edgeMid: Color32;
    dash: Color32;
    fill: Color32;
    fillDot: Color32;
    stud: Color32;
}
ART_COLORS
.edgeMid: Color32edgeMid;
} // Picks the color of one panel edge pixel. `depth` is how far the pixel is from the outer // edge and `along` is how far it is along the edge. The dashes repeat every 4 pixels // ALONG the edge. Stretching smears them into long bars; tiling repeats them crisply. // That is the difference you will see. function function edgeColorAt(depth: any, along: any): Color32edgeColorAt(depth: anydepth, along: anyalong) { if (depth: anydepth === 0) { return
const ART_COLORS: {
    outline: Color32;
    edgeLight: Color32;
    edgeMid: Color32;
    dash: Color32;
    fill: Color32;
    fillDot: Color32;
    stud: Color32;
}
ART_COLORS
.outline: Color32outline;
} if (depth: anydepth === const PANEL_CAP: 5PANEL_CAP - 1) { return
const ART_COLORS: {
    outline: Color32;
    edgeLight: Color32;
    edgeMid: Color32;
    dash: Color32;
    fill: Color32;
    fillDot: Color32;
    stud: Color32;
}
ART_COLORS
.edgeLight: Color32edgeLight;
} return (along: anyalong - const PANEL_CAP: 5PANEL_CAP) % 4 < 2 ?
const ART_COLORS: {
    outline: Color32;
    edgeLight: Color32;
    edgeMid: Color32;
    dash: Color32;
    fill: Color32;
    fillDot: Color32;
    stud: Color32;
}
ART_COLORS
.dash: Color32dash :
const ART_COLORS: {
    outline: Color32;
    edgeLight: Color32;
    edgeMid: Color32;
    dash: Color32;
    fill: Color32;
    fillDot: Color32;
    stud: Color32;
}
ART_COLORS
.edgeMid: Color32edgeMid;
} // Picks the color of one pixel of the 18x18 panel. `dx` and `dy` are the distance from // the nearest left/right and top/bottom outer edge, which lets the same rules paint all // four corners and all four edges. function function panelColorAt(x: any, y: any): Color32panelColorAt(x: anyx, y: anyy) { const const dx: anydx = Math.min(x: anyx, const PANEL_SIZE: 18PANEL_SIZE - 1 - x: anyx); const const dy: anydy = Math.min(y: anyy, const PANEL_SIZE: 18PANEL_SIZE - 1 - y: anyy); const const isSideColumn: booleanisSideColumn = const dx: anydx < const PANEL_CAP: 5PANEL_CAP; const const isSideRow: booleanisSideRow = const dy: anydy < const PANEL_CAP: 5PANEL_CAP; if (const isSideColumn: booleanisSideColumn && const isSideRow: booleanisSideRow) { return function cornerColorAt(dx: any, dy: any): Color32cornerColorAt(const dx: anydx, const dy: anydy); } // Top and bottom edges run along x; left and right edges run along y. if (const isSideRow: booleanisSideRow) { return function edgeColorAt(depth: any, along: any): Color32edgeColorAt(const dy: anydy, x: anyx); } if (const isSideColumn: booleanisSideColumn) { return function edgeColorAt(depth: any, along: any): Color32edgeColorAt(const dx: anydx, y: anyy); } // Center: a diagonal dot pattern, so a stretched center shows fat blocks and a tiled // one stays fine. return (x: anyx + y: anyy) % 4 === 0 ?
const ART_COLORS: {
    outline: Color32;
    edgeLight: Color32;
    edgeMid: Color32;
    dash: Color32;
    fill: Color32;
    fillDot: Color32;
    stud: Color32;
}
ART_COLORS
.fillDot: Color32fillDot :
const ART_COLORS: {
    outline: Color32;
    edgeLight: Color32;
    edgeMid: Color32;
    dash: Color32;
    fill: Color32;
    fillDot: Color32;
    stud: Color32;
}
ART_COLORS
.fill: Color32fill;
} // Picks the color of one pixel of the bar. `lx` and `y` are measured inside the bar's own // 18x10 box. function function barColorAt(lx: any, y: any): Color32barColorAt(lx: anylx, y: anyy) { const const dx: anydx = Math.min(lx: anylx, const BAR_W: 18BAR_W - 1 - lx: anylx); const const dy: anydy = Math.min(y: anyy, const BAR_H: 10BAR_H - 1 - y: anyy); // The two end caps: outline around a mid-blue body, with a gold stud. if (const dx: anydx < const PANEL_CAP: 5PANEL_CAP) { if (const dx: anydx === 0 || const dy: anydy === 0) { return
const ART_COLORS: {
    outline: Color32;
    edgeLight: Color32;
    edgeMid: Color32;
    dash: Color32;
    fill: Color32;
    fillDot: Color32;
    stud: Color32;
}
ART_COLORS
.outline: Color32outline;
} return const dx: anydx === 2 && y: anyy >= 4 && y: anyy <= 5 ?
const ART_COLORS: {
    outline: Color32;
    edgeLight: Color32;
    edgeMid: Color32;
    dash: Color32;
    fill: Color32;
    fillDot: Color32;
    stud: Color32;
}
ART_COLORS
.stud: Color32stud :
const ART_COLORS: {
    outline: Color32;
    edgeLight: Color32;
    edgeMid: Color32;
    dash: Color32;
    fill: Color32;
    fillDot: Color32;
    stud: Color32;
}
ART_COLORS
.edgeMid: Color32edgeMid;
} // The middle: outline on top and bottom, a light line under it, then diagonal stripes // that repeat every 4 pixels along the bar. if (const dy: anydy === 0) { return
const ART_COLORS: {
    outline: Color32;
    edgeLight: Color32;
    edgeMid: Color32;
    dash: Color32;
    fill: Color32;
    fillDot: Color32;
    stud: Color32;
}
ART_COLORS
.outline: Color32outline;
} if (const dy: anydy === 1) { return
const ART_COLORS: {
    outline: Color32;
    edgeLight: Color32;
    edgeMid: Color32;
    dash: Color32;
    fill: Color32;
    fillDot: Color32;
    stud: Color32;
}
ART_COLORS
.edgeLight: Color32edgeLight;
} return (lx: anylx - const PANEL_CAP: 5PANEL_CAP + y: anyy) % 4 < 2 ?
const ART_COLORS: {
    outline: Color32;
    edgeLight: Color32;
    edgeMid: Color32;
    dash: Color32;
    fill: Color32;
    fillDot: Color32;
    stud: Color32;
}
ART_COLORS
.dash: Color32dash :
const ART_COLORS: {
    outline: Color32;
    edgeLight: Color32;
    edgeMid: Color32;
    dash: Color32;
    fill: Color32;
    fillDot: Color32;
    stud: Color32;
}
ART_COLORS
.edgeMid: Color32edgeMid;
} // Paints the whole sheet - the panel on the left and the bar on the right - on an // offscreen canvas. Returns the canvas and its 2D context. function
function buildArtSheet(): {
    canvas: any;
    ctx: any;
}
buildArtSheet
() {
const const canvas: anycanvas = new OffscreenCanvas(const SHEET_W: numberSHEET_W, const SHEET_H: 18SHEET_H); const const ctx: anyctx = const canvas: anycanvas.getContext('2d'); if (!const ctx: anyctx) { throw new Error('Could not create 2D context for the panel sheet'); } // Start fully transparent, so the gap between the panel and the bar stays empty. const ctx: anyctx.clearRect(0, 0, const SHEET_W: numberSHEET_W, const SHEET_H: 18SHEET_H); for (let let y: numbery = 0; let y: numbery < const PANEL_SIZE: 18PANEL_SIZE; let y: numbery++) { for (let let x: numberx = 0; let x: numberx < const PANEL_SIZE: 18PANEL_SIZE; let x: numberx++) { function plot(ctx: any, x: any, y: any, color: any): voidplot(const ctx: anyctx, let x: numberx, let y: numbery, function panelColorAt(x: any, y: any): Color32panelColorAt(let x: numberx, let y: numbery)); } } for (let let y: numbery = 0; let y: numbery < const BAR_H: 10BAR_H; let y: numbery++) { for (let let x: numberx = 0; let x: numberx < const BAR_W: 18BAR_W; let x: numberx++) { function plot(ctx: any, x: any, y: any, color: any): voidplot(const ctx: anyctx, const BAR_X: 20BAR_X + let x: numberx, let y: numbery, function barColorAt(lx: any, y: any): Color32barColorAt(let x: numberx, let y: numbery)); } } return { canvas: anycanvas, ctx: anyctx }; } // Demonstrates nine-slice panels: one asset, stretch vs tile, corner crop, recolor, and a // three-slice bar. class class DemoDemo { /** @type {Palette | null} */ Demo.palette: Palette | null
@type{Palette | null}
palette
= null;
/** @type {SpriteSheet | null} */ Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
= null;
// Slot map for the shared UI kit theme, filled in init() by applyTheme(). Demo.theme: nulltheme = null; // How many colors the art uses. Each recolored block is this many slots further // along, so this number is also the palette offset step. Demo.colorCount: numbercolorCount = 0; // The sheet loaded from public/sprites/panel.png in init(). Its colors sit in their // own palette slots. Demo.fileSheet: nullfileSheet = null; // The nine-slices. All are built once in init(). They are frozen, finished assets. Demo.stretchPanel: nullstretchPanel = null; Demo.tilePanel: nulltilePanel = null; Demo.mixedPanel: nullmixedPanel = null; Demo.bar: nullbar = null; Demo.filePanel: nullfilePanel = null; // Every box this demo draws more than once is built in init() too, never inside // render(). Demo.sizeRects: {}sizeRects = []; Demo.modeRects: {}modeRects = []; Demo.recolorRects: {}recolorRects = []; Demo.staticBarRects: {}staticBarRects = []; // The boxes whose size changes every frame. render() rewrites them with set(), so no // new Rect2i is made per frame. Demo.tweenRect: nulltweenRect = null; Demo.liveBarRect: nullliveBarRect = null; Demo.fileRect: nullfileRect = null; // Text that never changes, built once so render() does not glue strings together // every frame. Demo.sizeLabels: {}sizeLabels = []; Demo.recolorLabels: {}recolorLabels = []; // The tick the open animation started on. The Replay button sets it to "now". Demo.tweenStart: numbertweenStart = 0;
Demo.configure(): {
    displaySize: Vector2i;
}
configure
() {
return { displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const DISPLAY_W: 640DISPLAY_W, const DISPLAY_H: 400DISPLAY_H),
}; } // Builds the art, the palette blocks, every NineSlice, and every reused Rect2i. async Demo.init(): unknowninit() { this.Demo.palette: Palette | null
@type{Palette | null}
palette
=
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.paletteCreate: (size?: number) => Palette
Creates a standalone palette instance.
@since1.0.3@paramsize - Palette size. Defaults to 256 colors.@returnsNew mutable palette.
paletteCreate
(256);
// Install the shared UI colors into slots 240-251, far above the art at slots // 10-30. this.Demo.theme: nulltheme = import applyThemeapplyTheme(this.Demo.palette: Palette
@type{Palette | null}
palette
);
try { const { const canvas: anycanvas, const ctx: anyctx } =
function buildArtSheet(): {
    canvas: any;
    ctx: any;
}
buildArtSheet
();
// Give each unique art color its own palette slot, starting at COLOR_BASE. const const baseColors: anybaseColors = import registerCanvasColorsregisterCanvasColors(this.Demo.palette: Palette
@type{Palette | null}
palette
, const ctx: anyctx, const SHEET_W: numberSHEET_W, const SHEET_H: 18SHEET_H, const COLOR_BASE: 10COLOR_BASE);
this.Demo.colorCount: numbercolorCount = const baseColors: anybaseColors.length; // Hover block: every color a little lighter, like a button lit up under the // mouse. this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.fillBlock(start: number, source: readonly Color32[], transform: (color: Color32, index: number) => Color32): number
Writes a transformed block of colors into contiguous palette slots. Writes `transform(source[i], i)` into slot `start + i` for every `i` in `[0, source.length)`, delegating each write to {@link set } so it inherits {@link set } 's validation, including the rule that slot 0 must stay transparent. Collapses the common pattern of looping `palette.set(start + i, transform(baseColors[i]))` into one call.
@since1.7.0@paramstart - First palette index to write.@paramsource - Source colors to read from, in order. `source[i]` maps to slot `start + i`.@paramtransform - Called once per source color as `transform(color, i)`; its return value is written to slot `start + i`.@returnsThe next free slot after the written block (`start + source.length`), for chaining further writes.@throwsError if `start` is not a non-negative integer.@throwsError if the block would exceed the palette size.@throwsError if an individual slot write is invalid - see {@link set}.
fillBlock
(
const COLOR_BASE: 10COLOR_BASE + this.Demo.colorCount: numbercolorCount, const baseColors: anybaseColors, (c: Color32c) => new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(Math.min(255, c: Color32c.Color32.r: number
Red channel (0-255).
r
+ 50), Math.min(255, c: Color32c.Color32.g: number
Green channel (0-255).
g
+ 50), Math.min(255, c: Color32c.Color32.b: number
Blue channel (0-255).
b
+ 50)),
); // Alert block: push red up and pull green and blue down. this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.fillBlock(start: number, source: readonly Color32[], transform: (color: Color32, index: number) => Color32): number
Writes a transformed block of colors into contiguous palette slots. Writes `transform(source[i], i)` into slot `start + i` for every `i` in `[0, source.length)`, delegating each write to {@link set } so it inherits {@link set } 's validation, including the rule that slot 0 must stay transparent. Collapses the common pattern of looping `palette.set(start + i, transform(baseColors[i]))` into one call.
@since1.7.0@paramstart - First palette index to write.@paramsource - Source colors to read from, in order. `source[i]` maps to slot `start + i`.@paramtransform - Called once per source color as `transform(color, i)`; its return value is written to slot `start + i`.@returnsThe next free slot after the written block (`start + source.length`), for chaining further writes.@throwsError if `start` is not a non-negative integer.@throwsError if the block would exceed the palette size.@throwsError if an individual slot write is invalid - see {@link set}.
fillBlock
(
const COLOR_BASE: 10COLOR_BASE + this.Demo.colorCount: numbercolorCount * 2, const baseColors: anybaseColors, (c: Color32c) => new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(Math.min(255, c: Color32c.Color32.r: number
Red channel (0-255).
r
+ 110), Math.floor(c: Color32c.Color32.g: number
Green channel (0-255).
g
* 0.5), Math.floor(c: Color32c.Color32.b: number
Blue channel (0-255).
b
* 0.4)),
); const const image: anyimage = await import canvasToImagecanvasToImage(const canvas: anycanvas); this.Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
= new new SpriteSheet(image: HTMLImageElement | null, size?: Vector2i): SpriteSheet
Creates a sprite sheet from a loaded image. Use the static load() method for easier loading from URL.
@paramimage - Pre-loaded HTMLImageElement, or null for raw indexed data sheets.@paramsize - Explicit dimensions (required when image is null).
SpriteSheet
(const image: anyimage);
this.Demo.sheet: SpriteSheet
@type{SpriteSheet | null}
sheet
.SpriteSheet.indexize(palette: Palette): void
Converts the sprite sheet's RGBA pixels to palette indices. Each non-transparent pixel is looked up in the provided palette via exact color matching. Index 0 is always transparent. The resulting indices are stored internally; an `r8uint` GPU texture is created lazily on the next `getTexture()` call. The original RGBA data is retained so `reindexize()` can re-convert after a palette swap without reloading the image.
@parampalette - Active palette used for color-to-index mapping.@throwsIf any opaque pixel's color is not present in the palette.
indexize
(this.Demo.palette: Palette
@type{Palette | null}
palette
);
// Load the PNG file. loadIndexed() reads the file, adds its colors to the // palette starting at the given slot, and maps every pixel to those slots. // The three blocks above take up COLOR_BASE through COLOR_BASE + colorCount * // 3 - 1, so the file starts right after them. const const fileArt: Promise<IndexedSpriteLoadResult>fileArt = await class SpriteSheet
Sprite-sheet wrapper around a loaded image asset. The class keeps the original image available for CPU-side inspection while lazily creating and caching a GPU texture for rendering. When possible, `load()` also pre-decodes the source into an `ImageBitmap` so texture uploads preserve pixel-art alpha and color values more reliably. After calling `indexize()`, the sheet stores palette indices rather than RGBA data. The GPU texture becomes an `r8uint` format uploaded via `writeTexture`. The original RGBA bytes are retained so `reindexize()` can re-convert without reloading the image.
@since0.1.0
SpriteSheet
.
SpriteSheet.loadIndexed(url: string, palette: Palette, startSlot: number, options?: {
    sort?: "luminance" | "none";
    tileSize?: number | Vector2i;
}): Promise<IndexedSpriteLoadResult>
Convenience one-call path for palette-indexed sprite setup. This combines: 1) {@link SpriteSheet.loadColorsIntoPalette } 2) {@link SpriteSheet.load } 3) {@link SpriteSheet.indexize } It returns the indexized sheet plus a full-frame source rectangle and the colors that were written into the palette. Callers still control when to activate the palette via `BT.paletteSet(palette)`.
@paramurl - Path or URL to the PNG file.@parampalette - Target palette used for both registration and indexization.@paramstartSlot - First palette slot to write discovered colors into.@changed1.8.0 Added `options.tileSize`.@paramoptions - Optional color-sort behavior for registration and tile grid size.@paramoptions.sort - Color ordering for palette registration.@paramoptions.tileSize - Tile grid cell size (see {@link SpriteSheet.tileSize}).@returnsObject with `sheet`, `srcRect`, and registered `colors`.@throwsRangeError if `options.tileSize` is not a positive integer on both axes.
loadIndexed
(
const FILE_PANEL_URL: "/sprites/panel.png"FILE_PANEL_URL, this.Demo.palette: Palette
@type{Palette | null}
palette
,
const COLOR_BASE: 10COLOR_BASE + this.Demo.colorCount: numbercolorCount * 3, { sort?: "luminance" | "none" | undefinedsort: 'none', }, ); this.Demo.fileSheet: nullfileSheet = const fileArt: Promise<IndexedSpriteLoadResult>fileArt.sheet; this.Demo.buildNineSlices(): voidbuildNineSlices(); this.Demo.buildRects(): voidbuildRects();
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.paletteSet: (palette: Palette) => void
Stores the active engine palette. Use this to swap the **entire palette** (e.g. switch between a day and night theme). After this call the renderer uploads the new palette uniform on the next frame. **Palette-value swap (change what a slot looks like):** mutate the live {@link BT.palette } in place with `palette.set(slot, newColor)`. The renderer uploads dirty slots on the next frame; no `paletteSet()` or {@link BT.spritesRefresh } needed. **Palette-layout swap (same colors, different slot positions):** build a new palette with the same colors at new indices, call `paletteSet()`, then call {@link BT.spritesRefresh } so every sprite sheet re-maps its original RGBA pixels against the new slot layout.
@since1.0.3@parampalette - Palette to make active.
paletteSet
(this.Demo.palette: Palette
@type{Palette | null}
palette
);
} catch (function (local var) error: unknownerror) { console.error('[NineSliceDemo] Failed to build the panel art:', function (local var) error: unknownerror); return false; } return true; } Demo.update(): voidupdate() { // Always first: this latches the Replay button's R key shortcut. import uiui.tick(); } Demo.render(): voidrender() {
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.clear: (paletteIndex: number) => void
Sets the frame clear color using a palette index. The renderer uses this color when clearing the full display at the start of the next frame.
@since0.1.0@parampaletteIndex - Palette index for the full-screen clear pass.
clear
(this.Demo.theme: nulltheme.bg);
this.Demo.renderSizes(): voidrenderSizes(); this.Demo.renderModes(): voidrenderModes(); this.Demo.renderTween(): voidrenderTween(); this.Demo.renderRecolor(): voidrenderRecolor(); this.Demo.renderBars(): voidrenderBars(); this.Demo.renderFilePanel(): voidrenderFilePanel(); this.Demo.renderCodeSnippet(): voidrenderCodeSnippet(); } // Cuts the sheet into nine-slices. fromSheet() takes two rectangles in sheet pixels: // `outer` is the whole picture and `inner` is the center. Everything between them is // the border - corners and edges. Demo.buildNineSlices(): voidbuildNineSlices() { const const outer: Rect2iouter = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(0, 0, const PANEL_SIZE: 18PANEL_SIZE, const PANEL_SIZE: 18PANEL_SIZE);
const const inner: Rect2iinner = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(const PANEL_CAP: 5PANEL_CAP, const PANEL_CAP: 5PANEL_CAP, const PANEL_INNER: numberPANEL_INNER, const PANEL_INNER: numberPANEL_INNER);
// The default for edges and center is 'stretch'. this.Demo.stretchPanel: nullstretchPanel = class NineSlice
A sprite region split into 9 parts for scalable UI panels: 4 corners drawn at 1:1, 4 edges and a center that stretch or tile. Draw it with `BT.drawNineSlice`. Immutable: build it once in `init()` and draw it every frame.
@since1.8.0
NineSlice
.NineSlice.fromSheet(sheet: SpriteSheet, outer: Rect2i, inner: Rect2i, options?: NineSliceOptions): NineSlice
Builds a nine-slice from a sheet region. Both rects are in sheet coordinates; `inner` marks the center, and the space between `inner` and `outer` gives the corners and edges. `inner` may touch `outer`, so a zero-size side makes a three-slice bar.
@since1.8.0@paramsheet - Sheet holding the panel art. It may be indexized later; drawing checks that.@paramouter - Whole panel region, inside the sheet.@paraminner - Center region, inside `outer`, at least 1x1.@paramoptions - Edge and center fill modes, both `'stretch'` by default.@returnsA frozen nine-slice holding copies of both rects.@throwsIf `sheet` is not a `SpriteSheet`, a rect is not a `Rect2i` of integers, `outer` leaves the sheet, `inner` leaves `outer` or is smaller than 1x1, `options` is not an object, or a mode is not `'stretch'` or `'tile'`.@exampleconst panel = NineSlice.fromSheet(sheet, new Rect2i(0, 0, 16, 16), new Rect2i(4, 4, 8, 8), { edges: 'tile' }); BT.drawNineSlice(panel, new Rect2i(20, 20, 120, 64));
fromSheet
(this.Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
, const outer: Rect2iouter, const inner: Rect2iinner);
// 'tile' repeats the strip at its real size, starting from the box's top-left // corner. this.Demo.tilePanel: nulltilePanel = class NineSlice
A sprite region split into 9 parts for scalable UI panels: 4 corners drawn at 1:1, 4 edges and a center that stretch or tile. Draw it with `BT.drawNineSlice`. Immutable: build it once in `init()` and draw it every frame.
@since1.8.0
NineSlice
.NineSlice.fromSheet(sheet: SpriteSheet, outer: Rect2i, inner: Rect2i, options?: NineSliceOptions): NineSlice
Builds a nine-slice from a sheet region. Both rects are in sheet coordinates; `inner` marks the center, and the space between `inner` and `outer` gives the corners and edges. `inner` may touch `outer`, so a zero-size side makes a three-slice bar.
@since1.8.0@paramsheet - Sheet holding the panel art. It may be indexized later; drawing checks that.@paramouter - Whole panel region, inside the sheet.@paraminner - Center region, inside `outer`, at least 1x1.@paramoptions - Edge and center fill modes, both `'stretch'` by default.@returnsA frozen nine-slice holding copies of both rects.@throwsIf `sheet` is not a `SpriteSheet`, a rect is not a `Rect2i` of integers, `outer` leaves the sheet, `inner` leaves `outer` or is smaller than 1x1, `options` is not an object, or a mode is not `'stretch'` or `'tile'`.@exampleconst panel = NineSlice.fromSheet(sheet, new Rect2i(0, 0, 16, 16), new Rect2i(4, 4, 8, 8), { edges: 'tile' }); BT.drawNineSlice(panel, new Rect2i(20, 20, 120, 64));
fromSheet
(this.Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
, const outer: Rect2iouter, const inner: Rect2iinner, { NineSliceOptions.edges?: NineSliceMode | undefined
How the four edges fill their length.
edges
: 'tile', NineSliceOptions.center?: NineSliceMode | undefined
How the center fills its box.
center
: 'tile' });
// The common UI case: a patterned border that tiles around a center that // stretches. this.Demo.mixedPanel: nullmixedPanel = class NineSlice
A sprite region split into 9 parts for scalable UI panels: 4 corners drawn at 1:1, 4 edges and a center that stretch or tile. Draw it with `BT.drawNineSlice`. Immutable: build it once in `init()` and draw it every frame.
@since1.8.0
NineSlice
.NineSlice.fromSheet(sheet: SpriteSheet, outer: Rect2i, inner: Rect2i, options?: NineSliceOptions): NineSlice
Builds a nine-slice from a sheet region. Both rects are in sheet coordinates; `inner` marks the center, and the space between `inner` and `outer` gives the corners and edges. `inner` may touch `outer`, so a zero-size side makes a three-slice bar.
@since1.8.0@paramsheet - Sheet holding the panel art. It may be indexized later; drawing checks that.@paramouter - Whole panel region, inside the sheet.@paraminner - Center region, inside `outer`, at least 1x1.@paramoptions - Edge and center fill modes, both `'stretch'` by default.@returnsA frozen nine-slice holding copies of both rects.@throwsIf `sheet` is not a `SpriteSheet`, a rect is not a `Rect2i` of integers, `outer` leaves the sheet, `inner` leaves `outer` or is smaller than 1x1, `options` is not an object, or a mode is not `'stretch'` or `'tile'`.@exampleconst panel = NineSlice.fromSheet(sheet, new Rect2i(0, 0, 16, 16), new Rect2i(4, 4, 8, 8), { edges: 'tile' }); BT.drawNineSlice(panel, new Rect2i(20, 20, 120, 64));
fromSheet
(this.Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
, const outer: Rect2iouter, const inner: Rect2iinner, { NineSliceOptions.edges?: NineSliceMode | undefined
How the four edges fill their length.
edges
: 'tile', NineSliceOptions.center?: NineSliceMode | undefined
How the center fills its box.
center
: 'stretch' });
// A three-slice bar: `inner` touches the top AND bottom of `outer`, so there are // no top or bottom edges - only a left cap, a middle, and a right cap. The middle // tiles along the bar. const const barOuter: Rect2ibarOuter = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(const BAR_X: 20BAR_X, 0, const BAR_W: 18BAR_W, const BAR_H: 10BAR_H);
const const barInner: Rect2ibarInner = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(const BAR_X: 20BAR_X + const PANEL_CAP: 5PANEL_CAP, 0, const BAR_W: 18BAR_W - const PANEL_CAP: 5PANEL_CAP * 2, const BAR_H: 10BAR_H);
this.Demo.bar: nullbar = class NineSlice
A sprite region split into 9 parts for scalable UI panels: 4 corners drawn at 1:1, 4 edges and a center that stretch or tile. Draw it with `BT.drawNineSlice`. Immutable: build it once in `init()` and draw it every frame.
@since1.8.0
NineSlice
.NineSlice.fromSheet(sheet: SpriteSheet, outer: Rect2i, inner: Rect2i, options?: NineSliceOptions): NineSlice
Builds a nine-slice from a sheet region. Both rects are in sheet coordinates; `inner` marks the center, and the space between `inner` and `outer` gives the corners and edges. `inner` may touch `outer`, so a zero-size side makes a three-slice bar.
@since1.8.0@paramsheet - Sheet holding the panel art. It may be indexized later; drawing checks that.@paramouter - Whole panel region, inside the sheet.@paraminner - Center region, inside `outer`, at least 1x1.@paramoptions - Edge and center fill modes, both `'stretch'` by default.@returnsA frozen nine-slice holding copies of both rects.@throwsIf `sheet` is not a `SpriteSheet`, a rect is not a `Rect2i` of integers, `outer` leaves the sheet, `inner` leaves `outer` or is smaller than 1x1, `options` is not an object, or a mode is not `'stretch'` or `'tile'`.@exampleconst panel = NineSlice.fromSheet(sheet, new Rect2i(0, 0, 16, 16), new Rect2i(4, 4, 8, 8), { edges: 'tile' }); BT.drawNineSlice(panel, new Rect2i(20, 20, 120, 64));
fromSheet
(this.Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
, const barOuter: Rect2ibarOuter, const barInner: Rect2ibarInner, { NineSliceOptions.center?: NineSliceMode | undefined
How the center fills its box.
center
: 'tile' });
// The file panel: the whole 14x14 picture, with the 2x2 center inside 6-pixel // corners. The defaults stretch the edges and the center, which suits this flat // rim. const const fileOuter: Rect2ifileOuter = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(0, 0, const FILE_PANEL_SIZE: 14FILE_PANEL_SIZE, const FILE_PANEL_SIZE: 14FILE_PANEL_SIZE);
const const fileInner: Rect2ifileInner = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(
const FILE_PANEL_CAP: 6FILE_PANEL_CAP, const FILE_PANEL_CAP: 6FILE_PANEL_CAP, const FILE_PANEL_SIZE: 14FILE_PANEL_SIZE - const FILE_PANEL_CAP: 6FILE_PANEL_CAP * 2, const FILE_PANEL_SIZE: 14FILE_PANEL_SIZE - const FILE_PANEL_CAP: 6FILE_PANEL_CAP * 2, ); this.Demo.filePanel: nullfilePanel = class NineSlice
A sprite region split into 9 parts for scalable UI panels: 4 corners drawn at 1:1, 4 edges and a center that stretch or tile. Draw it with `BT.drawNineSlice`. Immutable: build it once in `init()` and draw it every frame.
@since1.8.0
NineSlice
.NineSlice.fromSheet(sheet: SpriteSheet, outer: Rect2i, inner: Rect2i, options?: NineSliceOptions): NineSlice
Builds a nine-slice from a sheet region. Both rects are in sheet coordinates; `inner` marks the center, and the space between `inner` and `outer` gives the corners and edges. `inner` may touch `outer`, so a zero-size side makes a three-slice bar.
@since1.8.0@paramsheet - Sheet holding the panel art. It may be indexized later; drawing checks that.@paramouter - Whole panel region, inside the sheet.@paraminner - Center region, inside `outer`, at least 1x1.@paramoptions - Edge and center fill modes, both `'stretch'` by default.@returnsA frozen nine-slice holding copies of both rects.@throwsIf `sheet` is not a `SpriteSheet`, a rect is not a `Rect2i` of integers, `outer` leaves the sheet, `inner` leaves `outer` or is smaller than 1x1, `options` is not an object, or a mode is not `'stretch'` or `'tile'`.@exampleconst panel = NineSlice.fromSheet(sheet, new Rect2i(0, 0, 16, 16), new Rect2i(4, 4, 8, 8), { edges: 'tile' }); BT.drawNineSlice(panel, new Rect2i(20, 20, 120, 64));
fromSheet
(this.Demo.fileSheet: nullfileSheet, const fileOuter: Rect2ifileOuter, const fileInner: Rect2ifileInner);
} // Makes every destination box once. A Rect2i is just four numbers (x, y, width, // height). Demo.buildRects(): voidbuildRects() { // Section 1: five boxes in a row, 12 pixels apart. let let x: numberx = 12; for (const const size: anysize of const SIZES: {}SIZES) { this.Demo.sizeRects: {}sizeRects.push(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(let x: numberx, 24, const size: anysize.w, const size: anysize.h));
this.Demo.sizeLabels: {}sizeLabels.push(`${const size: anysize.w}x${const size: anysize.h}`); let x: numberx += const size: anysize.w + 12; } // Section 2: three boxes of the same size, one per mode. for (let let i: numberi = 0; let i: numberi < const MODE_NAMES: {}MODE_NAMES.length; let i: numberi++) { this.Demo.modeRects: {}modeRects.push(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(12 + let i: numberi * 104, 116, 92, 64));
} // Section 3: this box is rewritten every frame, so it only needs to exist. this.Demo.tweenRect: nulltweenRect = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(0, 0, 0, 0);
// Section 4: three same-size panels, one per look. for (let let i: numberi = 0; let i: numberi < const RECOLOR_NAMES: {}RECOLOR_NAMES.length; let i: numberi++) { this.Demo.recolorRects: {}recolorRects.push(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(const RIGHT_X: 340RIGHT_X + let i: numberi * 88, 160, 72, 36));
this.Demo.recolorLabels: {}recolorLabels.push(`${const RECOLOR_NAMES: {}RECOLOR_NAMES[let i: numberi]} +${this.Demo.colorCount: numbercolorCount * let i: numberi}`); } // Section 5: two fixed bars and one whose width changes, all with the art's own // height. this.Demo.staticBarRects: {}staticBarRects.push(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(const RIGHT_X: 340RIGHT_X, 284, 48, const BAR_H: 10BAR_H), new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(const RIGHT_X: 340RIGHT_X, 300, 96, const BAR_H: 10BAR_H));
this.Demo.liveBarRect: nullliveBarRect = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(const RIGHT_X: 340RIGHT_X, 316, 20, const BAR_H: 10BAR_H);
// Section 6: the file panel's box. It starts at its smallest size and render() // resizes it every frame. this.Demo.fileRect: nullfileRect = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(const FILE_BOX_X: 12FILE_BOX_X, const FILE_BOX_Y: 236FILE_BOX_Y, const FILE_MIN_W: 40FILE_MIN_W, const FILE_MIN_H: 24FILE_MIN_H);
} // Section 1: the same panel, five different boxes. Corners stay the same size; only // the middle grows. Demo.renderSizes(): voidrenderSizes() { import uiui.caption(12, 8, 'One asset, any box', { color: stringcolor: 'dim' }); for (let let i: numberi = 0; let i: numberi < this.Demo.sizeRects: {}sizeRects.length; let i: numberi++) {
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
.drawNineSlice: (nineSlice: NineSlice, destRect: Rect2i, paletteOffset?: number) => void
Draws a nine-slice panel - a sprite region split into 4 corners, 4 edges, and a center - into any box. Corners stay 1:1; edges and center stretch or tile as set on the {@link NineSlice } . A box smaller than the corners crops them from their outer side, so a panel can animate open from size 0. `paletteOffset` works exactly as in {@link BT.drawSprite } ; there is no tint. Allocation-free: build the `NineSlice` once in `init()` and reuse it. Stretched parts follow the sprite stretch rule, and both backends match pixel for pixel. Tiled parts draw one quad per tile, so tiling a 1-pixel strip across a large box costs many quads - stretch flat fills instead.
@since1.8.0@paramnineSlice - Panel from `NineSlice.fromSheet(sheet, outer, inner, options?)`.@paramdestRect - Box to fill; an empty or negative size draws nothing.@parampaletteOffset - Palette index offset applied at draw time (default 0).@example// init() this.panel = NineSlice.fromSheet(sheet, new Rect2i(0, 0, 16, 16), new Rect2i(4, 4, 8, 8), { edges: 'tile' }); // render() BT.drawNineSlice(this.panel, new Rect2i(20, 20, 120, 64));
drawNineSlice
(this.Demo.mixedPanel: nullmixedPanel, this.Demo.sizeRects: {}sizeRects[let i: numberi]);
import uiui.caption(this.Demo.sizeRects: {}sizeRects[let i: numberi].x, 80, this.Demo.sizeLabels: {}sizeLabels[let i: numberi], { color: stringcolor: 'dim' }); } } // Section 2: three boxes of exactly the same size, filled three ways. Stretch blows // each strip up to fit, so the dashes turn into fat bars. Tile repeats the strip at // its real size, so the dashes stay crisp - and the last tile is cut off at the box // edge. Mixed tiles the border and stretches the center. Demo.renderModes(): voidrenderModes() { import uiui.caption(12, 100, 'Stretch vs tile vs mixed', { color: stringcolor: 'dim' }); const const panels: {}panels = [this.Demo.stretchPanel: nullstretchPanel, this.Demo.tilePanel: nulltilePanel, this.Demo.mixedPanel: nullmixedPanel]; for (let let i: numberi = 0; let i: numberi < const panels: {}panels.length; let i: numberi++) {
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
.drawNineSlice: (nineSlice: NineSlice, destRect: Rect2i, paletteOffset?: number) => void
Draws a nine-slice panel - a sprite region split into 4 corners, 4 edges, and a center - into any box. Corners stay 1:1; edges and center stretch or tile as set on the {@link NineSlice } . A box smaller than the corners crops them from their outer side, so a panel can animate open from size 0. `paletteOffset` works exactly as in {@link BT.drawSprite } ; there is no tint. Allocation-free: build the `NineSlice` once in `init()` and reuse it. Stretched parts follow the sprite stretch rule, and both backends match pixel for pixel. Tiled parts draw one quad per tile, so tiling a 1-pixel strip across a large box costs many quads - stretch flat fills instead.
@since1.8.0@paramnineSlice - Panel from `NineSlice.fromSheet(sheet, outer, inner, options?)`.@paramdestRect - Box to fill; an empty or negative size draws nothing.@parampaletteOffset - Palette index offset applied at draw time (default 0).@example// init() this.panel = NineSlice.fromSheet(sheet, new Rect2i(0, 0, 16, 16), new Rect2i(4, 4, 8, 8), { edges: 'tile' }); // render() BT.drawNineSlice(this.panel, new Rect2i(20, 20, 120, 64));
drawNineSlice
(const panels: {}panels[let i: numberi], this.Demo.modeRects: {}modeRects[let i: numberi]);
import uiui.caption(this.Demo.modeRects: {}modeRects[let i: numberi].x, 184, const MODE_NAMES: {}MODE_NAMES[let i: numberi], { color: stringcolor: 'dim' }); } import uiui.caption(12, 198, 'Mixed: tiled edges, stretched center', { color: stringcolor: 'dim' }); } // Section 3: a panel growing from nothing. While the box is smaller than the two // corners together, the corners share it and are cropped from their outer side - so // you see a growing outline, not a squashed frame. Demo.renderTween(): voidrenderTween() { import uiui.caption(const RIGHT_X: 340RIGHT_X, 8, 'Open from 0x0', { color: stringcolor: 'dim' }); // How far into the animation are we? Count ticks since the start, and wrap around // so it loops. const const elapsed: numberelapsed = (
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
- this.Demo.tweenStart: numbertweenStart) % const TWEEN_CYCLE_TICKS: 180TWEEN_CYCLE_TICKS;
// Turn that into a number from 0 (closed) to 1 (fully open), then ease it so it // slows down as it arrives, like a drawer closing softly. const const progress: numberprogress = function applyEasing(t: number, easing: EasingFunction): number
Applies an easing curve to a normalized time value.
@since1.0.3@changed1.5.0 Added sine, cubic, quartic, quintic, expo, circ, back, elastic, and bounce families.@paramt - Normalized time in [0, 1]. Values outside this range are not clamped.@parameasing - Easing curve to apply.@returnsEased value. Guaranteed to return 0 for `t = 0` and 1 for `t = 1`.
applyEasing
(Math.min(1, const elapsed: numberelapsed / const TWEEN_OPEN_TICKS: 60TWEEN_OPEN_TICKS), 'ease-out');
const const w: anyw = Math.round(const progress: numberprogress * const TWEEN_MAX_W: 140TWEEN_MAX_W); const const h: anyh = Math.round(const progress: numberprogress * const TWEEN_MAX_H: 90TWEEN_MAX_H); // Grow from the middle of the stage: shift the top-left back by half the missing // size. The `>> 1` is "divide by two and drop the fraction", which keeps every // coordinate a whole number. this.Demo.tweenRect: nulltweenRect.set(const RIGHT_X: 340RIGHT_X + ((const TWEEN_MAX_W: 140TWEEN_MAX_W - const w: anyw) >> 1), 24 + ((const TWEEN_MAX_H: 90TWEEN_MAX_H - const h: anyh) >> 1), const w: anyw, const h: anyh);
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
.drawNineSlice: (nineSlice: NineSlice, destRect: Rect2i, paletteOffset?: number) => void
Draws a nine-slice panel - a sprite region split into 4 corners, 4 edges, and a center - into any box. Corners stay 1:1; edges and center stretch or tile as set on the {@link NineSlice } . A box smaller than the corners crops them from their outer side, so a panel can animate open from size 0. `paletteOffset` works exactly as in {@link BT.drawSprite } ; there is no tint. Allocation-free: build the `NineSlice` once in `init()` and reuse it. Stretched parts follow the sprite stretch rule, and both backends match pixel for pixel. Tiled parts draw one quad per tile, so tiling a 1-pixel strip across a large box costs many quads - stretch flat fills instead.
@since1.8.0@paramnineSlice - Panel from `NineSlice.fromSheet(sheet, outer, inner, options?)`.@paramdestRect - Box to fill; an empty or negative size draws nothing.@parampaletteOffset - Palette index offset applied at draw time (default 0).@example// init() this.panel = NineSlice.fromSheet(sheet, new Rect2i(0, 0, 16, 16), new Rect2i(4, 4, 8, 8), { edges: 'tile' }); // render() BT.drawNineSlice(this.panel, new Rect2i(20, 20, 120, 64));
drawNineSlice
(this.Demo.mixedPanel: nullmixedPanel, this.Demo.tweenRect: nulltweenRect);
import uiui.caption(const RIGHT_X: 340RIGHT_X, 118, `${const w: anyw}x${const h: anyh}`, { color: stringcolor: 'dim' }); // Replay restarts the animation. Pressing R does the same. import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { x: numberx: const RIGHT_X: 340RIGHT_X + const TWEEN_MAX_W: 140TWEEN_MAX_W + 16, y: numbery: 24 }); if (import uiui.button('Replay (R)', { key: stringkey: 'KeyR' })) { this.Demo.tweenStart: numbertweenStart =
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
;
} import uiui.end(); } // Section 4: no tint exists, only paletteOffset - the same shift sprites use. Offset // 0 is the original art; adding colorCount jumps to the next recolored block. Demo.renderRecolor(): voidrenderRecolor() { import uiui.caption(const RIGHT_X: 340RIGHT_X, 140, 'Palette offsets - one panel, three looks', { color: stringcolor: 'dim' }); for (let let i: numberi = 0; let i: numberi < this.Demo.recolorRects: {}recolorRects.length; let i: numberi++) {
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
.drawNineSlice: (nineSlice: NineSlice, destRect: Rect2i, paletteOffset?: number) => void
Draws a nine-slice panel - a sprite region split into 4 corners, 4 edges, and a center - into any box. Corners stay 1:1; edges and center stretch or tile as set on the {@link NineSlice } . A box smaller than the corners crops them from their outer side, so a panel can animate open from size 0. `paletteOffset` works exactly as in {@link BT.drawSprite } ; there is no tint. Allocation-free: build the `NineSlice` once in `init()` and reuse it. Stretched parts follow the sprite stretch rule, and both backends match pixel for pixel. Tiled parts draw one quad per tile, so tiling a 1-pixel strip across a large box costs many quads - stretch flat fills instead.
@since1.8.0@paramnineSlice - Panel from `NineSlice.fromSheet(sheet, outer, inner, options?)`.@paramdestRect - Box to fill; an empty or negative size draws nothing.@parampaletteOffset - Palette index offset applied at draw time (default 0).@example// init() this.panel = NineSlice.fromSheet(sheet, new Rect2i(0, 0, 16, 16), new Rect2i(4, 4, 8, 8), { edges: 'tile' }); // render() BT.drawNineSlice(this.panel, new Rect2i(20, 20, 120, 64));
drawNineSlice
(this.Demo.mixedPanel: nullmixedPanel, this.Demo.recolorRects: {}recolorRects[let i: numberi], this.Demo.colorCount: numbercolorCount * let i: numberi);
import uiui.caption(this.Demo.recolorRects: {}recolorRects[let i: numberi].x, 200, this.Demo.recolorLabels: {}recolorLabels[let i: numberi], { color: stringcolor: 'dim' }); } } // Section 5: a three-slice bar. Its height is fixed by the art; only the width is // free, so a health bar is one draw call whose width follows the health. Demo.renderBars(): voidrenderBars() { import uiui.caption(const RIGHT_X: 340RIGHT_X, 268, 'Three-slice bar - width is free', { color: stringcolor: 'dim' }); for (const const rect: anyrect of this.Demo.staticBarRects: {}staticBarRects) {
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
.drawNineSlice: (nineSlice: NineSlice, destRect: Rect2i, paletteOffset?: number) => void
Draws a nine-slice panel - a sprite region split into 4 corners, 4 edges, and a center - into any box. Corners stay 1:1; edges and center stretch or tile as set on the {@link NineSlice } . A box smaller than the corners crops them from their outer side, so a panel can animate open from size 0. `paletteOffset` works exactly as in {@link BT.drawSprite } ; there is no tint. Allocation-free: build the `NineSlice` once in `init()` and reuse it. Stretched parts follow the sprite stretch rule, and both backends match pixel for pixel. Tiled parts draw one quad per tile, so tiling a 1-pixel strip across a large box costs many quads - stretch flat fills instead.
@since1.8.0@paramnineSlice - Panel from `NineSlice.fromSheet(sheet, outer, inner, options?)`.@paramdestRect - Box to fill; an empty or negative size draws nothing.@parampaletteOffset - Palette index offset applied at draw time (default 0).@example// init() this.panel = NineSlice.fromSheet(sheet, new Rect2i(0, 0, 16, 16), new Rect2i(4, 4, 8, 8), { edges: 'tile' }); // render() BT.drawNineSlice(this.panel, new Rect2i(20, 20, 120, 64));
drawNineSlice
(this.Demo.bar: nullbar, const rect: anyrect);
} // The live bar swings between 20 and 200 pixels wide. 20 is the smallest that // still shows both caps in full (each is 5 pixels wide). const const swing: numberswing = (Math.sin(
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
/ 40) + 1) / 2;
this.Demo.liveBarRect: nullliveBarRect.width = Math.round(20 + const swing: numberswing * 180);
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
.drawNineSlice: (nineSlice: NineSlice, destRect: Rect2i, paletteOffset?: number) => void
Draws a nine-slice panel - a sprite region split into 4 corners, 4 edges, and a center - into any box. Corners stay 1:1; edges and center stretch or tile as set on the {@link NineSlice } . A box smaller than the corners crops them from their outer side, so a panel can animate open from size 0. `paletteOffset` works exactly as in {@link BT.drawSprite } ; there is no tint. Allocation-free: build the `NineSlice` once in `init()` and reuse it. Stretched parts follow the sprite stretch rule, and both backends match pixel for pixel. Tiled parts draw one quad per tile, so tiling a 1-pixel strip across a large box costs many quads - stretch flat fills instead.
@since1.8.0@paramnineSlice - Panel from `NineSlice.fromSheet(sheet, outer, inner, options?)`.@paramdestRect - Box to fill; an empty or negative size draws nothing.@parampaletteOffset - Palette index offset applied at draw time (default 0).@example// init() this.panel = NineSlice.fromSheet(sheet, new Rect2i(0, 0, 16, 16), new Rect2i(4, 4, 8, 8), { edges: 'tile' }); // render() BT.drawNineSlice(this.panel, new Rect2i(20, 20, 120, 64));
drawNineSlice
(this.Demo.bar: nullbar, this.Demo.liveBarRect: nullliveBarRect);
} // Section 6: a panel whose art comes from a PNG file, not from code. The box grows // and shrinks slowly, and the rim and rounded corners keep their size at every box // size. Demo.renderFilePanel(): voidrenderFilePanel() { import uiui.caption(const FILE_BOX_X: 12FILE_BOX_X, 220, 'Panel from public/sprites/panel.png, scaling', { color: stringcolor: 'dim' }); // A sine wave swings between -1 and 1. Adding 1 and dividing by 2 turns it into a // number from 0 to 1. Dividing the tick count by a big number makes the wave // slow. const const swing: numberswing = (Math.sin(
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
/ const FILE_SWING_TICKS: 120FILE_SWING_TICKS) + 1) / 2;
// Move each size from its minimum toward its maximum, by the share of the swing // reached so far. const const w: anyw = Math.round(const FILE_MIN_W: 40FILE_MIN_W + const swing: numberswing * (const FILE_MAX_W: 180FILE_MAX_W - const FILE_MIN_W: 40FILE_MIN_W)); const const h: anyh = Math.round(const FILE_MIN_H: 24FILE_MIN_H + const swing: numberswing * (const FILE_MAX_H: 64FILE_MAX_H - const FILE_MIN_H: 24FILE_MIN_H)); this.Demo.fileRect: nullfileRect.set(const FILE_BOX_X: 12FILE_BOX_X, const FILE_BOX_Y: 236FILE_BOX_Y, const w: anyw, const h: anyh);
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
.drawNineSlice: (nineSlice: NineSlice, destRect: Rect2i, paletteOffset?: number) => void
Draws a nine-slice panel - a sprite region split into 4 corners, 4 edges, and a center - into any box. Corners stay 1:1; edges and center stretch or tile as set on the {@link NineSlice } . A box smaller than the corners crops them from their outer side, so a panel can animate open from size 0. `paletteOffset` works exactly as in {@link BT.drawSprite } ; there is no tint. Allocation-free: build the `NineSlice` once in `init()` and reuse it. Stretched parts follow the sprite stretch rule, and both backends match pixel for pixel. Tiled parts draw one quad per tile, so tiling a 1-pixel strip across a large box costs many quads - stretch flat fills instead.
@since1.8.0@paramnineSlice - Panel from `NineSlice.fromSheet(sheet, outer, inner, options?)`.@paramdestRect - Box to fill; an empty or negative size draws nothing.@parampaletteOffset - Palette index offset applied at draw time (default 0).@example// init() this.panel = NineSlice.fromSheet(sheet, new Rect2i(0, 0, 16, 16), new Rect2i(4, 4, 8, 8), { edges: 'tile' }); // render() BT.drawNineSlice(this.panel, new Rect2i(20, 20, 120, 64));
drawNineSlice
(this.Demo.filePanel: nullfilePanel, this.Demo.fileRect: nullfileRect);
import uiui.caption(const FILE_BOX_X: 12FILE_BOX_X, 306, `${const w: anyw}x${const h: anyh}`, { color: stringcolor: 'dim' }); } // The two lines that matter, as a bordered kit panel in the bottom-left corner. Demo.renderCodeSnippet(): voidrenderCodeSnippet() { import uiui.begin(import UI_ANCHORSUI_ANCHORS.BOTTOM_LEFT, { y: numbery: const DISPLAY_H: 400DISPLAY_H - 62 }); import uiui.panel('In init(), once:'); import uiui.label('NineSlice.fromSheet(sheet, outer, inner, { edges: "tile" })', { color: stringcolor: 'info' }); import uiui.label('In render(), any box: BT.drawNineSlice(panel, rect, offset)', { color: stringcolor: 'info' }); import uiui.end(); } } function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
(class DemoDemo);