// 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
//   Random Basics        https://demos.blit386.dev/random-basics
//   Coordinate Patterns  https://demos.blit386.dev/coordinate-patterns
//
// WHAT YOU WILL SEE
// A landscape that you can reshape with the controls. Switch between three ways of making
// noise, stack extra layers of detail on top of each other, zoom in and out, and set it
// drifting like clouds.
//
// 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.
//
// WHAT YOU WILL LEARN
//   - noise2D(x, y) gives a smooth value from -1 to 1 for any spot you ask about
//   - Three flavors - Value, Perlin, and Simplex - each with a different character
//   - "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
//   - noise3D(x, y, z) uses the third number as time, which makes clouds drift
//   - Bigger blocks mean fewer squares to draw, so the picture is coarser but much cheaper
//
// The engine splits work the usual way: update() moves things; render() only draws.
// See the Basics demo for the full story: https://demos.blit386.dev/basics

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;
    ... 106 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 ValueNoise
Deterministic value noise. Same seed and coordinates always produce the same sample.
@since1.5.0
ValueNoise
} from 'blit386';
import { import applyThemeapplyTheme, import uiui, import UI_ANCHORSUI_ANCHORS } from './shared/ui.js'; /** @typedef {import('blit386').IBTDemo} IBTDemo */ /** @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 demo will draw at. Smaller blocks mean a finer picture and more work. // // 4 pixels is as fine as this demo goes, and that limit is measured rather than guessed. // Each block is one drawing instruction, so 4px blocks mean 4,800 of them per frame, which // the engine handles comfortably at a full 60 frames a second. Halving to 2px quadruples // that to 19,200 and the frame rate collapses to about one - the cost of a block is small, // but it is not free, and enough small costs add up to a stall. const const BLOCK_SIZES: {}BLOCK_SIZES = [8, 4]; const const DEFAULT_BLOCK_INDEX: 1DEFAULT_BLOCK_INDEX = 1; // The smallest block we ever draw decides how big the sample buffer has to be. Reading it // back out of the list means adding a finer size above cannot leave the buffer too small. const const SMALLEST_BLOCK: anySMALLEST_BLOCK = Math.min(...const BLOCK_SIZES: {}BLOCK_SIZES); const const MAX_CELLS: numberMAX_CELLS = (const DISPLAY_W: 320DISPLAY_W / const SMALLEST_BLOCK: anySMALLEST_BLOCK) * (const DISPLAY_H: 240DISPLAY_H / const SMALLEST_BLOCK: anySMALLEST_BLOCK); // 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; // Color ramps. The shared UI kit owns slots 240-251, so both ramps stay well below that. // Terrain runs deep water to snow; gray runs black to white. 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 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. * * @implements {IBTDemo} */ class class Demo
A landscape made of noise, with controls for every knob that shapes it.
@implementsIBTDemo
Demo
{
/** @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, how zoomed in, and how many stacked layers. Demo.kind: numberkind = const KIND_VALUE: 0KIND_VALUE; Demo.scale: numberscale = const SCALE_DEFAULT: 0.035SCALE_DEFAULT; Demo.octaves: numberoctaves = 4; // Which entry of BLOCK_SIZES is in use. Demo.blockIndex: numberblockIndex = const DEFAULT_BLOCK_INDEX: 1DEFAULT_BLOCK_INDEX; // Terrain colors, or plain gray. Demo.showTerrain: booleanshowTerrain = true; // Drifting clouds, and how far through the drift we are. Demo.animate: booleananimate = false; Demo.driftZ: numberdriftZ = 0; // One palette slot per block on screen, worked out ahead of drawing. Allocated once at // the largest size we could ever need, so no frame ever has to make a new array. /** @type {Uint8Array | null} */ Demo.cells: any
@type{Uint8Array | null}
cells
= null;
// How many blocks across and down the buffer currently holds. Demo.cellCols: numbercellCols = 0; Demo.cellRows: numbercellRows = 0; // Set whenever a control changes, telling render() the picture is out of date. Demo.needsRebuild: booleanneedsRebuild = true; // One rectangle, reused for every block we draw. Thousands of blocks go by each frame, // and making a throwaway Rect2i for each one would leave the browser a pile of objects to // clear up. Moving the same rectangle into place instead makes no garbage at all, which // is the habit the shared UI kit follows too. Demo.blockRect: Rect2iblockRect = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(0, 0, 0, 0);
/** * Builds both color ramps and the three generators. * * @returns {Promise<boolean>} */ async Demo.init(): Promise<boolean>
Builds both color ramps and the three generators.
@returns
init
() {
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;
    ... 106 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 to white. 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));
} // Install the shared UI colors, then hand the finished palette to the engine. this.Demo.theme: nulltheme = import applyThemeapplyTheme(this.Demo.palette: Palette
@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;
    ... 106 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);
this.Demo.cells: any
@type{Uint8Array | null}
cells
= new Uint8Array(const MAX_CELLS: numberMAX_CELLS);
return true; } /** * Advances the drift when animation is on. */ Demo.update(): void
Advances the drift when animation is on.
update
() {
// 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 panels. */ Demo.render(): void
Rebuilds the field when needed, draws it, then draws the panels.
render
() {
if (this.Demo.needsRebuild: booleanneedsRebuild) { this.Demo.rebuildField(): void
Works out a palette slot for every block on screen. This is the expensive part, which is why it only runs when something actually changed. Standing still with animation off costs nothing at all.
rebuildField
();
this.Demo.needsRebuild: booleanneedsRebuild = false; } this.Demo.drawField(): void
Draws every block from the buffer worked out above.
drawField
();
this.Demo.renderKindPanel(): void
The three flavor buttons and the block size.
renderKindPanel
();
this.Demo.renderShapePanel(): void
The sliders and switches that shape the landscape.
renderShapePanel
();
} /** * Works out a palette slot for every block on screen. * * 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(): void
Works out a palette slot for every block on screen. This is the expensive part, which is why it only runs when something actually changed. Standing still with animation off costs nothing at all.
rebuildField
() {
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]; this.Demo.cellCols: numbercellCols = Math.ceil(const DISPLAY_W: 320DISPLAY_W / const block: anyblock); this.Demo.cellRows: numbercellRows = Math.ceil(const DISPLAY_H: 240DISPLAY_H / const block: anyblock); for (let let row: numberrow = 0; let row: numberrow < this.Demo.cellRows: numbercellRows; let row: numberrow++) { for (let let col: numbercol = 0; let col: numbercol < this.Demo.cellCols: numbercellCols; 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: numbervalue = this.Demo.sample(generator: ValueNoise | PerlinNoise | SimplexNoise, nx: number, ny: number): number
Asks one generator for a single value, in whichever way the controls call for.
@paramgenerator@paramnx - Position in the landscape, left to right.@paramny - Position in the landscape, top to bottom.@returnsA value from about -1 to 1.
sample
(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 t: numbert = (const value: numbervalue + 1) / 2; this.Demo.cells: any
@type{Uint8Array | null}
cells
[let row: numberrow * this.Demo.cellCols: numbercellCols + let col: numbercol] = this.Demo.rampSlot(t: number): number
Turns a 0-to-1 height into the palette slot that should be drawn.
@paramt - Height, where 0 is the lowest and 1 the highest.@returnsA palette slot number.
rampSlot
(const t: numbert);
} } } /** * Asks one generator for a single value, in whichever way the controls call for. * * @param {ValueNoise | PerlinNoise | SimplexNoise} generator * @param {number} nx - Position in the landscape, left to right. * @param {number} ny - Position in the landscape, top to bottom. * @returns {number} A value from about -1 to 1. */ Demo.sample(generator: ValueNoise | PerlinNoise | SimplexNoise, nx: number, ny: number): number
Asks one generator for a single value, in whichever way the controls call for.
@paramgenerator@paramnx - Position in the landscape, left to right.@paramny - Position in the landscape, top to bottom.@returnsA value from about -1 to 1.
sample
(generator: PerlinNoise | SimplexNoise | ValueNoise
@paramgenerator
generator
, nx: number
- Position in the landscape, left to right.
@paramnx - Position in the landscape, left to right.
nx
, ny: number
- Position in the landscape, top to bottom.
@paramny - Position in the landscape, top to bottom.
ny
) {
// 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: PerlinNoise | SimplexNoise | ValueNoise
@paramgenerator
generator
.function noise3D(x: number, y: number, z: number): number
Samples 3D Perlin noise at `(x, y, z)`.
@paramx - Continuous world X.@paramy - Continuous world Y.@paramz - Continuous world Z.@returnsSmooth value in approximately `[-1, 1]`.@since1.5.0
noise3D
(nx: number
- Position in the landscape, left to right.
@paramnx - Position in the landscape, left to right.
nx
, ny: number
- Position in the landscape, top to bottom.
@paramny - Position in the landscape, top to bottom.
ny
, this.Demo.driftZ: numberdriftZ);
} return generator: PerlinNoise | SimplexNoise | ValueNoise
@paramgenerator
generator
.function fbm3D(x: number, y: number, z: number, octaves?: number, persistence?: number, lacunarity?: number): number
Fractal Brownian motion over {@link noise3D } .
@paramx - Continuous world X.@paramy - Continuous world Y.@paramz - Continuous world Z.@paramoctaves - Octave count (default `4`; floored, at least 1).@parampersistence - Amplitude falloff per octave (default `0.5`).@paramlacunarity - Frequency scale per octave (default `2`).@returnsNormalized fBm in approximately `[-1, 1]`.@since1.5.0
fbm3D
(nx: number
- Position in the landscape, left to right.
@paramnx - Position in the landscape, left to right.
nx
, ny: number
- Position in the landscape, top to bottom.
@paramny - Position in the landscape, top to bottom.
ny
, 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: PerlinNoise | SimplexNoise | ValueNoise
@paramgenerator
generator
.function noise2D(x: number, y: number): number
Samples 2D Perlin noise at `(x, y)`.
@paramx - Continuous world X.@paramy - Continuous world Y.@returnsSmooth value in approximately `[-1, 1]`.@since1.5.0
noise2D
(nx: number
- Position in the landscape, left to right.
@paramnx - Position in the landscape, left to right.
nx
, ny: number
- Position in the landscape, top to bottom.
@paramny - Position in the landscape, top to bottom.
ny
);
} // 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: PerlinNoise | SimplexNoise | ValueNoise
@paramgenerator
generator
.function fbm2D(x: number, y: number, octaves?: number, persistence?: number, lacunarity?: number): number
Fractal Brownian motion over {@link noise2D } .
@paramx - Continuous world X.@paramy - Continuous world Y.@paramoctaves - Octave count (default `4`; floored, at least 1).@parampersistence - Amplitude falloff per octave (default `0.5`).@paramlacunarity - Frequency scale per octave (default `2`).@returnsNormalized fBm in approximately `[-1, 1]`.@since1.5.0
fbm2D
(nx: number
- Position in the landscape, left to right.
@paramnx - Position in the landscape, left to right.
nx
, ny: number
- Position in the landscape, top to bottom.
@paramny - Position in the landscape, top to bottom.
ny
, this.Demo.octaves: numberoctaves);
} /** * Turns a 0-to-1 height into the palette slot that should be drawn. * * @param {number} t - Height, where 0 is the lowest and 1 the highest. * @returns {number} A palette slot number. */ Demo.rampSlot(t: number): number
Turns a 0-to-1 height into the palette slot that should be drawn.
@paramt - Height, where 0 is the lowest and 1 the highest.@returnsA palette slot number.
rampSlot
(t: number
- Height, where 0 is the lowest and 1 the highest.
@paramt - Height, where 0 is the lowest and 1 the highest.
t
) {
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: number
- Height, where 0 is the lowest and 1 the highest.
@paramt - Height, where 0 is the lowest and 1 the highest.
t
* 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; } /** * Draws every block from the buffer worked out above. */ Demo.drawField(): void
Draws every block from the buffer worked out above.
drawField
() {
const const block: anyblock = const BLOCK_SIZES: {}BLOCK_SIZES[this.Demo.blockIndex: numberblockIndex]; // The same rectangle is nudged into place for each block, rather than a new one being // made every time. See blockRect above for why that matters so much here. this.Demo.blockRect: Rect2iblockRect.Rect2i.width: number
Width in pixels (defaults to 0).
width
= const block: anyblock;
this.Demo.blockRect: Rect2iblockRect.Rect2i.height: number
Height in pixels (defaults to 0).
height
= const block: anyblock;
for (let let row: numberrow = 0; let row: numberrow < this.Demo.cellRows: numbercellRows; let row: numberrow++) { for (let let col: numbercol = 0; let col: numbercol < this.Demo.cellCols: numbercellCols; let col: numbercol++) { this.Demo.blockRect: Rect2iblockRect.Rect2i.x: number
Left edge X coordinate (defaults to 0).
x
= let col: numbercol * const block: anyblock;
this.Demo.blockRect: Rect2iblockRect.Rect2i.y: number
Top edge Y coordinate (defaults to 0).
y
= let row: numberrow * const block: anyblock;
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;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.drawRectFill: (rect: Rect2i, paletteIndex: number) => void
Draws a filled rectangle.
@since0.1.0@paramrect - Rectangle bounds in display coordinates.@parampaletteIndex - Palette color index.
drawRectFill
(this.Demo.blockRect: Rect2iblockRect, this.Demo.cells: any
@type{Uint8Array | null}
cells
[let row: numberrow * this.Demo.cellCols: numbercellCols + let col: numbercol]);
} } } /** * The three flavor buttons and the block size. */ Demo.renderKindPanel(): void
The three flavor buttons and the block size.
renderKindPanel
() {
import uiui.begin(import UI_ANCHORSUI_ANCHORS.BOTTOM_LEFT); import uiui.panel('Flavor'); for (let let i: numberi = 0; let i: numberi < const KIND_NAMES: {}KIND_NAMES.length; let i: numberi++) { if (import uiui.button(const KIND_NAMES: {}KIND_NAMES[let i: numberi], { key: stringkey: `Digit${let i: numberi + 1}` })) { this.Demo.kind: numberkind = let i: numberi; this.Demo.needsRebuild: booleanneedsRebuild = true; } } if (import uiui.button(`Blocks: ${const BLOCK_SIZES: {}BLOCK_SIZES[this.Demo.blockIndex: numberblockIndex]}px`, { key: stringkey: 'KeyB' })) { // 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; } import uiui.end(); } /** * The sliders and switches that shape the landscape. */ Demo.renderShapePanel(): void
The sliders and switches that shape the landscape.
renderShapePanel
() {
import uiui.begin(import UI_ANCHORSUI_ANCHORS.BOTTOM_RIGHT); 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 })); 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 }); 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('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('Drift (uses 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.separator(); if (this.Demo.octaves: numberoctaves <= 1) { import uiui.label('One octave: plain noise.', { color: stringcolor: 'dim' }); } else { import uiui.label(`${this.Demo.octaves: numberoctaves} octaves: fbm adds detail.`, { 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 Demo
A landscape made of noise, with controls for every knob that shapes it.
@implementsIBTDemo
Demo
);