/*

Pattern Fill: cover the whole screen with one small repeating picture.
@description Tile one small PNG across the screen edge to edge, cropping the last row of tiles to fit.

Part of the BLIT386 demo series.

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

Live version: https://demos.blit386.dev/pattern-fill

Guides:
  - Palette: https://blit386.dev/docs/guides/palette#index-sprites-preferred-and-manual-flows

Reference:
  - Assets: https://blit386.dev/docs/api/assets#loading-assets
  - Rendering: https://blit386.dev/docs/api/rendering#drawing

---

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

A 960x600 screen covered edge to edge by one 16x16 picture, repeated like bathroom tiles.
Nothing moves and there is no panel. The engine overlay is switched off, so the pattern is
the whole demo.

960 divides by 16 exactly (60 tiles across), but 600 does not (37.5 tiles down). So the
bottom row of tiles is only half a tile tall: the demo copies just the top 8 pixels of the
picture there. The picture is never squashed or stretched, so every pixel stays crisp and
lines up with the screen's pixel grid.

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

- SpriteSheet.loadIndexed(url, palette, startSlot, options) loads a PNG, writes its colors
  into the palette from startSlot upward, and turns every pixel into a palette slot
  number.
- BT.drawSprite(sheet, srcRect, pos) copies one rectangle of the sheet to the screen. A
  smaller srcRect copies only part of the picture, which is how the bottom row of tiles is
  cropped.
- Math.min() picks the smaller of two numbers, which is all it takes to cut a tile short
  at the screen edge.
- Building the full list of tile positions once in init() so render() allocates nothing.

*/

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 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 SpriteSheet
Sprite-sheet wrapper around a loaded image asset. The class keeps the original image available for CPU-side inspection while lazily creating and caching a GPU texture for rendering. When possible, `load()` also pre-decodes the source into an `ImageBitmap` so texture uploads preserve pixel-art alpha and color values more reliably. After calling `indexize()`, the sheet stores palette indices rather than RGBA data. The GPU texture becomes an `r8uint` format uploaded via `writeTexture`. The original RGBA bytes are retained so `reindexize()` can re-convert without reloading the image.
@since0.1.0
SpriteSheet
, class Vector2i
Integer 2D vector for pixel-perfect positioning. Used for points, sizes, directions, and camera offsets throughout the engine. The API includes both allocation-free `*To()` / `*InPlace()` variants and convenience methods that return new vectors.
@since0.1.0
Vector2i
} from 'blit386';
/** @typedef {import('blit386').Palette} Palette */ const const DISPLAY_W: 960DISPLAY_W = 960; const const DISPLAY_H: 600DISPLAY_H = 600; // The PNG file and the size of one tile in it. The file is a single 16x16 picture. const const PATTERN_URL: "/sprites/pattern-16-2.png"PATTERN_URL = '/sprites/pattern-16-2.png'; const const TILE_SIZE: 16TILE_SIZE = 16; class class DemoDemo { /** @type {Palette | null} */ Demo.palette: Palette | null
@type{Palette | null}
palette
= null;
/** @type {SpriteSheet | null} */ Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
= null;
// One entry per tile on screen: where to draw it, and which piece of the picture to // copy. Built once in init() so drawing a frame allocates nothing. /** @type {Array<{ pos: Vector2i, src: Rect2i }>} */
Demo.tiles: Array<{
    pos: Vector2i;
    src: Rect2i;
}>
@type{Array<{ pos: Vector2i, src: Rect2i }>}
tiles
= [];
Demo.configure(): {
    displaySize: Vector2i;
    isOverlayEnabled: boolean;
}
configure
() {
return { displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const DISPLAY_W: 960DISPLAY_W, const DISPLAY_H: 600DISPLAY_H),
isOverlayEnabled: booleanisOverlayEnabled: false, }; } // Loads the picture, then works out the full list of tiles that cover the screen. 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);
try { // loadIndexed() reads the PNG, puts its colors into the palette from slot 1 // upward (slot 0 is reserved for "see-through"), and converts every pixel // into a palette slot number. 'none' keeps the colors in file order. const const art: Promise<IndexedSpriteLoadResult>art = await class SpriteSheet
Sprite-sheet wrapper around a loaded image asset. The class keeps the original image available for CPU-side inspection while lazily creating and caching a GPU texture for rendering. When possible, `load()` also pre-decodes the source into an `ImageBitmap` so texture uploads preserve pixel-art alpha and color values more reliably. After calling `indexize()`, the sheet stores palette indices rather than RGBA data. The GPU texture becomes an `r8uint` format uploaded via `writeTexture`. The original RGBA bytes are retained so `reindexize()` can re-convert without reloading the image.
@since0.1.0
SpriteSheet
.
SpriteSheet.loadIndexed(url: string, palette: Palette, startSlot: number, options?: {
    sort?: "luminance" | "none";
    tileSize?: number | Vector2i;
}): Promise<IndexedSpriteLoadResult>
Convenience one-call path for palette-indexed sprite setup. This combines: 1) {@link SpriteSheet.loadColorsIntoPalette } 2) {@link SpriteSheet.load } 3) {@link SpriteSheet.indexize } It returns the indexized sheet plus a full-frame source rectangle and the colors that were written into the palette. Callers still control when to activate the palette via `BT.paletteSet(palette)`.
@paramurl - Path or URL to the PNG file.@parampalette - Target palette used for both registration and indexization.@paramstartSlot - First palette slot to write discovered colors into.@changed1.8.0 Added `options.tileSize`.@paramoptions - Optional color-sort behavior for registration and tile grid size.@paramoptions.sort - Color ordering for palette registration.@paramoptions.tileSize - Tile grid cell size (see {@link SpriteSheet.tileSize}).@returnsObject with `sheet`, `srcRect`, and registered `colors`.@throwsRangeError if `options.tileSize` is not a positive integer on both axes.
loadIndexed
(const PATTERN_URL: "/sprites/pattern-16-2.png"PATTERN_URL, this.Demo.palette: Palette
@type{Palette | null}
palette
, 1, { sort?: "luminance" | "none" | undefinedsort: 'none' });
this.Demo.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
= const art: Promise<IndexedSpriteLoadResult>art.sheet;
} catch (function (local var) error: unknownerror) { console.error('[PatternFillDemo] Failed to load the pattern:', function (local var) error: unknownerror); return 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
.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
);
// Walk the screen in 16-pixel steps, row by row. Math.min() cuts a tile short // when fewer than 16 pixels are left before the screen edge. With a 960x600 // screen that only happens in the last row, which is 8 pixels tall. for (let let y: numbery = 0; let y: numbery < const DISPLAY_H: 600DISPLAY_H; let y: numbery += const TILE_SIZE: 16TILE_SIZE) { for (let let x: numberx = 0; let x: numberx < const DISPLAY_W: 960DISPLAY_W; let x: numberx += const TILE_SIZE: 16TILE_SIZE) { const const w: anyw = Math.min(const TILE_SIZE: 16TILE_SIZE, const DISPLAY_W: 960DISPLAY_W - let x: numberx); const const h: anyh = Math.min(const TILE_SIZE: 16TILE_SIZE, const DISPLAY_H: 600DISPLAY_H - let y: numbery); this.
Demo.tiles: Array<{
    pos: Vector2i;
    src: Rect2i;
}>
@type{Array<{ pos: Vector2i, src: Rect2i }>}
tiles
.push({ pos: Vector2ipos: 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
(let x: numberx, let y: numbery), src: Rect2isrc: 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 w: anyw, const h: anyh) });
} } return true; } Demo.update(): voidupdate() { // Nothing moves: the pattern is the whole demo. } Demo.render(): voidrender() { // No clear needed: the tiles cover every pixel of the screen. for (const
const tile: Array<{
    pos: Vector2i;
    src: Rect2i;
}>
tile
of this.
Demo.tiles: Array<{
    pos: Vector2i;
    src: Rect2i;
}>
@type{Array<{ pos: Vector2i, src: Rect2i }>}
tiles
) {
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.sheet: SpriteSheet | null
@type{SpriteSheet | null}
sheet
,
const tile: Array<{
    pos: Vector2i;
    src: Rect2i;
}>
tile
.src,
const tile: Array<{
    pos: Vector2i;
    src: Rect2i;
}>
tile
.pos);
} } } 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);