Skip to the plate
FORMA PUBLIC DOMAIN GENERATIVE ATLAS / ED. 0.28
Plate 52, Error-Diffusion Dithering: a still of the dither / error diffusion plate as the atlas renders it, in the colour accent.

PL. 52  ·  COLOUR / DITHER / ERROR DIFFUSION

Error-Diffusion Dithering

Robert W. Floyd & Louis Steinberg, 1976

OPEN THE LIVE PLATE ▸

DEFINITION

quantise each pixel; pass the error on
ε → 7⁄16 →,  3⁄16 ↙,  5⁄16 ↓,  1⁄16 ↘

NOTES

Where Bayer consults a fixed matrix, Floyd and Steinberg carry the rounding error to pixels not yet visited, so the mistakes cancel instead of accumulating. The result keeps detail ordered dithering loses — and it is inherently sequential: each pixel depends on every pixel before it, which is why this plate has no shader tab. The pair of dither plates is the whole parallel-versus-serial trade, drawn — and plate 131 adds the third route: the same error carried along a Hilbert curve instead of the raster, so the diffusion follows locality rather than scan order.

PROVENANCE

Origin
R. W. Floyd & L. Steinberg, "An Adaptive Algorithm for Spatial Greyscale", Proceedings of the Society for Information Display 17(2), 1976
Standing
Public domain — a quantisation algorithm from a published paper
Constants
The grid is locked: it sets both the dot size and the cost

HOUDINI · VEX

The same published mathematics as a Detail Wrangle body. Paste it into a Wrangle with Run Over set to Detail; every constant is the published value plus a tweak channel, so Create Spare Parameters gives a slider that starts where the paper does.

// FORMA — PL. 52 · ERROR-DIFFUSION DITHERING — Robert W. Floyd & Louis Steinberg, 1976
//   quantise each pixel; pass the error on
//   ε → 7⁄16 →,  3⁄16 ↙,  5⁄16 ↓,  1⁄16 ↘
// Paste into a Detail Wrangle (Run Over: Detail), no inputs needed.
// Written from the published mathematics, not adapted from any code.
// Constants arrive at their published values. Press the node's Create
// Spare Parameters button and every tweak becomes a slider — starting
// at 0, the published figure, and moving in the constant's own units.
// https://forma-gen.com/#plate=errordiff

float p_grid = 100 + chf('grid_tweak');     // grid · live 60 .. 140
float p_lv   = 2 + chf('lv_tweak');         // tone levels · live 2 .. 4

// The plate's own colour: FORMA's COLOUR accent as a cosine ramp,
// brightest near t = 0 and t = 1, near-black around t = 0.5.
vector forma_ramp(float t){
  return set(
    0.46 + 0.5 * cos(6.28318530718 * (t + 0)),
    0.2002 + 0.2176 * cos(6.28318530718 * (t + 0.05)),
    0.3896 + 0.4235 * cos(6.28318530718 * (t + 0.1)));
}

// Floyd–Steinberg, the serial half of the atlas's dither pair: quantise
// each cell in scan order and push the rounding error onto the neighbours
// the scan has not reached — 7/16 right, 3/16 down-left, 5/16 down, 1/16
// down-right, the 1976 split exactly. Inherently sequential, which is why
// the plate has no shader tab and why this cook is a single ordered loop.
// One point per non-black cell, the plate's own tone-scaled ramp as Cd and
// the tone itself as Alpha.
//
// The source the plate dithers is a sine grating that rotates on the clock
// through the plate's own phase; a cook has neither, so the grating is
// frozen at a fixed oblique angle — 30°, so both diffusion axes participate
// — with the roll at zero. A staging decision on the record, as ripple's
// rain is: the mathematics is the diffusion, not the test card under it.
float ang = M_PI / 6.0;

int n = int(rint(p_grid));
int h = n;                        // the square cloth of a square card
int L = int(rint(p_lv));

float img[];
resize(img, n * h);
float co = cos(ang), si = sin(ang);
for (int y = 0; y < h; y++){
    for (int x = 0; x < n; x++){
        float u = float(x) / float(n), v = float(y) / float(h);
        img[y * n + x] = 0.5 + 0.5 * sin(6.283 * 1.5 * (u * co + v * si));
    }
}

// the one ordered pass that is the whole algorithm
for (int y = 0; y < h; y++){
    for (int x = 0; x < n; x++){
        int i = y * n + x;
        float q = rint(img[i] * float(L - 1)) / float(L - 1);
        q = min(1.0, max(0.0, q));
        float err = img[i] - q;
        if (x + 1 < n) img[i + 1] += err * 7.0 / 16.0;
        if (y + 1 < h){
            if (x > 0) img[i + n - 1] += err * 3.0 / 16.0;
            img[i + n] += err * 5.0 / 16.0;
            if (x + 1 < n) img[i + n + 1] += err * 1.0 / 16.0;
        }
        if (q <= 0.0) continue;   // the ground is the ink, not a point
        // canvas y runs down; negated so the grating leans as the plate shows it
        int pt = addpoint(0, set(float(x - n / 2), float(h / 2 - y), 0.0));
        setpointattrib(0, "Cd", pt, forma_ramp(0.86 + 0.2 * q) * q);
        setpointattrib(0, "Alpha", pt, q);
    }
}