Skip to content

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

ProDither & ASCIIeditorialminimalbrutalist
Preview

Props

3
2
0.22

The source is part of Pro

$27/month or $179 once. See Pro

No runtime dependencies
"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.

Pairs well with

Trending
Pro

Marbled Endpaper

backgrounds
Pro
Trending
Pro