Feature Playground

Image / media · 5.7

Error-diffusion dithering

A Game Boy screenshot, or a 1-bit Mac.

Reducing an image to a tiny palette while preserving apparent detail.

4 knobs

How it actually works

The only feature in this catalog that is not a shader and could not be one. Everything else here is embarrassingly parallel: every pixel decides independently. This one requires the pixel to its left to have already made up its mind. That single dependency is why it lives on the CPU in 2026.

Notably, this runs on the CPU, not the GPU. Error diffusion is inherently sequential: each pixel's error propagates to neighbours that have not been processed yet, which fights the entire GPU model. JavaScript walks the pixels in order, rounds each to the nearest palette colour, takes the error (desired minus actual), and spreads it to neighbours by weights. Floyd-Steinberg's are 7/16, 3/16, 5/16, 1/16, asymmetric to avoid diagonal artifacts. Palette index comes from luminance: 0.299R + 0.587G + 0.114B.

The knobs, named

Algorithm, palette size, luminance weights, serpentine. An eight-way algorithm dropdown over the same image is the most educational control here: you can see the difference between Atkinson and Floyd-Steinberg instantly, and nobody can explain it in words.

KnobSourceWhat it teaches
Algorithm sourced The eight are the source's own list. Atkinson throws away 25% of the error, which is why old Macs look blown-out and clean. Bayer is here as the contrast: no error at all, just a threshold lookup.
Palette size sourced 2 is 1-bit. The technique is at its most honest at 2, where every pixel is a lie and the image still works.
Pixel scale ours Chunkiness. Also the perf knob: this is CPU work and the cost is per pixel.
Serpentine sourced Alternate the scan direction each row. Off, the errors all drift the same way and you get diagonal streaks.

sourced means the source names this parameter. ours means the source names none and the knob is our design against the mechanism. No knob here is invented and passed off as sourced.

Evidence

VERIFIED (author)

Codrops "Efecto" (2026); "WebGL Backgrounds with Bayer Dithering"; "Animating 160,000 Cubes in Three.js to Visualize Dithering"; "Dithering Process Visualization". The algorithms and the 7/16, 3/16, 5/16, 1/16 weights are verbatim. The ordered-vs-error-diffusion real-time contrast is marked INFERRED in the index.

Seen on
Codrops "Efecto" and the dithering series.
Dependencies
vanilla JS + Canvas 2D
Difficulty
moderate (Floyd-Steinberg is about 20 lines); hard to make interactive at video rates on CPU
Performance
CPU-bound. It blocks the main thread on large images, so the real answer is a worker. Ours keeps the source small and recomputes only on knob change, which is the sourced advice.
Accessibility and the floor
Static output, no motion, no reduced-motion branch needed. It is the same picture whether or not you allow animation.

Notes

Composability. Pairs with halftone (5.6). Same source image, two philosophies of destruction.

The sourced perf guidance is to do it once and cache, or move to ordered/Bayer for real time, and that is exactly what this page does: it dithers on knob change, not per frame. Nothing here runs in the rAF loop at all. It is the one feature whose correct implementation is to not animate.