Chris Almaguer Data visualization design

Case study - Tooling & type

A camera that can only draw with type

ASCII iOS Camera converts the live camera feed into a grid of glyphs, at thirty frames a second, on the phone. Ten styles, from a braille dot matrix to a one-bit ramp. It is on the App Store, and the two things that decided how it looks were both measurement mistakes.

Mobile application SwiftUI App Store
View on the App Store

Shot on it

6 frames, 10 styles
ASCII iOS Camera output. Michelangelo's David rendered as dense ASCII, the face resolving out of the character grid.
ASCII iOS Camera output. A portrait in the halftone dot ramp, tone carried by dot size rather than by letterforms.
ASCII iOS Camera output. A standing figure in high-contrast glyphs, the background dissolving into sparse punctuation.
ASCII iOS Camera output. A parked car in the line-stroke style, panel edges picked out as continuous strokes.
ASCII iOS Camera output. A row of cars in a dense ramp, reflections reading as brightness steps across the grid.
ASCII iOS Camera output. A close subject in the braille dot-matrix style, which samples two by four per output cell.

01 - The idea

Brightness in, glyphs out

Every frame is sampled down to a grid, each cell reduced to one brightness value, and each value mapped to the glyph that carries about that much ink. That is the entire idea. Everything interesting is in the two decisions it hides: which glyphs, in what order, and how big a cell actually is.

Ten styles ship, and they do not all sample the same way. A ramp style reads one sample per cell. Quadrant blocks read two by two. Braille reads two by four, which is why it holds the most detail and became the launch default. The conversion core is a separate Swift package with no camera or interface code in it at all, so it can be unit tested against known inputs rather than judged by eye.

02 - The first measurement bug

A monospace cell is not as wide as you assume

The grid needs to know how wide a character is relative to its height. I had that as a constant: 0.58. It looked right. It was wrong. SF Mono's real advance ratio is about 0.618, and that six percent difference stretched every single frame horizontally and clipped the right edge of the image.

It survived to a public build because a stretched ASCII portrait still looks like an ASCII portrait. Nothing about it announces the error. The fix was to stop assuming and measure the advance at runtime, per style, since the styles do not all use the same face.

This is the same failure as the color tool on this site, in a different medium: a plausible constant, never checked, quietly wrong in every output it touched.

03 - The second measurement bug

The ramp has to be ordered by ink, not by instinct

A glyph ramp is a scale: the darkest character at one end, the lightest at the other. The punctuation ramp had dense and light glyphs interleaved, so a shadow could render heavier than the midtone beside it. Tone reversed inside its own scale, which is the visual equivalent of a chart axis that runs backwards in the middle.

Two related corrections came with it. Luminance moved to the Rec.709 coefficients, since the earlier ones weight green differently and shift every mapping. And the digits ramp gained a leading blank, because without one a true black rendered as a field of small ones rather than as nothing.

There is an ordered dither underneath all of it, so neighbouring cells at the same brightness do not all snap to the same glyph and band the image.

04 - Impact

What shipping it actually cost

It is approved and on the App Store, which is the part that is easy to say. The work that got it there was mostly unglamorous. Recording was capturing at point resolution instead of pixel resolution, which on a 3x device is nine times fewer pixels than the screen was showing. Encoding sat on the main thread and made the interface stutter while recording, so it moved to its own queue and now drops frames under pressure rather than queueing them.

And saved videos came out upside down, from a missing coordinate flip that had been there since the first native build and shipped in two of them.

The interface ended up monochrome: white text, grey sliders, inverted toggles. The first version had green text and system-blue controls, which competed with an image made entirely of black and white characters. The controls stopped being decoration and went back to being controls.


Color Safety Lens

The same mistake in colour: a plausible constant, never measured, wrong everywhere.

Next