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);