IInbox
Composed

Image to ink

ASCII, ordered dither, halftone and mosaic over one luminance grid — the maths kept pure so it can be tested away from a canvas.

Four conversions from a photograph to ink. The tool is at /lab/imagery; everything that could actually be wrong lives in imagery.ts as pure functions over a luminance grid, so it is tested without a canvas.

Installation

npx shadcn@latest add https://ui.whoisroktim.lol/r/imagery.json

Four things that are easy to get subtly wrong

Each of these produces output that looks plausible when you get it wrong, which is why they are all asserted in tests/dom/imagery.test.ts.

Sampling. sampleGrid averages every pixel in a cell. Point-sampling the centre of a 6px cell throws away thirty-five of every thirty-six pixels and turns fine detail into noise that changes as you resize.

Character aspect. A monospace cell is about twice as tall as it is wide, so ASCII samples on a grid half as tall. Sampling square and rendering into character cells is the classic bug — the result comes out at double height and everyone blames the source image.

Dot area, not dot radius. Ink coverage goes as r², so a halftone dot for a 50% tone needs r = √0.5 / 2, not 0.25. Scaling radius linearly with darkness makes every midtone far too light, and it is the difference between a halftone and a grid of dots.

Ramp order. The ASCII ramp has to be monotonic in ink coverage. A ramp ordered by how busy the glyphs look gives an image with random bright patches.

The same Bayer matrix as everything else

ditherFromGrid thresholds against the canonical Bayer 8×8 — the identical matrix used by the dither transition and the dither shader. An image converted here sits on the same texture as the rest of the system, and the test compares them cell for cell rather than assuming it.

Gamma

Luminance is Rec. 709 computed on gamma-encoded sRGB, not linearised first. That is technically the wrong way round, and it is deliberate: every reference dither and every ASCII ramp in use was tuned against encoded values, so linearising produces output that is more correct in isolation and looks wrong beside everything else.