/**
* Basics Demo - Your very first BLIT386 program!
*
* Welcome! This demo teaches you the absolute basics of making things appear
* on screen with the BLIT386 engine. You will learn:
* - How a demo is structured (configure, init, update, render, overlayRows)
* - How to pick colors with a palette and clear the screen
* - How to load a sprite (a tiny picture) and draw it
* - How to make that sprite move and bounce off the screen edges
* - How smooth motion works when update() and render() run at different speeds
* (BT.renderAlpha + Vector2i.lerp)
* - How to show a text hint with the shared UI kit (ui.label)
* - How to show live stats and a timing chart in the engine overlay
* (overlayRows, BT.assignTag)
*
* If you are new to BLIT386, read this file carefully from top to bottom.
* Almost every block has a comment explaining what it does and why.
*
* This demo sets targetFPS to 30 in configure() (slower than the engine default of 60)
* so motion is easy to follow. update() therefore runs about 30 times per second.
*
* IMPORTANT - update() vs. render():
*
* update() runs at a FIXED rate (targetFPS from configure()). It is where you do
* all game logic: move things, check wall collisions, count bounces. It may run
* multiple times per screen refresh if the computer needs to catch up, but never
* more than 8 times in a row.
*
* render() runs ONCE per screen refresh (often 60 times per second on a laptop
* screen, but it can be faster on 120 or 144 times-per-second monitors). It is
* where you draw everything. NEVER put game logic here - only drawing code.
*
* When you switch to a different browser tab, BOTH update() and render() pause
* completely. The browser stops calling them to save battery. When you come
* back, the engine catches up with a few extra update() calls (up to 8) so
* your game does not jump forward in time by a huge amount.
*
* Live version: https://demos.blit386.dev/basics
*/
/**
* "import" loads tools from the BLIT386 engine library.
* Think of it like opening a toolbox before you start building.
* - bootstrap: a helper that starts the engine and connects your demo to it
* - BT: the main engine object - you call BT.clear(), BT.drawSprite(), etc.
* - Color32: represents a color with Red, Green, Blue (and optional Alpha)
* - SpriteSheet: a loaded image you can draw pieces of on screen (a "sprite")
* - Vector2i: a 2D point or direction using whole numbers (x, y)
*/
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;
... 98 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT, class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32, type SpriteSheet = SpriteSheet
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.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';
// The shared demo UI kit - every demo in this series uses it for on-screen text and panels
// so they all look the same. applyTheme() installs the kit's colors into our palette, and
// ui.* draws things like the hint label you see in the top-left corner.
import { import applyThemeapplyTheme, import uiui } from './shared/ui.js';
/**
* These @typedef lines tell code editors what types we mean when we write
* Palette, SpriteSheet, and so on. They do not change how the demo runs.
* IBTDemo is the contract that says a demo needs init, update, and render.
* configure() and overlayRows() are optional extras (this demo uses both).
* If you skip configure(), the engine uses defaults: 320x240 logical pixels,
* 640x480 canvas, 60 updates per second.
*/
/** @typedef {import('blit386').IBTDemo} IBTDemo */
/** @typedef {import('blit386').HardwareSettings} HardwareSettings */
/** @typedef {import('blit386').Palette} Palette */
/** @typedef {import('blit386').SpriteSheet} SpriteSheet */
/** @typedef {import('blit386').Rect2i} Rect2i */
// BLIT386 uses a "palette" - a numbered list of colors you choose BEFORE drawing.
// Think of it like an artist picking paint colors and laying them on a palette tray
// before starting a painting. Each color gets a number (an "index").
// When we draw, we say "use color number 1" instead of spelling out the color each time.
//
// Index 0 is always transparent (completely invisible). Our custom colors start at 1.
const const C_BG: 1C_BG = 1; // Almost-black with a faint green tint - our screen background.
const const C_OVERLAY_BAR: 2C_OVERLAY_BAR = 2; // Slightly lighter bar behind overlay text (easy to read on dark green).
const const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN = 3; // Bright green for the position line in the overlay.
const const C_OVERLAY_AMBER: 4C_OVERLAY_AMBER = 4; // Amber (warm yellow) for the bounce count in the overlay.
const const C_OVERLAY_ERROR: 5C_OVERLAY_ERROR = 5; // Red the timing chart uses when a frame is badly late.
// Where the sprite's own colors begin in the palette.
// We already use slots 1-5 for background and overlay colors. Starting the sprite
// colors at 10 leaves a little empty room (slots 6-9) so we can add more scene
// colors later without having to renumber the sprite slots.
const const SPRITE_BASE: 10SPRITE_BASE = 10;
// Path to the sprite image. The "public" folder contents are served at the
// site root, so /sprites/logo-1.png maps to public/sprites/logo-1.png on disk.
const const SPRITE_URL: "/sprites/logo-1.png"SPRITE_URL = '/sprites/logo-1.png';
/**
* Bouncing-sprite demo - a friendly first BLIT386 demo.
*
* Every BLIT386 demo is a class the engine drives. Three methods are required;
* configure() and overlayRows() are optional extras this file also uses:
*
* 1. configure() - optional. If you define it, the engine calls it once at
* the very start so you can change settings (FPS, overlay options, and more).
* If you skip it, you get sensible defaults (320x240, 640x480 output, 60 FPS).
*
* 2. init() - called once after hardware settings are ready. This is where you
* load images, set up colors, and pick starting positions. It uses "async"
* because loading files takes time, and we need to wait for them to finish
* (like waiting for a web page to load).
*
* 3. update() - called at the targetFPS rate (30 per second in this demo).
* This is where you move the sprite, flip speed when it hits a wall, and
* count bounces. It runs at a FIXED pace so motion matches on fast and slow
* computers. See the file header for the full explanation.
*
* 4. overlayRows() - optional. Feeds extra text lines into the engine overlay
* (the HUD you toggle with the ~ key). Not required, but handy for live stats.
*
* 5. render() - called once per screen refresh to draw everything. Clear the
* screen, draw shapes, print text - all drawing goes here.
*
* @implements {IBTDemo}
*/
class class DemoBouncing-sprite demo - a friendly first BLIT386 demo.
Every BLIT386 demo is a class the engine drives. Three methods are required;
configure() and overlayRows() are optional extras this file also uses:
1. configure() - optional. If you define it, the engine calls it once at
the very start so you can change settings (FPS, overlay options, and more).
If you skip it, you get sensible defaults (320x240, 640x480 output, 60 FPS).
2. init() - called once after hardware settings are ready. This is where you
load images, set up colors, and pick starting positions. It uses "async"
because loading files takes time, and we need to wait for them to finish
(like waiting for a web page to load).
3. update() - called at the targetFPS rate (30 per second in this demo).
This is where you move the sprite, flip speed when it hits a wall, and
count bounces. It runs at a FIXED pace so motion matches on fast and slow
computers. See the file header for the full explanation.
4. overlayRows() - optional. Feeds extra text lines into the engine overlay
(the HUD you toggle with the ~ key). Not required, but handy for live stats.
5. render() - called once per screen refresh to draw everything. Clear the
screen, draw shapes, print text - all drawing goes here.Demo {
// - Instance properties -
// These are values that belong to this demo. They keep track of where
// things are and how they are moving. We set them up here at the top
// so they are easy to find.
// "pos" is short for "position". It stores where the sprite is on screen.
// Vector2i holds two whole numbers: x (horizontal) and y (vertical).
// (0, 0) is the top-left corner of the screen. x increases going right,
// y increases going DOWN (this is different from math class where y goes up!).
// We start near the screen center as a placeholder; init() will
// overwrite this with the exact center calculated from the real display size.
Demo.pos: Vector2ipos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(160, 120);
// "speed" is how many pixels the sprite moves each update().
// x=1 means it moves 1 pixel to the right each tick.
// y=1 means it moves 1 pixel downward each tick.
// When we make a number negative (like -1), the sprite moves in the
// opposite direction (left instead of right, or up instead of down).
Demo.speed: Vector2ispeed = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(1, 1);
// "prevPos" remembers where the sprite was at the START of the most recent
// update() tick, before that tick moved it. render() uses this together with
// BT.renderAlpha to draw the sprite smoothly between ticks - see the big comment
// above render() below for the full explanation. It starts equal to pos (both
// point at the same placeholder above) so the very first render() has nothing
// to blend between; init() updates both again once the real starting position
// is known.
Demo.prevPos: Vector2iprevPos = this.Demo.pos: Vector2ipos;
// "size" is how big the sprite is: we start with 16x16 as a guess.
// We update this from the loaded image in init() so the bounce
// checks stay correct even if you swap the PNG for a bigger one.
Demo.size: Vector2isize = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(16, 16);
// "bounces" counts how many times the sprite has hit a wall.
// We show this in the overlay so you can see it going up.
Demo.bounces: numberbounces = 0;
// "palette" holds the list of colors the engine will use for drawing.
// We create it in init() once we know what colors we need.
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = null;
// "spriteSheet" is the loaded image we will draw on screen.
// It stays null until init() finishes loading the PNG file.
/** @type {SpriteSheet | null} */
Demo.spriteSheet: SpriteSheet | nullspriteSheet = null;
// "spriteRect" tells the engine WHICH rectangular piece of the image to draw.
// A sprite sheet can hold many sprites in one big picture. Our PNG only has
// one sprite, so the rectangle covers the whole image: (x=0, y=0, full width, full height).
/** @type {Rect2i | null} */
Demo.spriteRect: Rect2i | nullspriteRect = null;
// Reused every frame for the engine overlay (position + bounces).
// We keep one array and update the text strings in place so we do not
// create brand-new objects on every screen refresh.
Demo.overlayRowData: {}overlayRowData = [
{ leftText: stringleftText: 'Position 0, 0', textPaletteIndex: numbertextPaletteIndex: const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN },
{ leftText: stringleftText: 'Bounces 0', textPaletteIndex: numbertextPaletteIndex: const C_OVERLAY_AMBER: 4C_OVERLAY_AMBER },
];
/**
* Called once at the very start. Returns settings the engine should use.
* This demo does not change the screen size (the engine keeps its defaults:
* 320x240 logical pixels, 640x480 on the web page). We mainly slow down
* update() and turn on helpful overlay tools for learning.
*
* @returns {Partial<HardwareSettings>}
*/
Demo.configure(): Partial<HardwareSettings>Called once at the very start. Returns settings the engine should use.
This demo does not change the screen size (the engine keeps its defaults:
320x240 logical pixels, 640x480 on the web page). We mainly slow down
update() and turn on helpful overlay tools for learning.configure() {
// We only change the settings listed below. Everything else (display size,
// canvas size, and more) comes from the engine's defaultConfig().
return {
// How often update() should run. 30 times per second is slower than the
// engine default (60), so the bounce is easier to watch.
targetFPS: numbertargetFPS: 30,
// Live palette grid at the bottom: every palette slot as a tiny color chip.
// Slots your demo draws this frame show their color; unused slots look dim.
isOverlayPaletteEnabled: booleanisOverlayPaletteEnabled: true,
// Show 16 color chips per row and only 1 row at a time
// (scroll the rest with the mouse wheel or by dragging).
overlayPaletteColumns: numberoverlayPaletteColumns: 16,
overlayPaletteRowsVisible: numberoverlayPaletteRowsVisible: 1,
// Scrolling timing chart under the title row.
// Green marks show update() time; amber marks show render() time.
// One mark per screen refresh - handy for seeing when work spikes.
isOverlayTimingChartEnabled: booleanisOverlayTimingChartEnabled: true,
overlayTimingChartHeight: numberoverlayTimingChartHeight: 32,
// Which palette slots to use for the overlay bars
// (top FPS strip, bottom title strip, and the bar behind our custom rows).
overlayStyle: {
barPaletteIndex: number;
textPaletteIndex: number;
gapPaletteIndex: number;
}
overlayStyle: {
barPaletteIndex: numberbarPaletteIndex: const C_OVERLAY_BAR: 2C_OVERLAY_BAR,
textPaletteIndex: numbertextPaletteIndex: const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN,
gapPaletteIndex: numbergapPaletteIndex: const C_BG: 1C_BG,
},
// Colors for the timing chart: green = update, amber = render,
// amber again for "a bit late", red for "badly late", green for our H/V tags.
overlayTimingChartStyle: {
updateBarPaletteIndex: number;
renderBarPaletteIndex: number;
warningPaletteIndex: number;
errorPaletteIndex: number;
tagPaletteIndex: number;
}
overlayTimingChartStyle: {
updateBarPaletteIndex: numberupdateBarPaletteIndex: const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN,
renderBarPaletteIndex: numberrenderBarPaletteIndex: const C_OVERLAY_AMBER: 4C_OVERLAY_AMBER,
warningPaletteIndex: numberwarningPaletteIndex: const C_OVERLAY_AMBER: 4C_OVERLAY_AMBER,
errorPaletteIndex: numbererrorPaletteIndex: const C_OVERLAY_ERROR: 5C_OVERLAY_ERROR,
tagPaletteIndex: numbertagPaletteIndex: const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN,
},
};
}
/**
* Called once after hardware settings are ready (from configure() mixed with
* engine defaults, or defaults alone if you skip configure).
* Sets up the palette, loads the sprite image, and places the sprite
* in the center of the screen.
*
* The "async" keyword lets us use "await" inside this method. "await" pauses
* until something slow finishes (like loading an image from the server) and
* then continues with the result. Without "async", we could not use "await".
*
* @returns {Promise<boolean>} Return true when everything is ready.
* Returning false tells the engine that something went wrong.
*/
async Demo.init(): Promise<boolean>Called once after hardware settings are ready (from configure() mixed with
engine defaults, or defaults alone if you skip configure).
Sets up the palette, loads the sprite image, and places the sprite
in the center of the screen.
The "async" keyword lets us use "await" inside this method. "await" pauses
until something slow finishes (like loading an image from the server) and
then continues with the result. Without "async", we could not use "await".init() {
// Step 1: set up the color palette
// A palette is like an artist's tray of paint colors laid out before painting.
// We pick every color we need here so the engine knows about them in advance.
// BT.paletteCreate(256) makes a new empty palette with room for 256 colors.
// 256 is a common size in retro-style games - plenty of slots for this demo.
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;
... 98 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteCreate: (size?: number) => PaletteCreates a standalone palette instance.paletteCreate(256);
// Fill in the colors we need for background and overlay text.
// palette.set(number, color) stores one color in a numbered slot.
// Color32(Red, Green, Blue) - each value is 0 to 255.
// 0 = none of that color, 255 = maximum of that color.
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_BG: 1C_BG, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(16, 28, 16)); // Almost-black, faint green tint.
// Overlay colors (must match overlayStyle and overlayRowData above).
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_BAR: 2C_OVERLAY_BAR, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(24, 44, 28)); // Dark bar, slightly lighter than C_BG.
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_GREEN: 3C_OVERLAY_GREEN, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(80, 200, 110)); // Bright green for overlay text.
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_AMBER: 4C_OVERLAY_AMBER, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(220, 180, 60)); // Amber accent for the bounce row.
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_ERROR: 5C_OVERLAY_ERROR, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(200, 70, 70)); // Red when the timing chart marks a bad spike.
// Step 2: load the sprite AND put its colors into the palette
// The engine draws sprites using palette numbers, not raw Red/Green/Blue.
// So every color in the PNG must live in a palette slot first.
//
// SpriteSheet.loadIndexed() does the whole job in one call:
// 1. Opens the PNG and writes each unique color into the palette
// starting at SPRITE_BASE.
// 2. Loads the image as a sprite sheet.
// 3. "Indexizes" it: turns every pixel into a palette number
// (like labeling each paint blob with the slot it matches).
// 4. Returns the sheet plus a rectangle that covers the whole image.
//
// { sort: 'none' } keeps colors in the order they appear in the file
// (left to right, top to bottom). The default would sort them darkest-first
// instead - fine for many games, but we keep file order here so the slots
// match the PNG layout if you peek at the overlay palette grid.
//
// We "await" because reading the PNG takes a moment.
const const indexed: Promise<IndexedSpriteLoadResult>indexed = 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";
}): 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 SPRITE_URL: "/sprites/logo-1.png"SPRITE_URL, this.Demo.palette: Palettepalette, const SPRITE_BASE: 10SPRITE_BASE, { sort?: "luminance" | "none" | undefinedsort: 'none' });
this.Demo.spriteSheet: SpriteSheet | nullspriteSheet = const indexed: Promise<IndexedSpriteLoadResult>indexed.sheet;
this.Demo.spriteRect: Rect2i | nullspriteRect = const indexed: Promise<IndexedSpriteLoadResult>indexed.srcRect;
// Step 3: install the shared UI theme
// applyTheme() writes the demo series' twelve standard UI colors into high
// palette slots (240-251 by default, far away from our low scene slots), so the
// kit's hint label in render() has colors to draw with. It must run BEFORE
// BT.paletteSet() below so those colors are included when the palette goes live.
// (It also returns a map of slot names, but we do not need that map in this demo.)
import applyThemeapplyTheme(this.Demo.palette: Palettepalette);
// Step 4: activate the palette
// Tell the engine "use this palette from now on."
// Before this call, the engine does not know what colors are available.
// We do this AFTER adding the sprite and UI colors so they are included.
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;
... 98 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);
// Step 5: remember the sprite's pixel size
// The sprite sheet exposes its dimensions through the .size property.
// We copy them into our own size vector so the bounce checks below use
// the real image size (not our 16x16 guess from above).
this.Demo.size: Vector2isize = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(this.Demo.spriteSheet: SpriteSheet | nullspriteSheet.SpriteSheet.size: Vector2iGets the sprite-sheet dimensions in pixels.size.Vector2i.x: numberHorizontal component (defaults to 0).x, this.Demo.spriteSheet: SpriteSheet | nullspriteSheet.SpriteSheet.size: Vector2iGets the sprite-sheet dimensions in pixels.size.Vector2i.y: numberVertical component (defaults to 0).y);
// Step 6: position the sprite in the center of the screen
// BT.displaySize is how big the screen is (320x240 with the defaults).
// We subtract the sprite's size so the CENTER of the sprite is centered,
// not its top-left corner.
// Math.floor() rounds down to a whole number - we need whole pixels
// because you cannot draw at position 160.5 on a pixel screen.
const const screen: Vector2iscreen = 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;
... 98 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.displaySize: Vector2iActive logical render resolution in pixels.
This is the game/simulation coordinate space configured by the demo, not
the canvas element's CSS size. Each read returns a clone.displaySize;
const const x: anyx = Math.floor(const screen: Vector2iscreen.Vector2i.x: numberHorizontal component (defaults to 0).x / 2 - this.Demo.size: Vector2isize.Vector2i.x: numberHorizontal component (defaults to 0).x / 2);
const const y: anyy = Math.floor(const screen: Vector2iscreen.Vector2i.y: numberVertical component (defaults to 0).y / 2 - this.Demo.size: Vector2isize.Vector2i.y: numberVertical component (defaults to 0).y / 2);
this.Demo.pos: Vector2ipos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const x: anyx, const y: anyy);
// Keep prevPos in sync with the real starting position so the very first
// render() does not try to smoothly slide in from the (160, 120) placeholder.
this.Demo.prevPos: Vector2iprevPos = this.Demo.pos: Vector2ipos;
// Return true to tell the engine: "Everything loaded fine, start the demo!"
return true;
}
/**
* Called at a fixed rate (30 times per second in this demo).
*
* This is where ALL game logic goes: moving things, checking collisions,
* counting scores, etc. Never draw anything here - that belongs in render().
*
* update() may be called 0 to 8 times between screen refreshes:
* - Usually it runs about 30 times per second (our targetFPS).
* On a 60-times-per-second monitor, that is about one update every
* two screen refreshes.
* - If the computer is slow, it may run multiple times to catch up.
* - If you switch to another browser tab, it pauses completely.
* - When you come back, it runs up to 8 times to catch up.
*
* Each call to update() is called a "tick". You can check how many ticks
* have happened since the demo started with BT.ticks.
*/
Demo.update(): voidCalled at a fixed rate (30 times per second in this demo).
This is where ALL game logic goes: moving things, checking collisions,
counting scores, etc. Never draw anything here - that belongs in render().
update() may be called 0 to 8 times between screen refreshes:
- Usually it runs about 30 times per second (our targetFPS).
On a 60-times-per-second monitor, that is about one update every
two screen refreshes.
- If the computer is slow, it may run multiple times to catch up.
- If you switch to another browser tab, it pauses completely.
- When you come back, it runs up to 8 times to catch up.
Each call to update() is called a "tick". You can check how many ticks
have happened since the demo started with BT.ticks.update() {
// --- Bounce logic (game rules live only in update(), never in render()) ---
// Remember where the sprite was BEFORE this tick moves it. render() will use
// this a moment from now to draw a smooth in-between position instead of a
// pop - see the big comment above render() below.
this.Demo.prevPos: Vector2iprevPos = this.Demo.pos: Vector2ipos;
// Move the sprite by adding its speed to its position.
// Think of it like taking steps: if you are standing at position 160
// and your speed is 1, after one step you are at 161.
// .add() creates a new Vector2i with both numbers added together.
this.Demo.pos: Vector2ipos = this.Demo.pos: Vector2ipos.Vector2i.add(other: Vector2i): Vector2iAdds another vector and returns the result as a new vector.add(this.Demo.speed: Vector2ispeed);
// Wall test for left/right: pos is the sprite's TOP-LEFT corner.
// The right edge of the sprite is at pos.x + size.x, so we compare against
// displaySize.x - size.x (the farthest right the top-left corner may go
// while the whole sprite still fits on screen).
//
// We only flip the speed here - we do not push the sprite back onto the
// edge. So for one tick it can sit one pixel past the wall, then travel
// inward again. That is normal for this simple bounce.
if (this.Demo.pos: Vector2ipos.Vector2i.x: numberHorizontal component (defaults to 0).x <= 0 || this.Demo.pos: Vector2ipos.Vector2i.x: numberHorizontal component (defaults to 0).x >= 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;
... 98 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.displaySize: Vector2iActive logical render resolution in pixels.
This is the game/simulation coordinate space configured by the demo, not
the canvas element's CSS size. Each read returns a clone.displaySize.Vector2i.x: numberHorizontal component (defaults to 0).x - this.Demo.size: Vector2isize.Vector2i.x: numberHorizontal component (defaults to 0).x) {
// Bounce: multiply speed.x by -1 to reverse horizontal direction.
// If speed.x was 1 (going right), it becomes -1 (going left).
this.Demo.speed: Vector2ispeed.Vector2i.x: numberHorizontal component (defaults to 0).x = -this.Demo.speed: Vector2ispeed.Vector2i.x: numberHorizontal component (defaults to 0).x;
// Count this as a bounce.
this.Demo.bounces: numberbounces++;
// Mark the moment on the timing chart (press ~ / Backquote to show the overlay).
// Each tag scrolls left with the green/amber marks so you can line up
// spikes in update/render time with when the logo hit a wall.
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;
... 98 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.assignTag: (label?: string) => voidPlaces a labeled marker on the overlay timing chart at the current tick.
Requires `isOverlayTimingChartEnabled: true` in `configure()`. Tags scroll with the chart
history and are pruned when they leave the visible window. Empty labels become
`"Untitled"`. Chart width resets add an automatic `"Start"` tag.assignTag('H');
}
// Same check for the top and bottom edges.
if (this.Demo.pos: Vector2ipos.Vector2i.y: numberVertical component (defaults to 0).y <= 0 || this.Demo.pos: Vector2ipos.Vector2i.y: numberVertical component (defaults to 0).y >= 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;
... 98 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.displaySize: Vector2iActive logical render resolution in pixels.
This is the game/simulation coordinate space configured by the demo, not
the canvas element's CSS size. Each read returns a clone.displaySize.Vector2i.y: numberVertical component (defaults to 0).y - this.Demo.size: Vector2isize.Vector2i.y: numberVertical component (defaults to 0).y) {
// Flip the vertical speed.
this.Demo.speed: Vector2ispeed.Vector2i.y: numberVertical component (defaults to 0).y = -this.Demo.speed: Vector2ispeed.Vector2i.y: numberVertical component (defaults to 0).y;
this.Demo.bounces: numberbounces++;
// Same timing-chart marker as the left/right bounce above.
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;
... 98 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.assignTag: (label?: string) => voidPlaces a labeled marker on the overlay timing chart at the current tick.
Requires `isOverlayTimingChartEnabled: true` in `configure()`. Tags scroll with the chart
history and are pruned when they leave the visible window. Empty labels become
`"Untitled"`. Chart width resets add an automatic `"Start"` tag.assignTag('V');
}
}
/**
* Called once per screen refresh to draw everything.
*
* render() runs AFTER update(). By the time render() is called, all
* positions and scores are already calculated. render() just reads
* those values and draws the picture.
*
* IMPORTANT: render() runs once per screen refresh (your monitor's refresh
* rate - often 60, but 120 or 144 on gaming displays). Do NOT put game logic
* here because it would run at different speeds on different monitors.
*
* Every frame you must clear the screen and redraw everything from
* scratch. If you skip clearing, the old frame stays and new drawings
* pile up on top of it (which can look cool, but is usually a bug!).
*
* WHY THE SPRITE MIGHT LOOK LIKE IT STUTTERS WITHOUT THE FIX BELOW:
* This demo sets targetFPS to 30 (see configure() above), but render() still
* runs at your monitor's full refresh rate - 60, 120, whatever your screen
* supports. That means update() (which moves the sprite) and render()
* (which draws it) run at DIFFERENT speeds. On a 60-times-per-second monitor,
* render() runs roughly twice for every one update() - so if render() just
* drew this.pos every time, the sprite would sit frozen for one refresh, then
* hop forward, then sit frozen again. That hop-hop-hop motion is "stutter."
*
* THE FIX: BT.renderAlpha. Think of it like a movie: update() ticks are the
* individual film frames (say, one every two screen refreshes), and render()
* is a projector that can run faster than the film advances. BT.renderAlpha
* tells the projector how far along we are between "the last film frame" and
* "the next one" - a fraction from 0 (the last update() tick just finished) up
* to just under 1 (the next update() tick is about to happen). We use it below
* to blend prevPos (where the sprite WAS) toward pos (where it IS) so every
* single render() draws the sprite at its true in-between position instead of
* only where it was as of the last tick.
*/
Demo.render(): voidCalled once per screen refresh to draw everything.
render() runs AFTER update(). By the time render() is called, all
positions and scores are already calculated. render() just reads
those values and draws the picture.
IMPORTANT: render() runs once per screen refresh (your monitor's refresh
rate - often 60, but 120 or 144 on gaming displays). Do NOT put game logic
here because it would run at different speeds on different monitors.
Every frame you must clear the screen and redraw everything from
scratch. If you skip clearing, the old frame stays and new drawings
pile up on top of it (which can look cool, but is usually a bug!).
WHY THE SPRITE MIGHT LOOK LIKE IT STUTTERS WITHOUT THE FIX BELOW:
This demo sets targetFPS to 30 (see configure() above), but render() still
runs at your monitor's full refresh rate - 60, 120, whatever your screen
supports. That means update() (which moves the sprite) and render()
(which draws it) run at DIFFERENT speeds. On a 60-times-per-second monitor,
render() runs roughly twice for every one update() - so if render() just
drew this.pos every time, the sprite would sit frozen for one refresh, then
hop forward, then sit frozen again. That hop-hop-hop motion is "stutter."
THE FIX: BT.renderAlpha. Think of it like a movie: update() ticks are the
individual film frames (say, one every two screen refreshes), and render()
is a projector that can run faster than the film advances. BT.renderAlpha
tells the projector how far along we are between "the last film frame" and
"the next one" - a fraction from 0 (the last update() tick just finished) up
to just under 1 (the next update() tick is about to happen). We use it below
to blend prevPos (where the sprite WAS) toward pos (where it IS) so every
single render() draws the sprite at its true in-between position instead of
only where it was as of the last tick.render() {
// Clear the entire screen to the background color. This erases the previous frame.
// C_BG is palette index 1, which we set to (16, 28, 16) in init() - almost
// black with a faint green tint.
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;
... 98 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.clear: (paletteIndex: number) => voidSets the frame clear color using a palette index.
The renderer uses this color when clearing the full display at the start
of the next frame.clear(const C_BG: 1C_BG);
// Blend prevPos toward pos by BT.renderAlpha to get the sprite's true position
// at this exact render moment. Vector2i.lerp(a, b, t) walks a fraction t of the
// way from a to b: t=0 gives a (prevPos), t=1 gives b (pos), and anything in
// between gives a smooth blend - exactly what BT.renderAlpha provides each frame.
const const drawPos: Vector2idrawPos = 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.Vector2i.lerp(a: Vector2i, b: Vector2i, t: number): Vector2iLinearly interpolates between two vectors.
Result is truncated to integers. t is clamped to [0, 1].lerp(this.Demo.prevPos: Vector2iprevPos, this.Demo.pos: Vector2ipos, 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;
... 98 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.renderAlpha: numberFractional progress between the last completed fixed update and the next.
Intended for interpolating render state between fixed-update steps.renderAlpha);
// Draw the bouncing sprite at its smoothed position.
// BT.drawSprite takes (sheet, sourceRect, destinationPosition, paletteOffset).
// - sheet: the loaded image we want to draw from.
// - sourceRect: WHICH part of the image to draw. Our sprite fills the
// whole image, so spriteRect is the full image bounds.
// - destinationPosition: WHERE on screen to draw it (the top-left corner).
// - paletteOffset: a number added to every pixel's palette index. We
// pass 0 here to use the original colors. Bigger numbers can swap to
// alternate "team colors" - you will see this trick in a future demo.
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;
... 98 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => voidDraws a sprite region from an indexed sprite sheet.
Sprite draws are batched internally. Grouping draws from the same
{@link
SpriteSheet
}
minimizes batch flushes and reduces GPU state changes.
The sprite sheet must have been converted to palette indices via
`spriteSheet.indexize(palette)` before the first draw call. Prefer
`SpriteSheet.loadIndexed(...)` for one-call setup.
**Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1.
Index 0 is always transparent and is discarded by the fragment shader. The final palette
lookup is `storedIndex + paletteOffset`, so:
- `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`.
`palette[0]` is never reachable because stored indices start at 1.
- `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored
at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`,
and so on. Use this for palette-swap effects such as team colors or damage flashes.
**Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to
the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's
robust buffer access returns 0 for every component; because the fragment shader forces alpha
to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative
JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also
produces out-of-bounds black pixels.drawSprite(this.Demo.spriteSheet: SpriteSheet | nullspriteSheet, this.Demo.spriteRect: Rect2i | nullspriteRect, const drawPos: Vector2idrawPos, 0);
// On-canvas hint drawn with the shared UI kit. ui.begin()/ui.end() open and close
// a small group of UI rows; with no ui.panel() call the group is just floating
// text with no box around it. 'topLeft' anchors it to the top-left corner, and
// { color: 'dim' } picks the kit's muted gray so the hint stays out of the way.
// (Under the hood the kit prints with the same built-in 6x14 system font.)
// "~" is the Backquote key (usually under Esc, left of the 1 key).
import uiui.begin('topLeft');
import uiui.label('Press ~ or click/tap the symbol below', { color: stringcolor: 'dim' });
import uiui.label('to toggle the overlay', { color: stringcolor: 'dim' });
import uiui.end();
// Live stats (position, bounce count) come from overlayRows() in the engine HUD.
// The overlay also shows FPS, backend, and this demo's page title - we do not
// duplicate those strings here.
}
/**
* Optional hook: feeds extra text rows into the engine overlay (not the game canvas).
*
* The overlay is the HUD the engine draws after render(): FPS, demo title, timing chart,
* palette grid, and these custom rows. Toggle it with the ~ key (Backquote on the
* keyboard) or by clicking/tapping the symbol in the bottom-left corner.
* Each row here is plain text plus a palette index for its color.
* We reuse overlayRowData every frame and only rewrite the strings - no new arrays.
*
* @returns {readonly { leftText: string, textPaletteIndex: number }[]}
*/
Demo.overlayRows(): readonly {
leftText: string;
textPaletteIndex: number;
}[]
Optional hook: feeds extra text rows into the engine overlay (not the game canvas).
The overlay is the HUD the engine draws after render(): FPS, demo title, timing chart,
palette grid, and these custom rows. Toggle it with the ~ key (Backquote on the
keyboard) or by clicking/tapping the symbol in the bottom-left corner.
Each row here is plain text plus a palette index for its color.
We reuse overlayRowData every frame and only rewrite the strings - no new arrays.overlayRows() {
this.Demo.overlayRowData: {}overlayRowData[0].leftText = `Position (${this.Demo.pos: Vector2ipos.Vector2i.x: numberHorizontal component (defaults to 0).x}, ${this.Demo.pos: Vector2ipos.Vector2i.y: numberVertical component (defaults to 0).y})`;
this.Demo.overlayRowData: {}overlayRowData[1].leftText = `Bounces ${this.Demo.bounces: numberbounces}`;
return this.Demo.overlayRowData: {}overlayRowData;
}
}
// bootstrap() is the function that starts everything. You pass it your Demo
// class, and it takes care of:
// 1. Setting up the HTML canvas on the page
// 2. Picking a backend: WebGPU when the browser supports it, otherwise Canvas 2D software mode (see README)
// 3. Creating a new instance of your Demo class
// 4. Calling configure() when you define it, then init(), then the update/render loop
//
// After this line runs, your demo is alive and running!
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 DemoBouncing-sprite demo - a friendly first BLIT386 demo.
Every BLIT386 demo is a class the engine drives. Three methods are required;
configure() and overlayRows() are optional extras this file also uses:
1. configure() - optional. If you define it, the engine calls it once at
the very start so you can change settings (FPS, overlay options, and more).
If you skip it, you get sensible defaults (320x240, 640x480 output, 60 FPS).
2. init() - called once after hardware settings are ready. This is where you
load images, set up colors, and pick starting positions. It uses "async"
because loading files takes time, and we need to wait for them to finish
(like waiting for a web page to load).
3. update() - called at the targetFPS rate (30 per second in this demo).
This is where you move the sprite, flip speed when it hits a wall, and
count bounces. It runs at a FIXED pace so motion matches on fast and slow
computers. See the file header for the full explanation.
4. overlayRows() - optional. Feeds extra text lines into the engine overlay
(the HUD you toggle with the ~ key). Not required, but handy for live stats.
5. render() - called once per screen refresh to draw everything. Clear the
screen, draw shapes, print text - all drawing goes here.Demo);