./jerome{m}

~/Experiments/Dither Lab.md · experiment · 4 min · updated

Dither Lab

2026 · Rust · WebAssembly · How this site works

Every cover on this site is dithered when the site is built: a picture goes in, a 1-bit amber one comes out. The Dither Lab is that step turned into a toy. Drop in a picture, paste one, or point the camera at yourself, then push it through a dozen dithering methods, a few palettes, and lights you place by hand. Nothing is uploaded: the picture never leaves your browser.

dither · liveDrop, paste or open a picture, or use the camera. Click it to place a sun or a halo, drag to move one, double-click to remove it; hold C to see the original. Copy link shares the recipe, never the picture.open ↗

A few colours for every tone

Dithering trades colours for pattern: when a screen or a printer has only a few colours, a pattern of them, seen from far enough, reads as the tones in between. The lab does it two ways.

  • A tone ramp, the default. The palette runs dark to light and each pixel's brightness picks a place along it; the dither decides, pixel by pixel, which of the two nearest colours to use. The picture's hues are thrown away and the palette's take their place, which is how navy, ochre and gold make a night scene.
  • Nearest colour. Each pixel goes to the palette colour closest to its own, and the error goes into the pattern. That keeps hues, so it suits colourful palettes like the PICO-8's sixteen.

One switch does a lot of the look: last colour for light. The picture's tones spread over every colour but the brightest, which only light can reach. Without a light the image is two colours of ink; place a sun and it burns through in a colour nothing else in the picture has.

Ordered: a pattern that never moves

Ordered dithering compares every pixel with a fixed threshold that repeats in a tile. The classic tile is Bryce Bayer's matrix (1973), built by doubling: a 2×2 seed, then four scaled copies of it interleaved, and again.

text
0 2      0  8  2 10      bayer 8 is the same
3 1     12  4 14  6      step once more: the
         3 11  1  9      covers on this site
        15  7 13  5      use exactly that one

Its strength is that it is local: a pixel's result depends only on its own value and position, so a dithered video or a dragged light doesn't crawl. Its weakness is the visible cross-hatch, which is half the charm here. The lab builds its matrices by that doubling, and a test checks the 8×8 one against the matrix the covers use.

Blue noise is the same idea with a tile that looks random but has no clumps: thresholds whose low values sit as far from one another as possible. The lab makes a 64×64 one when it first needs it, with Robert Ulichney's void-and-cluster method (1993): scatter a few points, move the tightest cluster into the emptiest void until nothing changes, then rank every pixel by taking clusters out and filling voids in.

Error diffusion: passing the mistake on

The other family walks the picture pixel by pixel, picks the closest colour, and pushes the difference onto the neighbours it hasn't reached yet. The kernels say where the error goes:

text
floyd–steinberg (1976)        · 7      ÷ 16
                            3 5 1

atkinson (Macintosh)          · 1 1    ÷ 8, but only 6/8 of it
                            1 1 1
                              1

sierra lite                   · 2      ÷ 4
                            1 1

Jarvis–Judice–Ninke (1976) and Stucki (1981) spread it over twelve neighbours, which gives smoother tones and softer edges. Atkinson's is my favourite: dropping a quarter of the error keeps highlights and shadows clean, which is a lot of why the original Mac looked the way it did. Serpentine runs every other row right to left, so the error doesn't pile up in diagonal streaks.

Screens: dots and lines

Halftone is how print does it: dots on a grid, larger where the tone is lighter, with the screen's cell size and angle as controls. Lines is an engraving screen: parallel lines that thicken with the tone, which turns a photo into something closer to a banknote than a newspaper.

Lights

A sun is three things. A solid disk in the light colour; a glow, added to the picture's tones before the dither, so it dissolves through the pattern instead of sitting on top; and, with reflection on, a column below it that widens with distance and is cut into ripples by a sine, like a low sun on water. A halo is a one-pixel ring, drawn after the dither so it stays crisp. Lights are stored in fractions of the picture, so they stay put when the resolution changes.

How it's built

The engine is a small Rust crate with no dependencies, compiled to an 82 KB WebAssembly module with no bindings at all: the page writes RGBA, settings and palette into buffers the module owns, calls run, and reads back a palette index per pixel. One trap worth writing down: a buffer call can grow WebAssembly memory, and growing it detaches every view onto the old memory, so the page asks for the pointer first and only then looks at memory.buffer.

The preview paints each dot as a whole number of device pixels and lets nothing scale it, because a dither stretched by a fraction turns into stripes. Export paints at a whole-number scale too, up to 4096 pixels on the long side. Settings and lights live in the frame's address as compressed JSON, like the shader playground's, which is what the island's copy link shares.

Recipes

  • Nocturne, the default: Bayer 8, the nocturne palette, last colour for light, gamma around 1.4, and a sun low over something that can take a reflection.
  • Handheld: 160 columns, the Game Boy palette, round pixels.
  • Newsprint: halftone, the paper palette, cell 6 at 45°.
  • Sixteen colours: Floyd–Steinberg, the PICO-8 palette, nearest colour.

Back to Experiments · see how this site works · browse the Notes.