A palette-first retro engine for the web. You draw with numbered colors instead of RGBA pixels – the same trick that made VGA games shimmer – and a modern GPU does the rest. Roll the palette and water flows, fire rises, the sky drifts at dusk, all without redrawing a single pixel. WebGPU when your browser has it, an automatic Canvas 2D fallback when it does not.
It is small, it is fast, and it is built to feel like a toy. That is the whole point.
import { bootstrap, BT, Color32, Rect2i, Vector2i } from 'blit386';
class Game {
// The box position, in pixels. update() changes it; render() only reads it.
x = 140;
speed = 1;
// init() runs once at startup. Set up your colors and load things here.
// Slot 0 is always transparent, so we start numbering at 1.
// Think of the palette as a numbered paint box.
async init() {
const palette = BT.paletteCreate(16); // room for 16 colors
palette.set(1, new Color32(32, 0, 128)); // a deep blue background
palette.set(2, new Color32(255, 220, 90)); // a warm yellow
BT.paletteSet(palette); // make this the palette the engine draws with
return true; // tell the engine setup went fine
}
// update() is the THINKING step: change the world here (move things, read
// input, run physics), but never draw. It runs at a FIXED rate – targetFPS,
// 60 times a second by default – no matter how fast the screen is. The engine
// runs it as many times per frame as it needs to hold that pace, so your game
// moves at the same speed on every machine.
update() {
this.x += this.speed; // slide the box sideways
if (this.x < 0 || this.x > 280) {
this.speed = -this.speed; // bounce off the screen edges
}
}
// render() is the DRAWING step: only paint the world as it is right now,
// never change state. It runs ONCE PER SCREEN REFRESH – way faster on a
// high-refresh monitor, slower on a struggling machine – so it is not locked
// to update().
render() {
// Draw with slot numbers, not colors.
BT.clear(1); // fill the screen with slot 1
BT.drawRectFill(new Rect2i(this.x, 100, 40, 40), 2); // the moving box
BT.systemPrint(new Vector2i(108, 160), 2, 'HELLO BLIT386'); // built-in font; bitmap fonts work too
}
}
bootstrap(Game); // hand the class to the engine and start the loopThat is a whole game on a 320×240 screen: update() thinks, render() draws, and the box slides back and forth at the
same speed on every machine – because update() ticks at a fixed rate while render() just follows your screen. No
config, no scene graph, no ceremony.
import { bootstrap, BT, Color32, Palette, SpriteSheet, Vector2i } from 'blit386';
class Game {
async init() {
this.palette = new Palette(256);
this.palette.set(1, new Color32(20, 20, 40)); // background
// loadIndexed() scans the PNG, drops its colors into the palette starting
// at slot 10, and hands you back the sheet plus a rect for the whole image.
this.hero = await SpriteSheet.loadIndexed('/sprites/hero.png', this.palette, 10);
BT.paletteSet(this.palette); // activate AFTER loadIndexed returns
return true;
}
update() {}
render() {
BT.clear(1);
BT.drawSprite(this.hero.sheet, this.hero.srcRect, new Vector2i(140, 100));
}
}
bootstrap(Game);This is the party trick. Build eight shades of blue, then tell the engine to rotate them. The pixels never change – only the paint-box labels shuffle – and the whole sea starts to move, exactly like it did in DeluxePaint.
import { bootstrap, BT, Color32, Rect2i } from 'blit386';
const OCEAN_START = 1; // eight blue slots live in 1..8
const OCEAN_END = 8;
class Game {
async init() {
const palette = BT.paletteCreate(16);
// A gradient from deep navy to bright cyan across the eight slots.
for (let i = 0; i < 8; i++) {
const t = i / 7; // 0..1
palette.set(OCEAN_START + i, new Color32(0, Math.floor(40 + t * 160), Math.floor(100 + t * 155)));
}
BT.paletteSet(palette);
// Roll those eight slots forward, ~4 steps a second. The engine keeps
// doing this every frame on its own. We never touch a pixel again.
BT.paletteCycle(OCEAN_START, OCEAN_END, 4);
return true;
}
update() {}
render() {
// Eight horizontal bands, one per ocean slot. As the palette rolls, the
// colors slide down the screen like a calm, glittering sea.
for (let i = 0; i < 8; i++) {
BT.drawRectFill(new Rect2i(0, i * 30, 320, 30), OCEAN_START + i);
}
}
}
bootstrap(Game);- Draw with numbers, not pixels: A 256-color paint box; every primitive and sprite is just a slot index.
- Animate colors, not geometry: Cycle, fade, flash, and swap give you water, lava, and lightning for the cost of one tiny palette upload.
- Retro palettes in the box: VGA, CGA, C64, Game Boy, PICO-8, and NES presets.
- Recolor without redrawing: Palette offsets turn one sprite sheet into team colors, day and night, or power-up states – no duplicate textures.
- CRT when you want it: A two-tier post-process chain with bundled CRT presets for that curved-glass glow.
- Everything a tiny engine needs: Pointer, keyboard, and gamepad input, a fixed-timestep loop with render-time interpolation for smooth motion between ticks, bitmap fonts, a camera, and one-call PNG frame capture.
- Sound that plays, not just routes: Fire off sound effects and crossfading music through a three-bus mixer (sfx, music, main) with volume, mute, and fades, synthesize blips and booms from scratch or reach for a built-in preset, and read live levels off the overlay's audio meters – all while tracking the browser's autoplay-gesture unlock honestly instead of pretending it doesn't exist.
- Loading progress:
BT.loadingAssetsCounttracks in-flight image and audio loads so you can drive a loading screen. - Mobile polish: wake lock, screen orientation lock and detection, and opt-in pointer and keyboard scroll capture.
- Hot reload: the
blit386/viteplugin swaps code and assets in place during development without a full page reload.
The fastest way – easy enough that a pigeon would skip its dinner to try it – is the scaffolder. It writes a ready-to-run Vite project, installs the engine, and drops in a starter game plus local docs.
npm create blit386@latest my-game
cd my-game
npm run devWorks with npm, pnpm, yarn, or bun – it uses whichever you ran it with. Open the address it prints and edit
src/game.js. See create-blit386 for the options and what lands in the
project.
pnpm add blit386bootstrap() looks for a canvas inside #canvas-container:
<div id="canvas-container"><canvas id="blit386-canvas"></canvas></div>
<script type="module" src="/src/main.js"></script>You need an ESM bundler (Vite, esbuild, webpack, and friends) and Node >=22.18.0. The engine wants a WebGPU browser and quietly falls back to Canvas 2D when there is not one – see Browser support for the version details.
On Vite, add the blit386/vite plugin to vite.config.js for hot reload during development – most code and asset edits
apply to the running game without a page reload (a hardware setting change or an unrecognized asset type still falls
back to one). See the Hot Reload guide.
Play the hosted demos at demos.blit386.dev, or read the source in the blit386-demos repo – dozens of small, heavily commented examples from a single moving square up to a full Snake game.
The full, typeset documentation lives at blit386.dev/docs. The Markdown sources are in
docs/ – start with API: Core for bootstrap() and initialization. The rest of the
important pages:
| Documentation | What it covers |
|---|---|
| API: Core | bootstrap, init, default configuration |
| API: Game Loop | tick timing, present FPS, Timer |
| Game Loop Guide | render-time interpolation, smoothing motion |
| API: Camera | global pixel offset, world-clamp helpers |
| API: Core Types | Vector2i, Rect2i, Color32 |
| API: Rendering | primitives, sprites, text, post-process, frame capture |
| API: Palette | palette setup, presets, effects, serialization |
| API: Assets | sprite sheets, bitmap fonts, loading progress |
| API: Audio | buses, SFX voice pool, music, procedural synthesis |
| API: Overlay | overlay configure flags and style |
| Overlay Guide | engine HUD subsystem, toggle, custom rows, layout |
| API: Browser Support | WebGPU matrix, wake lock, orientation, build toolchain |
| Input Guide | pointer, keyboard, gamepad, scroll capture |
| Hot Reload Guide | blit386/vite plugin, hot-swap, asset hot-replace |
| Palette Guide | the palette-first workflow, offsets, and effects |
| Audio Guide | loading, playing, and designing sound |
| Bitmap Fonts | .btfont format, BMFont conversion |
| Post-Process Effects | the effect chain, built-in effects, CRT presets |
| Changelog | release history in Keep a Changelog style |
The full index – performance, testing, security, contributor guides – lives in Developer Experience.
BLIT386 owes its whole philosophy to RetroBlit by Martin Cietwierkowski (@daafu) – a retro pixel framework for Unity that throws out the scene graph and hands you a clean, low-level demo loop. BLIT386 brings that same feeling to the web with WebGPU: no frameworks, just sprites, primitives, fonts, and a palette.
BLIT386 is built by Václav Vančura (@vancura) – one person, so far. I am not a player, I am an engine maker.
Want to help? The contributor workflow, scripts, and release process live in Developer Experience and CONTRIBUTING.md.
ISC.