Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1,571 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BLIT386

CI npm version License: ISC WebGPU

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.

BLIT386 logo

Quick overview

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 loop

That 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.

Load a sprite and draw it

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);

Make an ocean out of one palette

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);

What makes it fun

  • 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.loadingAssetsCount tracks 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/vite plugin swaps code and assets in place during development without a full page reload.

Get started

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 dev

Works 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.

Add it to a project you already have

pnpm add blit386

bootstrap() 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.

Demos

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.

Documentation

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.

Inspiration

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.

Community

Made by

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.

License

ISC.