Ordered Dither Plate
A gradient quantised to a few tones by an 8x8 Bayer matrix — offset then quantised, dithered in linear light, one cell per device pixel.
Canvas 2D0 deps1 file · 7 KB TSXreduced-motion aware
Props
"use client";
import { useEffect, useRef } from "react";
// RECURSIVE BAYER CONSTRUCTION. M(2n) = [[4M, 4M+2], [4M+3, 4M+1]], which is
// the only ordering that spreads consecutive thresholds as far apart as
// possible. A hand-typed 8x8 table is where the typos live.
function bayer(order: number): number[][] {
let m = [[0, 2], [3, 1]];
for (let s = 2; s < order; s *= 2) {
const n = m.length;
const out: number[][] = [];
for (let y = 0; y < n * 2; y++) out.push(new Array(n * 2).fill(0));
for (let y = 0; y < n; y++) {
for (let x = 0; x < n; x++) {
const v = m[y][x] * 4;
out[y][x] = v;
out[y][x + n] = v + 2;
// ---------------------------------------------------------------
// Full source available with a viberdy Pro subscription.
// https://viberdy.dev/pricing
// ---------------------------------------------------------------
}Details
About this background
Ordered dithering is the newspaper-plate look, and it is routinely confused with halftoning. A halftone varies dot area on a screen rotated to an angle; ordered dithering keeps a fixed grid and varies which cells cross the threshold, with no dot and no screen angle — which is exactly why it reads as a printed plate rather than a photograph of one. Three details separate a working implementation from the usual broken one. The Bayer matrix is a signed OFFSET rather than a threshold: normalise it into minus a half to plus a half, add it to the tone, then quantise. Compare the tone against the matrix instead, as most attempts do, and every pixel resolves to the darkest or lightest level, so the whole midtone range collapses into a hard cut and the gradient disappears. The quantisation has to happen in linear light, because a gradient as authored is sRGB-encoded — a display curve, not a measure of light — and quantising it directly gives the shadows too many steps and the midtones too few, leaving a visible band across the middle. And the canvas backing store has to match device pixels, since the matrix is defined on a pixel grid: size it to CSS pixels and let the browser scale it and the cell structure resamples into grey mush with moire crawling through it. The matrix itself is built recursively rather than typed out, because one wrong entry in a hand-written table shows up forever as a speck.
Curator’s note
Compare the tone against the Bayer value and you get a hard cut; add the value as a signed offset and you get a gradient. Same table, opposite result.


