/*
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.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 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 SpriteSheetSprite-sheet wrapper around a loaded image asset.
The class keeps the original image available for CPU-side inspection while
lazily creating and caching a GPU texture for rendering. When possible,
`load()` also pre-decodes the source into an `ImageBitmap` so texture uploads
preserve pixel-art alpha and color values more reliably.
After calling `indexize()`, the sheet stores palette indices rather than RGBA
data. The GPU texture becomes an `r8uint` format uploaded via `writeTexture`.
The original RGBA bytes are retained so `reindexize()` can re-convert without
reloading the image.SpriteSheet, class Vector2iInteger 2D vector for pixel-perfect positioning.
Used for points, sizes, directions, and camera offsets throughout the engine.
The API includes both allocation-free `*To()` / `*InPlace()` variants and
convenience methods that return new vectors.Vector2i } from 'blit386';
/** @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 | nullpalette = null;
/** @type {SpriteSheet | null} */
Demo.sheet: SpriteSheet | nullsheet = 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;
}>
tiles = [];
Demo.configure(): {
displaySize: Vector2i;
isOverlayEnabled: boolean;
}
configure() {
return {
displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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 | nullpalette = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteCreate: (size?: number) => PaletteCreates a standalone palette instance.paletteCreate(256);
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 SpriteSheetSprite-sheet wrapper around a loaded image asset.
The class keeps the original image available for CPU-side inspection while
lazily creating and caching a GPU texture for rendering. When possible,
`load()` also pre-decodes the source into an `ImageBitmap` so texture uploads
preserve pixel-art alpha and color values more reliably.
After calling `indexize()`, the sheet stores palette indices rather than RGBA
data. The GPU texture becomes an `r8uint` format uploaded via `writeTexture`.
The original RGBA bytes are retained so `reindexize()` can re-convert without
reloading the image.SpriteSheet.SpriteSheet.loadIndexed(url: string, palette: Palette, startSlot: number, options?: {
sort?: "luminance" | "none";
tileSize?: number | Vector2i;
}): Promise<IndexedSpriteLoadResult>
Convenience one-call path for palette-indexed sprite setup.
This combines:
1)
{@link
SpriteSheet.loadColorsIntoPalette
}
2)
{@link
SpriteSheet.load
}
3)
{@link
SpriteSheet.indexize
}
It returns the indexized sheet plus a full-frame source rectangle and the
colors that were written into the palette. Callers still control when to
activate the palette via `BT.paletteSet(palette)`.loadIndexed(const PATTERN_URL: "/sprites/pattern-16-2.png"PATTERN_URL, this.Demo.palette: Palettepalette, 1, { sort?: "luminance" | "none" | undefinedsort: 'none' });
this.Demo.sheet: SpriteSheet | nullsheet = 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) => 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);
// 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;
}>
tiles.push({ pos: Vector2ipos: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(let x: numberx, let y: numbery), src: Rect2isrc: new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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;
}>
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 | nullsheet, 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.bootstrap(class DemoDemo);