/*
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): numberApplies an easing curve to a normalized time value.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.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 NineSliceA 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.NineSlice, 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, 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.SpriteSheet, 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 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): Color32Creates a clamped 8-bit RGBA color.Color32(0x14, 0x16, 0x3a),
edgeLight: Color32edgeLight: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(0x9a, 0xa8, 0xff),
edgeMid: Color32edgeMid: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(0x4a, 0x58, 0xc8),
dash: Color32dash: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(0xd8, 0xde, 0xff),
fill: Color32fill: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(0x24, 0x2c, 0x70),
fillDot: Color32fillDot: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(0x3a, 0x46, 0xa8),
stud: Color32stud: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.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 | nullpalette = null;
/** @type {SpriteSheet | null} */
Demo.sheet: SpriteSheet | nullsheet = 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): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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 | 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);
// 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: Palettepalette);
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: Palettepalette, 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: Palettepalette.Palette.fillBlock(start: number, source: readonly Color32[], transform: (color: Color32, index: number) => Color32): numberWrites 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.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): Color32Creates a clamped 8-bit RGBA color.Color32(Math.min(255, c: Color32c.Color32.r: numberRed channel (0-255).r + 50), Math.min(255, c: Color32c.Color32.g: numberGreen channel (0-255).g + 50), Math.min(255, c: Color32c.Color32.b: numberBlue channel (0-255).b + 50)),
);
// Alert block: push red up and pull green and blue down.
this.Demo.palette: Palettepalette.Palette.fillBlock(start: number, source: readonly Color32[], transform: (color: Color32, index: number) => Color32): numberWrites 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.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): Color32Creates a clamped 8-bit RGBA color.Color32(Math.min(255, c: Color32c.Color32.r: numberRed channel (0-255).r + 110), Math.floor(c: Color32c.Color32.g: numberGreen channel (0-255).g * 0.5), Math.floor(c: Color32c.Color32.b: numberBlue channel (0-255).b * 0.4)),
);
const const image: anyimage = await import canvasToImagecanvasToImage(const canvas: anycanvas);
this.Demo.sheet: SpriteSheet | nullsheet = new new SpriteSheet(image: HTMLImageElement | null, size?: Vector2i): SpriteSheetCreates a sprite sheet from a loaded image.
Use the static load() method for easier loading from URL.SpriteSheet(const image: anyimage);
this.Demo.sheet: SpriteSheetsheet.SpriteSheet.indexize(palette: Palette): voidConverts 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.indexize(this.Demo.palette: Palettepalette);
// 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 SpriteSheetSprite-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.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)`.loadIndexed(
const FILE_PANEL_URL: "/sprites/panel.png"FILE_PANEL_URL,
this.Demo.palette: Palettepalette,
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) => 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);
} 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) => voidSets the frame clear color using a palette index.
The renderer uses this color when clearing the full display at the start
of the next frame.clear(this.Demo.theme: nulltheme.bg);
this.Demo.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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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 NineSliceA 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.NineSlice.NineSlice.fromSheet(sheet: SpriteSheet, outer: Rect2i, inner: Rect2i, options?: NineSliceOptions): NineSliceBuilds 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.fromSheet(this.Demo.sheet: SpriteSheet | nullsheet, 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 NineSliceA 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.NineSlice.NineSlice.fromSheet(sheet: SpriteSheet, outer: Rect2i, inner: Rect2i, options?: NineSliceOptions): NineSliceBuilds 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.fromSheet(this.Demo.sheet: SpriteSheet | nullsheet, const outer: Rect2iouter, const inner: Rect2iinner, { NineSliceOptions.edges?: NineSliceMode | undefinedHow the four edges fill their length.edges: 'tile', NineSliceOptions.center?: NineSliceMode | undefinedHow 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 NineSliceA 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.NineSlice.NineSlice.fromSheet(sheet: SpriteSheet, outer: Rect2i, inner: Rect2i, options?: NineSliceOptions): NineSliceBuilds 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.fromSheet(this.Demo.sheet: SpriteSheet | nullsheet, const outer: Rect2iouter, const inner: Rect2iinner, { NineSliceOptions.edges?: NineSliceMode | undefinedHow the four edges fill their length.edges: 'tile', NineSliceOptions.center?: NineSliceMode | undefinedHow 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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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 NineSliceA 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.NineSlice.NineSlice.fromSheet(sheet: SpriteSheet, outer: Rect2i, inner: Rect2i, options?: NineSliceOptions): NineSliceBuilds 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.fromSheet(this.Demo.sheet: SpriteSheet | nullsheet, const barOuter: Rect2ibarOuter, const barInner: Rect2ibarInner, { NineSliceOptions.center?: NineSliceMode | undefinedHow 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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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 NineSliceA 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.NineSlice.NineSlice.fromSheet(sheet: SpriteSheet, outer: Rect2i, inner: Rect2i, options?: NineSliceOptions): NineSliceBuilds 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.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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(const RIGHT_X: 340RIGHT_X, 284, 48, const BAR_H: 10BAR_H), new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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) => voidDraws 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.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) => voidDraws 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.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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.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): numberApplies an easing curve to a normalized time value.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) => voidDraws 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.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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.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) => voidDraws 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.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) => voidDraws 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.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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.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) => voidDraws 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.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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.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) => voidDraws 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.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.bootstrap(class DemoDemo);