// 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.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 Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32, class PerlinNoiseDeterministic Perlin noise. Same seed and coordinates always produce the same sample.PerlinNoise, class Rect2iInteger rectangle for pixel-perfect bounds and regions.
Used throughout the engine for sprite regions, display-space bounds, and
zero-allocation geometry helpers. Both convenience getters and allocation-free
`*To()` helpers are provided so callers can choose between readability and
hot-path efficiency.Rect2i, class SimplexNoiseDeterministic simplex noise (2D / 3D). Same seed and coordinates always produce the same sample.SimplexNoise, class ValueNoiseDeterministic value noise. Same seed and coordinates always produce the same sample.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 DemoA landscape made of noise, with controls for every knob that shapes it.Demo {
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = 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: anycells = 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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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.init() {
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteCreate: (size?: number) => PaletteCreates a standalone palette instance.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: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_TERRAIN_BASE: 10C_TERRAIN_BASE + let i: numberi, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.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: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_GRAY_BASE: 30C_GRAY_BASE + let i: numberi, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.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: Palettepalette);
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) => 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);
// 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): ValueNoiseCreates a value-noise sampler. Omit `seed` to use `0` (same default as coordinate hashes).ValueNoise(const WORLD_SEED: 424242WORLD_SEED);
this.Demo.generators: {}generators[const KIND_PERLIN: 1KIND_PERLIN] = new new PerlinNoise(seed?: number): PerlinNoiseCreates a Perlin-noise sampler. Omit `seed` to use `0` (same default as coordinate hashes).PerlinNoise(const WORLD_SEED: 424242WORLD_SEED);
this.Demo.generators: {}generators[const KIND_SIMPLEX: 2KIND_SIMPLEX] = new new SimplexNoise(seed?: number): SimplexNoiseCreates a simplex-noise sampler. Omit `seed` to use `0` (same default as coordinate hashes).SimplexNoise(const WORLD_SEED: 424242WORLD_SEED);
this.Demo.cells: anycells = new Uint8Array(const MAX_CELLS: numberMAX_CELLS);
return true;
}
/**
* Advances the drift when animation is on.
*/
Demo.update(): voidAdvances 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(): voidRebuilds the field when needed, draws it, then draws the panels.render() {
if (this.Demo.needsRebuild: booleanneedsRebuild) {
this.Demo.rebuildField(): voidWorks 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(): voidDraws every block from the buffer worked out above.drawField();
this.Demo.renderKindPanel(): voidThe three flavor buttons and the block size.renderKindPanel();
this.Demo.renderShapePanel(): voidThe 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(): voidWorks 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): numberAsks one generator for a single value, in whichever way the controls call for.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: anycells[let row: numberrow * this.Demo.cellCols: numbercellCols + let col: numbercol] = this.Demo.rampSlot(t: number): numberTurns a 0-to-1 height into the palette slot that should be drawn.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): numberAsks one generator for a single value, in whichever way the controls call for.sample(generator: PerlinNoise | SimplexNoise | ValueNoisegenerator, nx: number- Position in the landscape, left to right.nx, ny: number- 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 | ValueNoisegenerator.function noise3D(x: number, y: number, z: number): numberSamples 3D Perlin noise at `(x, y, z)`.noise3D(nx: number- Position in the landscape, left to right.nx, ny: number- Position in the landscape, top to bottom.ny, this.Demo.driftZ: numberdriftZ);
}
return generator: PerlinNoise | SimplexNoise | ValueNoisegenerator.function fbm3D(x: number, y: number, z: number, octaves?: number, persistence?: number, lacunarity?: number): numberFractal Brownian motion over
{@link
noise3D
}
.fbm3D(nx: number- Position in the landscape, left to right.nx, ny: number- 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 | ValueNoisegenerator.function noise2D(x: number, y: number): numberSamples 2D Perlin noise at `(x, y)`.noise2D(nx: number- Position in the landscape, left to right.nx, ny: number- 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 | ValueNoisegenerator.function fbm2D(x: number, y: number, octaves?: number, persistence?: number, lacunarity?: number): numberFractal Brownian motion over
{@link
noise2D
}
.fbm2D(nx: number- Position in the landscape, left to right.nx, ny: number- 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): numberTurns a 0-to-1 height into the palette slot that should be drawn.rampSlot(t: number- 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.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(): voidDraws 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: numberWidth in pixels (defaults to 0).width = const block: anyblock;
this.Demo.blockRect: Rect2iblockRect.Rect2i.height: numberHeight 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: numberLeft edge X coordinate (defaults to 0).x = let col: numbercol * const block: anyblock;
this.Demo.blockRect: Rect2iblockRect.Rect2i.y: numberTop 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) => voidDraws a filled rectangle.drawRectFill(this.Demo.blockRect: Rect2iblockRect, this.Demo.cells: anycells[let row: numberrow * this.Demo.cellCols: numbercellCols + let col: numbercol]);
}
}
}
/**
* The three flavor buttons and the block size.
*/
Demo.renderKindPanel(): voidThe 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(): voidThe 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.bootstrap(class DemoA landscape made of noise, with controls for every knob that shapes it.Demo);