Skip to content
2025Live and maintained

Dither Lab

A browser tool that converts any image into 1-bit, 4-colour and 8-colour pixel art with real dithering algorithms. Fully client-side.

typescriptcanvastoolingwebgl

I wanted to understand dithering properly, so I built the smallest tool that would let me experiment with it. You drop an image in, you get pixel art out.

The algorithms

Four, because each fails differently and the failure modes are the interesting part.

Floyd–Steinberg is the default and the best all-rounder. It propagates quantisation error to neighbouring pixels that haven't been visited yet, weighted by distance.

ts
const FLOYD_STEINBERG = [
  { dx: 1, dy: 0, weight: 7 / 16 },
  { dx: -1, dy: 1, weight: 3 / 16 },
  { dx: 0, dy: 1, weight: 5 / 16 },
  { dx: 1, dy: 1, weight: 1 / 16 },
]

Ordered dithering uses a fixed Bayer matrix — 2×2, 4×4, 8×8. It's deterministic, which means it's tileable and perfect for CSS backgrounds, but it produces visible cross-hatch patterns.

ts
const BAYER_4 = [
  [0, 8, 2, 10],
  [12, 4, 14, 6],
  [3, 11, 1, 9],
  [15, 7, 13, 5],
]

Atkinson is Floyd–Steinberg that refuses to propagate all the error. It looks contrastier and holds up better on high-contrast source images — it's what early Mac art looked like.

None is the control. If the image still looks fine without dithering, your palette is doing too much work and you don't need any of this.

Everything is client-side

No upload, no server, no storage. canvas.getImageData() gives me a typed array, I mutate it in place, and putImageData writes it back. On a 400×400 image the whole pipeline runs in well under a frame, so the preview updates while you drag a slider.

The palette builder reduces to a nearest-colour lookup, which sounds like it should be a map — but it's a linear scan over a handful of colours, and the scan beats the map because the palette is tiny and the cache behaviour is perfect.

The part I'd do differently

I built the UI first and the engine second, then spent two evenings fighting the fact that I'd chosen DOM controls for a canvas application. Sliders should be <input type="range"> — native, keyboard-accessible, and it gives you arrow-key stepping for free. I hand-rolled mine. I know.

The engine is the good part. Everything after that is UI, and UI I can do quickly.

HAVE A QUESTION?

Open an issue on the repository, or just email me. Both work.

Email Razi

MORE PROJECTS

Live

This website — a statically exported Next.js app with a hand-built 8-bit design system, MDX content, and a CRT phosphor you can switch.

nextjstypescripttailwinddesign
LiveSource
Live

A zero-dependency WebAudio library that plays NES-style music from a compact note syntax. Ships about 4kb and no assets.

typescriptaudioweb-audioopen-source
LiveSource
In progress

A habit tracker built as a text adventure. Quests, XP, stats, and a deliberately punishing stat decay that made me actually do the things.

typescriptreactsqlitedesign
Source

Command palette

Search for a command to run