From the web

Dither

Images rebuilt from ordered dots, like old print and 1-bit screens.

OGL · GLSL

A threshold matrix turns continuous tone into pure black and white. Done in a shader it runs live, and the pointer reveals the original underneath.

Each cell of the screen samples the image once at its centre and turns that brightness into 1 bit by comparing it with a threshold from a Bayer matrix. The matrix value is computed from interleaved bits of the cell coordinates, so no lookup table is needed. Halftone swaps the matrix for a 45 degree dot grid whose radius follows brightness. The still image only renders when the pointer, a parameter or the size changes.

// Bayer threshold in (0,1) for a 2^bits square matrix, from bit interleaving.
float bayer(ivec2 cell, int bits) {
  int n = 1 << bits;
  int x = cell.x & (n - 1);
  int y = (cell.x ^ cell.y) & (n - 1);
  int v = 0;
  for (int i = 0; i < 3; i++) {
    if (i >= bits) break;
    v = v * 4 + (((y >> i) & 1) * 2 + ((x >> i) & 1));
  }
  return (float(v) + 0.5) / float(n * n);
}

// ordered dither: one sample per cell, compared against the matrix
vec2 cellId = floor(frag / cellPx);
vec2 centre = (cellId + 0.5) * cellPx;
float l = luma(sampleImage(centre / uRes, cellPx));
float t = uPattern > 0.5 ? bayer(ivec2(cellId), 3) : bayer(ivec2(cellId), 2);
float on = step(t, l + (0.5 - uThreshold));

// pointer reveal: soft circle blending toward the original colours
float m = 1.0 - smoothstep(uReveal * 0.55, uReveal, distance(frag, uPointer));
fragColor = vec4(mix(mix(BG, INK, on), full, m), 1.0);