/*

Noise: turning scrambled numbers into landscapes.
@description Value, Perlin, and Simplex noise at matched settings, with an octaves slider and a terrain ramp.

Part of the BLIT386 demo series.

Prerequisites:
  - Basics: https://demos.blit386.dev/basics
  - Coordinate Patterns: https://demos.blit386.dev/coordinate-patterns

Live version: https://demos.blit386.dev/noise

Guides:
  - Random: https://blit386.dev/docs/guides/random#procedural-patterns-from-coordinates

Reference:
  - Random: https://blit386.dev/docs/api/random#pattern-noise

---

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

A 320x240 landscape filling the whole screen (the engine overlay is off), drawn as a grid
of square blocks, colored on a twelve-step ramp from deep water through sand, grass,
forest, and rock up to snow - or, with terrain colors switched off, on a sixteen-step
ramp from near-black to white.

A toolbar along the top holds the "1 Value", "2 Perlin", and "3 Simplex" buttons and a
block-size button that steps through 8, 4, 2, and 1 pixel blocks. The flavor in use and
the block-size button are lit green. Two panels of the same
height sit in the bottom corners: "Cost" on the left with the block size, how many noise
samples one picture takes, and how often it is rebuilt, and a panel on the right named
after the current flavor with the Octaves and Zoom sliders and two checkboxes.

It opens on Perlin noise at the finest setting, one block per pixel, drifting slowly like
clouds. Switch between the three ways of making noise, stack extra layers of detail on
top of each other, zoom in and out, and coarsen the blocks to see what each step costs.

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

- new ValueNoise(seed), new PerlinNoise(seed), and new SimplexNoise(seed): three flavors
  with the same methods and a different character.
- noise2D(x, y) gives a smooth value from -1 to 1 for any spot you ask about.
- "Octaves" means stacking the same noise again, smaller and fainter each time, which is
  what adds crags to smooth hills. That stack is called fbm: fbm2D(x, y, octaves).
- noise3D(x, y, z) and fbm3D(x, y, z, octaves) use the third number as time, which makes
  clouds drift.
- Bigger blocks mean fewer noise samples, so the picture is coarser but much cheaper to
  work out: halving the block size quadruples the samples, up to 76,800 at 1px.
- ui.button(label, { lit: { fill, rim } }) from the shared UI kit draws a button that is
  switched on, and litRimColor() works out its lighter rim from the theme.
- Working the whole picture out once into a buffer of palette slots, turning it into a
  sprite sheet with SpriteSheet.fromIndexedPixels(), and drawing it every frame with one
  BT.drawSprite() call - redone only when a control changes.

WHERE THIS CARRIES ON FROM
--------------------------

The Coordinate Patterns demo gave every tile its own scrambled number, and the result
looked like static: https://demos.blit386.dev/coordinate-patterns

Real landscapes are not static. Next to a hilltop you find more hilltop, not sea. Noise
fixes exactly that: it still works out an answer from a position, but nearby positions
now get answers close to each other. That single change turns speckle into scenery.

TRY THIS
--------

- Press 1, 2, or 3 (or tap Value, Perlin, Simplex) to switch flavor. All three share one
  seed, so the differences you see are the flavor, not luck.
- Drag "Octaves" from 1 to 6. One octave is plain noise; each extra octave piles finer
  detail on top.
- Drag "Zoom" to stretch the landscape out or crowd it together.
- Press T ("T Terrain colors") to switch between the terrain ramp and plain gray.
- Press A ("A Drift (3D)") to stop or restart the landscape moving through the third
  dimension.
- The demo starts at 1px blocks with drift on: the picture is worked out from scratch,
  76,800 samples at a time, every tick. Press B (the "B 1px" button) to step through 8,
  4, 2, and 1 pixel blocks, and watch the "Samples" count in the Cost panel.

*/

import { function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@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 PerlinNoise
Deterministic Perlin noise. Same seed and coordinates always produce the same sample.
@since1.5.0
PerlinNoise
, 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
, class SimplexNoise
Deterministic simplex noise (2D / 3D). Same seed and coordinates always produce the same sample.
@since1.5.0
SimplexNoise
, 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 ValueNoise
Deterministic value noise. Same seed and coordinates always produce the same sample.
@since1.5.0
ValueNoise
, 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 applyThemeapplyTheme, import litRimColorlitRimColor, import uiui } from './shared/ui.js'; /** @typedef {import('blit386').Palette} Palette */ // This demo runs at the engine's default screen size of 320x240 "game pixels". const const DISPLAY_W: 320DISPLAY_W = 320; const const DISPLAY_H: 240DISPLAY_H = 240; // The seed every generator shares, so switching flavors compares like with like. const const WORLD_SEED: 424242WORLD_SEED = 424242; // The three flavors, in the order the buttons offer them. const const KIND_VALUE: 0KIND_VALUE = 0; const const KIND_PERLIN: 1KIND_PERLIN = 1; const const KIND_SIMPLEX: 2KIND_SIMPLEX = 2; const const KIND_NAMES: {}KIND_NAMES = ['Value', 'Perlin', 'Simplex']; // Block sizes the B key steps through. Each block is one noise sample, so halving the // block size quadruples the samples: 1,200 at 8px, 4,800 at 4px, 19,200 at 2px, and // 76,800 at 1px - one per screen pixel. // // Drawing does not get dearer, though. The blocks are not drawn one by one: every // rebuild paints them into one 320x240 picture, and each frame draws that picture with a // single call. (Drawing 1px blocks one rectangle at a time would mean 76,800 draws per // frame, which brings the frame rate to a crawl.) What grows is the noise math, and only // when the picture is rebuilt - every tick while it drifts, otherwise only when a control // changes. const const BLOCK_SIZES: {}BLOCK_SIZES = [8, 4, 2, 1]; const const DEFAULT_BLOCK_INDEX: 3DEFAULT_BLOCK_INDEX = 3; // Zoom range. A smaller number stretches the landscape out; a bigger one crowds it // together. const const SCALE_MIN: 0.01SCALE_MIN = 0.01; const const SCALE_MAX: 0.12SCALE_MAX = 0.12; const const SCALE_DEFAULT: 0.035SCALE_DEFAULT = 0.035; // How many times the noise may be stacked on itself. const const OCTAVES_MIN: 1OCTAVES_MIN = 1; const const OCTAVES_MAX: 6OCTAVES_MAX = 6; // How fast the clouds drift when animation is switched on. const const DRIFT_SPEED: 0.012DRIFT_SPEED = 0.012; // The whole screen, as the source rectangle of the field picture and its spot on screen. const const FIELD_RECT: Rect2iFIELD_RECT = 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 DISPLAY_W: 320DISPLAY_W, const DISPLAY_H: 240DISPLAY_H);
const const FIELD_POS: Vector2iFIELD_POS = 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
(0, 0);
// Layout. A slim toolbar along the top holds the flavor and block-size buttons, and two // panels of the same height sit in the bottom corners. The landscape fills the screen // behind them. const const MARGIN: 4MARGIN = 4; const const TOOL_GAP: 4TOOL_GAP = 4; const const PANEL_H: 119PANEL_H = 119; const const PANEL_Y: numberPANEL_Y = const DISPLAY_H: 240DISPLAY_H - const MARGIN: 4MARGIN - const PANEL_H: 119PANEL_H; const const PANEL_GAP: 6PANEL_GAP = 6; const const SHAPE_PANEL_W: 150SHAPE_PANEL_W = 150; const const COST_PANEL_W: numberCOST_PANEL_W = const DISPLAY_W: 320DISPLAY_W - 2 * const MARGIN: 4MARGIN - const PANEL_GAP: 6PANEL_GAP - const SHAPE_PANEL_W: 150SHAPE_PANEL_W; // Widgets inside a panel are 20 pixels narrower than the panel itself. const const PANEL_CONTENT_SHRINK: 20PANEL_CONTENT_SHRINK = 20; // The toolbar's buttons, left to right. The kit stacks a group's rows, so the toolbar // reserves one button row and each button is placed with its own tiny group on top. A // group pinned with pad 0 starts its first row 5 pixels above the y it is given. const const TOOLBAR_CONTENT_X: 9TOOLBAR_CONTENT_X = 9; const const TOOLBAR_CONTENT_Y: 4TOOLBAR_CONTENT_Y = 4; const const BUTTON_ROW_H: 27BUTTON_ROW_H = 27; const const PINNED_ROW_SHIFT: 5PINNED_ROW_SHIFT = 5; const const TOOLBAR_W: numberTOOLBAR_W = const DISPLAY_W: 320DISPLAY_W - 2 * const MARGIN: 4MARGIN; const const FLAVOR_BUTTON_W: 62FLAVOR_BUTTON_W = 62; const const BLOCK_BUTTON_W: numberBLOCK_BUTTON_W = const TOOLBAR_W: numberTOOLBAR_W - const PANEL_CONTENT_SHRINK: 20PANEL_CONTENT_SHRINK - 3 * const FLAVOR_BUTTON_W: 62FLAVOR_BUTTON_W - 3 * const TOOL_GAP: 4TOOL_GAP; // The bottom panels' rows fall short of PANEL_H by different amounts; this much empty // space before each one's closing note evens them out, with the note at the bottom. const const COST_NOTE_GAP: 36COST_NOTE_GAP = 36; const const SHAPE_NOTE_GAP: 4SHAPE_NOTE_GAP = 4; // The cost panel's value column starts after this many characters. const const COST_KV_COLS: 8COST_KV_COLS = 8; // Color ramps. The shared UI kit owns slots 240-251, so both ramps stay well below that. // Terrain runs deep water to snow in slots 10-21; gray runs near-black to white in slots // 30-45. const const C_TERRAIN_BASE: 10C_TERRAIN_BASE = 10; const const TERRAIN_STEPS: 12TERRAIN_STEPS = 12; const const C_GRAY_BASE: 30C_GRAY_BASE = 30; const const GRAY_STEPS: 16GRAY_STEPS = 16; // The toolbar lights the button that is switched on - the current flavor and block size // - in the same green the Keyboard Diagnostic demo lights a held key with, rimmed with a // lighter shade of it (written in init()). const const C_LIT: 50C_LIT = 50; const const C_LIT_RIM: 51C_LIT_RIM = 51; const
const LIT_BUTTON: {
    fill: number;
    rim: number;
}
LIT_BUTTON
= { fill: numberfill: const C_LIT: 50C_LIT, rim: numberrim: const C_LIT_RIM: 51C_LIT_RIM };
// The terrain ramp, from the bottom of the sea to the top of the mountains. Each entry is // the color for one step of the ramp. const const TERRAIN_COLORS: {}TERRAIN_COLORS = [ [16, 38, 78], // deep water [24, 56, 106], [36, 78, 134], [52, 102, 160], // shallows [186, 176, 122], // sand [96, 148, 78], // grass [74, 126, 62], [56, 104, 50], // forest [98, 94, 88], // rock [124, 120, 114], [168, 166, 162], [232, 234, 238], // snow ]; // A landscape made of noise, with controls for every knob that shapes it. class class DemoDemo { /** @type {Palette | null} */ Demo.palette: Palette | null
@type{Palette | null}
palette
= null;
// Slot map for the shared UI kit theme, filled in by applyTheme() during init(). Demo.theme: nulltheme = null; // One generator per flavor. All three share a seed so switching is a fair comparison. Demo.generators: {}generators = []; // Which flavor is on screen - one of the KIND_* numbers. Perlin first: its rounded, // natural-looking shapes make the clearest landscape to start from. Demo.kind: numberkind = const KIND_PERLIN: 1KIND_PERLIN; // How zoomed in the landscape is. Demo.scale: numberscale = const SCALE_DEFAULT: 0.035SCALE_DEFAULT; // How many stacked layers of noise are added together. Demo.octaves: numberoctaves = 4; // Which entry of BLOCK_SIZES is in use. Demo.blockIndex: numberblockIndex = const DEFAULT_BLOCK_INDEX: 3DEFAULT_BLOCK_INDEX; // Terrain colors, or plain gray. Demo.showTerrain: booleanshowTerrain = true; // Whether the clouds are drifting. On from the start, at the finest block size, so // the demo opens on its most expensive setting - see the Cost panel. Demo.animate: booleananimate = true; // How far through the drift we are - the third coordinate handed to the 3D noise. Demo.driftZ: numberdriftZ = 0; // The finished picture, one palette slot per screen pixel: every block is painted // into it as a square of its own color. Allocated once, so no rebuild ever has to // make a new array. /** @type {Uint8Array | null} */ Demo.pixels: any
@type{Uint8Array | null}
pixels
= null;
// The picture as something the engine can draw: a sprite sheet made from `pixels`, // replaced after every rebuild. /** @type {SpriteSheet | null} */ Demo.fieldSheet: SpriteSheet | null
@type{SpriteSheet | null}
fieldSheet
= null;
// How many noise samples the last rebuild took. Demo.sampleCount: numbersampleCount = 0; // Set whenever a control changes, telling render() the picture is out of date. Demo.needsRebuild: booleanneedsRebuild = true; // Turns the engine overlay off, so the landscape fills the screen.
Demo.configure(): {
    isOverlayEnabled: boolean;
}
configure
() {
return { isOverlayEnabled: booleanisOverlayEnabled: false, }; } // Builds both color ramps and the three generators. 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);
// The terrain ramp, straight from the table above. for (let let i: numberi = 0; let i: numberi < const TERRAIN_STEPS: 12TERRAIN_STEPS; let i: numberi++) { const [const r: anyr, const g: anyg, const b: anyb] = const TERRAIN_COLORS: {}TERRAIN_COLORS[let i: numberi]; this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_TERRAIN_BASE: 10C_TERRAIN_BASE + let i: numberi, 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
(const r: anyr, const g: anyg, const b: anyb));
} // The gray ramp, evenly spaced from near-black (20) to white (255). Dividing by // (steps - 1) makes the last step land exactly on 255. for (let let i: numberi = 0; let i: numberi < const GRAY_STEPS: 16GRAY_STEPS; let i: numberi++) { const const level: anylevel = Math.round((let i: numberi / (const GRAY_STEPS: 16GRAY_STEPS - 1)) * 235) + 20; this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_GRAY_BASE: 30C_GRAY_BASE + let i: numberi, 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
(const level: anylevel, const level: anylevel, const level: anylevel));
} this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_LIT: 50C_LIT, 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
(75, 210, 120)); // lit button green
// Install the shared UI colors, then hand the finished palette to the engine. The // lit buttons' rim is worked out from the theme, so it comes after applyTheme(). this.Demo.theme: nulltheme = import applyThemeapplyTheme(this.Demo.palette: Palette
@type{Palette | null}
palette
);
this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_LIT_RIM: 51C_LIT_RIM, import litRimColorlitRimColor(this.Demo.palette: Palette
@type{Palette | null}
palette
, this.Demo.palette: Palette
@type{Palette | null}
palette
.Palette.get(index: number): Color32
Returns a copy of the color stored at a palette index. Returns a clone so callers cannot mutate internal state without going through {@link set } , which keeps the dirty flag accurate.
@paramindex - Palette index to read.@returnsClone of the stored color entry.@throwsError if the index is invalid.
get
(const C_LIT: 50C_LIT)));
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.paletteSet: (palette: Palette) => 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
);
// Same seed for all three, so any difference you see is the flavor, not the seed. // Each one is stored at the position its KIND_ number names, so the buttons can // look a generator up just by counting. this.Demo.generators: {}generators[const KIND_VALUE: 0KIND_VALUE] = new new ValueNoise(seed?: number): ValueNoise
Creates a value-noise sampler. Omit `seed` to use `0` (same default as coordinate hashes).
@paramseed - Any finite number; only its lower 32 bits affect the field.@since1.5.0
ValueNoise
(const WORLD_SEED: 424242WORLD_SEED);
this.Demo.generators: {}generators[const KIND_PERLIN: 1KIND_PERLIN] = new new PerlinNoise(seed?: number): PerlinNoise
Creates a Perlin-noise sampler. Omit `seed` to use `0` (same default as coordinate hashes).
@paramseed - Any finite number; only its lower 32 bits affect the field.@since1.5.0
PerlinNoise
(const WORLD_SEED: 424242WORLD_SEED);
this.Demo.generators: {}generators[const KIND_SIMPLEX: 2KIND_SIMPLEX] = new new SimplexNoise(seed?: number): SimplexNoise
Creates a simplex-noise sampler. Omit `seed` to use `0` (same default as coordinate hashes).
@paramseed - Any finite number; only its lower 32 bits affect the field.@since1.5.0
SimplexNoise
(const WORLD_SEED: 424242WORLD_SEED);
// A Uint8Array holds whole numbers from 0 to 255 - exactly the range of a palette // slot - in a fraction of the memory a normal array would take. this.Demo.pixels: any
@type{Uint8Array | null}
pixels
= new Uint8Array(const DISPLAY_W: 320DISPLAY_W * const DISPLAY_H: 240DISPLAY_H);
return true; } // Advances the drift when animation is on. Demo.update(): voidupdate() { // Always first: this is what makes the { key } shortcuts on buttons work. import uiui.tick(); if (this.Demo.animate: booleananimate) { // Walking forward through the third dimension is what makes the picture move. // Standing still in x and y while sliding through z is exactly how drifting // clouds are made. this.Demo.driftZ: numberdriftZ += const DRIFT_SPEED: 0.012DRIFT_SPEED; this.Demo.needsRebuild: booleanneedsRebuild = true; } } // Rebuilds the field when needed, draws it, then draws the toolbar and panels. // // The field is a sprite like the panels, and sprites draw in the order they are // called, so it has to come first or it would cover them. A control changed this // frame therefore shows up in the next frame's rebuild - a sixtieth of a second // later. Demo.render(): voidrender() { if (this.Demo.needsRebuild: booleanneedsRebuild) { this.Demo.rebuildField(): voidrebuildField(); this.Demo.needsRebuild: booleanneedsRebuild = false; }
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
.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => void (+1 overload)drawSprite(this.Demo.fieldSheet: SpriteSheet | null
@type{SpriteSheet | null}
fieldSheet
, const FIELD_RECT: Rect2iFIELD_RECT, const FIELD_POS: Vector2iFIELD_POS);
this.Demo.renderToolbar(): voidrenderToolbar(); this.Demo.renderCostPanel(): voidrenderCostPanel(); this.Demo.renderShapePanel(): voidrenderShapePanel(); } // Works out a palette slot for every block on screen, paints each block into the // picture, and turns the picture into a sprite sheet. // // This is the expensive part, which is why it only runs when something actually // changed. Standing still with animation off costs nothing at all. Demo.rebuildField(): voidrebuildField() { const const block: anyblock = const BLOCK_SIZES: {}BLOCK_SIZES[this.Demo.blockIndex: numberblockIndex]; const const generator: anygenerator = this.Demo.generators: {}generators[this.Demo.kind: numberkind]; const const cols: anycols = Math.ceil(const DISPLAY_W: 320DISPLAY_W / const block: anyblock); const const rows: anyrows = Math.ceil(const DISPLAY_H: 240DISPLAY_H / const block: anyblock); for (let let row: numberrow = 0; let row: numberrow < const rows: anyrows; let row: numberrow++) { for (let let col: numbercol = 0; let col: numbercol < const cols: anycols; let col: numbercol++) { // Turn the block's position on screen into a position in the landscape. // Multiplying by the scale is the zoom: a smaller scale means neighboring // blocks land closer together in the landscape, so the view is stretched // out. const const nx: numbernx = let col: numbercol * const block: anyblock * this.Demo.scale: numberscale; const const ny: numberny = let row: numberrow * const block: anyblock * this.Demo.scale: numberscale; const const value: anyvalue = this.Demo.sample(generator: any, nx: any, ny: any): anysample(const generator: anygenerator, const nx: numbernx, const ny: numberny); // The generators answer somewhere between -1 and 1. Adding 1 and halving // shifts that to between 0 and 1, which is what a ramp position needs to // be. const const slot: anyslot = this.Demo.rampSlot(t: any): anyrampSlot((const value: anyvalue + 1) / 2); this.Demo.paintBlock(x: any, y: any, size: any, slot: any): voidpaintBlock(let col: numbercol * const block: anyblock, let row: numberrow * const block: anyblock, const block: anyblock, const slot: anyslot); } } this.Demo.sampleCount: numbersampleCount = const cols: anycols * const rows: anyrows; // A sheet's pixels go to the graphics card once, the first time it is drawn, so a // changed picture needs a new sheet. destroy() frees the old one's memory. this.Demo.fieldSheet: SpriteSheet | null
@type{SpriteSheet | null}
fieldSheet
?.SpriteSheet.destroy(): void
Releases the GPU texture from memory and clears all retained pixel data. Also closes any retained `ImageBitmap` that has not yet been uploaded. Clears CPU-side `rgbaPixels` and `indexedPixels`; only image-backed sheets that have not been destroyed can recreate a GPU texture via a new load or {@link indexize } call.
destroy
();
this.Demo.fieldSheet: SpriteSheet | null
@type{SpriteSheet | null}
fieldSheet
= 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.fromIndexedPixels(width: number, height: number, indexedPixels: Uint8Array<ArrayBuffer>): SpriteSheet
Creates a sprite sheet from pre-computed palette-indexed pixel data. The resulting sheet is immediately indexized - its `getTexture()` call will produce an `r8uint` GPU texture without needing `indexize()`. This is used for embedded assets like the built-in system font where the pixel data is already expressed as palette indices. Sheets created this way do not support `indexize()` or `reindexize()` because there is no source RGBA data to re-map.
@paramwidth - Texture width in pixels.@paramheight - Texture height in pixels.@paramindexedPixels - Flat array of palette indices, one byte per pixel (row-major).@returnsSprite sheet ready for rendering.
fromIndexedPixels
(const DISPLAY_W: 320DISPLAY_W, const DISPLAY_H: 240DISPLAY_H, this.Demo.pixels: any
@type{Uint8Array | null}
pixels
);
} // Fills one block-sized square of the picture with `slot`, starting at pixel (x, y). // Blocks along the right and bottom edges are cut off at the edge of the picture. Demo.paintBlock(x: any, y: any, size: any, slot: any): voidpaintBlock(x: anyx, y: anyy, size: anysize, slot: anyslot) { const const right: anyright = Math.min(x: anyx + size: anysize, const DISPLAY_W: 320DISPLAY_W); const const bottom: anybottom = Math.min(y: anyy + size: anysize, const DISPLAY_H: 240DISPLAY_H); for (let let py: anypy = y: anyy; let py: anypy < const bottom: anybottom; let py: anypy++) { // fill() writes one value into a run of the array: here, one row of the // block. this.Demo.pixels: any
@type{Uint8Array | null}
pixels
.fill(slot: anyslot, let py: anypy * const DISPLAY_W: 320DISPLAY_W + x: anyx, let py: anypy * const DISPLAY_W: 320DISPLAY_W + const right: anyright);
} } // Asks one generator for a single value, in whichever way the controls call for. `nx` // and `ny` are the position in the landscape. Returns a value from about -1 to 1. Demo.sample(generator: any, nx: any, ny: any): anysample(generator: anygenerator, nx: anynx, ny: anyny) { // With animation on, the third number is how far the drift has traveled. Without // it, the flat 2D version is all we need and is cheaper to work out. if (this.Demo.animate: booleananimate) { if (this.Demo.octaves: numberoctaves <= 1) { return generator: anygenerator.noise3D(nx: anynx, ny: anyny, this.Demo.driftZ: numberdriftZ); } return generator: anygenerator.fbm3D(nx: anynx, ny: anyny, this.Demo.driftZ: numberdriftZ, this.Demo.octaves: numberoctaves); } // One octave means the plain noise, with no stacking at all. This is the honest // starting point - everything above it is the same shape with detail piled on. if (this.Demo.octaves: numberoctaves <= 1) { return generator: anygenerator.noise2D(nx: anynx, ny: anyny); } // More than one octave means fbm: the same noise added to itself again and again, // each time twice as crowded and half as strong. Big shapes stay, small ones // appear. return generator: anygenerator.fbm2D(nx: anynx, ny: anyny, this.Demo.octaves: numberoctaves); } // Turns a height `t` from 0 (lowest) to 1 (highest) into the palette slot that should // be drawn. Demo.rampSlot(t: any): anyrampSlot(t: anyt) { const const steps: 12 | 16steps = this.Demo.showTerrain: booleanshowTerrain ? const TERRAIN_STEPS: 12TERRAIN_STEPS : const GRAY_STEPS: 16GRAY_STEPS; const const base: 10 | 30base = this.Demo.showTerrain: booleanshowTerrain ? const C_TERRAIN_BASE: 10C_TERRAIN_BASE : const C_GRAY_BASE: 30C_GRAY_BASE; // Multiplying by the number of steps turns a 0-to-1 height into a step number. // The guards keep a value that lands exactly on 1 (or slightly outside the // expected range) from reaching past the end of the ramp. let let step: anystep = Math.floor(t: anyt * const steps: 12 | 16steps); if (let step: anystep < 0) { let step: anystep = 0; } if (let step: anystep >= const steps: 12 | 16steps) { let step: anystep = const steps: 12 | 16steps - 1; } return const base: 10 | 30base + let step: anystep; } // The toolbar along the top: the three flavors, bound to the keys 1, 2, and 3, and // the block-size button. The panel goes first with one empty row, then each button is // placed on top of it in its own tiny group (see PINNED_ROW_SHIFT). Demo.renderToolbar(): voidrenderToolbar() { import uiui.begin('topLeft', { x: numberx: const MARGIN: 4MARGIN, y: numbery: const MARGIN: 4MARGIN, width: numberwidth: const TOOLBAR_W: numberTOOLBAR_W }); import uiui.panel(''); import uiui.spacer(const BUTTON_ROW_H: 27BUTTON_ROW_H - const TOOLBAR_CONTENT_Y: 4TOOLBAR_CONTENT_Y - 4); import uiui.end(); for (let let i: numberi = 0; let i: numberi < const KIND_NAMES: {}KIND_NAMES.length; let i: numberi++) { const const x: numberx = const MARGIN: 4MARGIN + const TOOLBAR_CONTENT_X: 9TOOLBAR_CONTENT_X + let i: numberi * (const FLAVOR_BUTTON_W: 62FLAVOR_BUTTON_W + const TOOL_GAP: 4TOOL_GAP); if (this.Demo.toolButton(label: any, key: any, x: any, width: any, isOn: any): anytoolButton(`${let i: numberi + 1} ${const KIND_NAMES: {}KIND_NAMES[let i: numberi]}`, `Digit${let i: numberi + 1}`, const x: numberx, const FLAVOR_BUTTON_W: 62FLAVOR_BUTTON_W, let i: numberi === this.Demo.kind: numberkind)) { this.Demo.kind: numberkind = let i: numberi; this.Demo.needsRebuild: booleanneedsRebuild = true; } } const const blockX: numberblockX = const MARGIN: 4MARGIN + const TOOLBAR_CONTENT_X: 9TOOLBAR_CONTENT_X + 3 * (const FLAVOR_BUTTON_W: 62FLAVOR_BUTTON_W + const TOOL_GAP: 4TOOL_GAP); // The block button is always lit: it shows the size in use, not one choice of // several. if (this.Demo.toolButton(label: any, key: any, x: any, width: any, isOn: any): anytoolButton(`B ${const BLOCK_SIZES: {}BLOCK_SIZES[this.Demo.blockIndex: numberblockIndex]}px`, 'KeyB', const blockX: numberblockX, const BLOCK_BUTTON_W: numberBLOCK_BUTTON_W, true)) { // Step through the sizes and wrap around at the end. this.Demo.blockIndex: numberblockIndex = (this.Demo.blockIndex: numberblockIndex + 1) % const BLOCK_SIZES: {}BLOCK_SIZES.length; this.Demo.needsRebuild: booleanneedsRebuild = true; } } // One toolbar button in its own tiny group at column `x`, lit green when `isOn`. The // block button's label changes with every press, so each button is identified by its // key instead - the kit tells widgets apart by their id, which is the label unless // one is given. Returns true on the frame it is clicked, tapped, or its key is // pressed, like ui.button(). Demo.toolButton(label: any, key: any, x: any, width: any, isOn: any): anytoolButton(label: anylabel, key: anykey, x: anyx, width: anywidth, isOn: anyisOn) { import uiui.begin('topLeft', { x: anyx, y: numbery: const MARGIN: 4MARGIN + const TOOLBAR_CONTENT_Y: 4TOOLBAR_CONTENT_Y + const PINNED_ROW_SHIFT: 5PINNED_ROW_SHIFT, pad: numberpad: 0 }); const const pressed: anypressed = import uiui.button(label: anylabel, { key: anykey, width: anywidth, id: anyid: key: anykey,
lit: {
    fill: number;
    rim: number;
} | undefined
lit
: isOn: anyisOn ?
const LIT_BUTTON: {
    fill: number;
    rim: number;
}
LIT_BUTTON
: var undefinedundefined });
import uiui.end(); return const pressed: anypressed; } // What the current block size costs: how many noise samples one rebuild takes, and // how often a rebuild happens. Demo.renderCostPanel(): voidrenderCostPanel() { const const block: anyblock = const BLOCK_SIZES: {}BLOCK_SIZES[this.Demo.blockIndex: numberblockIndex]; import uiui.begin('topLeft', { x: numberx: const MARGIN: 4MARGIN, y: numbery: const PANEL_Y: numberPANEL_Y, width: numberwidth: const COST_PANEL_W: numberCOST_PANEL_W, kvCols: numberkvCols: const COST_KV_COLS: 8COST_KV_COLS }); import uiui.panel('Cost'); import uiui.kv('Blocks', `${const block: anyblock}x${const block: anyblock}px`); // toLocaleString('en-US') writes 76800 as "76,800", which is easier to read. import uiui.kv('Samples', this.Demo.sampleCount: numbersampleCount.toLocaleString('en-US')); import uiui.kv('Rebuild', this.Demo.animate: booleananimate ? 'every tick' : 'on change'); import uiui.spacer(const COST_NOTE_GAP: 36COST_NOTE_GAP); import uiui.label('Half the block size,', { color: stringcolor: 'dim' }); import uiui.label('four times the math.', { color: stringcolor: 'dim' }); import uiui.end(); } // The sliders and switches that shape the landscape. Demo.renderShapePanel(): voidrenderShapePanel() { const const sliderW: numbersliderW = const SHAPE_PANEL_W: 150SHAPE_PANEL_W - const PANEL_CONTENT_SHRINK: 20PANEL_CONTENT_SHRINK; import uiui.begin('topLeft', { x: numberx: const DISPLAY_W: 320DISPLAY_W - const MARGIN: 4MARGIN - const SHAPE_PANEL_W: 150SHAPE_PANEL_W, y: numbery: const PANEL_Y: numberPANEL_Y, width: numberwidth: const SHAPE_PANEL_W: 150SHAPE_PANEL_W }); import uiui.panel(`${const KIND_NAMES: {}KIND_NAMES[this.Demo.kind: numberkind]} noise`); // Every control compares its new value against the old one, and only asks for a // rebuild when something really moved. Otherwise the picture would be redrawn // from scratch on every single frame for no reason. const const nextOctaves: anynextOctaves = Math.round( import uiui.slider('Octaves', this.Demo.octaves: numberoctaves, { min: numbermin: const OCTAVES_MIN: 1OCTAVES_MIN, max: numbermax: const OCTAVES_MAX: 6OCTAVES_MAX, width: numberwidth: const sliderW: numbersliderW, decimals: numberdecimals: 0 }), ); if (const nextOctaves: anynextOctaves !== this.Demo.octaves: numberoctaves) { this.Demo.octaves: numberoctaves = const nextOctaves: anynextOctaves; this.Demo.needsRebuild: booleanneedsRebuild = true; } const const nextScale: anynextScale = import uiui.slider('Zoom', this.Demo.scale: numberscale, { min: numbermin: const SCALE_MIN: 0.01SCALE_MIN, max: numbermax: const SCALE_MAX: 0.12SCALE_MAX, width: numberwidth: const sliderW: numbersliderW, decimals: numberdecimals: 3, }); if (const nextScale: anynextScale !== this.Demo.scale: numberscale) { this.Demo.scale: numberscale = const nextScale: anynextScale; this.Demo.needsRebuild: booleanneedsRebuild = true; } const const nextTerrain: anynextTerrain = import uiui.checkbox('T Terrain colors', this.Demo.showTerrain: booleanshowTerrain, { key: stringkey: 'KeyT' }); if (const nextTerrain: anynextTerrain !== this.Demo.showTerrain: booleanshowTerrain) { this.Demo.showTerrain: booleanshowTerrain = const nextTerrain: anynextTerrain; this.Demo.needsRebuild: booleanneedsRebuild = true; } const const nextAnimate: anynextAnimate = import uiui.checkbox('A Drift (3D)', this.Demo.animate: booleananimate, { key: stringkey: 'KeyA' }); // Switching drift off has to ask for a rebuild as well. Drifting samples the 3D // generators; standing still samples the 2D ones. Without this the picture would // keep showing the last 3D frame while the panel claimed it was back to plain 2D. if (const nextAnimate: anynextAnimate !== this.Demo.animate: booleananimate) { this.Demo.animate: booleananimate = const nextAnimate: anynextAnimate; this.Demo.needsRebuild: booleanneedsRebuild = true; } import uiui.spacer(const SHAPE_NOTE_GAP: 4SHAPE_NOTE_GAP); if (this.Demo.octaves: numberoctaves <= 1) { import uiui.label('1 octave: plain noise.', { color: stringcolor: 'dim' }); } else { import uiui.label(`fbm stacks ${this.Demo.octaves: numberoctaves} octaves.`, { color: stringcolor: 'dim' }); } 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);