ASCII Text Images in p5.js
ASCII art is just a lookup: read each pixel's brightness (0–255), map it to an index in a character string ordered by density, and print that character.
TLDR
- ASCII art is just a lookup: read each pixel’s brightness (0–255), map it to an index in a character string ordered by density, and print that character.
- Use a fixed-width font (Courier) and render to DOM text (not canvas) so the output is copy-pasteable and the row/character spacing is tunable via CSS line-height.
- The static-image version becomes real-time video by swapping the image source for a video capture and putting the pixel loop back inside
draw().
Caveman
Map pixel brightness to letters. Picture becomes text.
ELI5
Every dot in a picture has a brightness. You line up characters from “takes up almost no ink” (a space) to “very dense” (like @ or B), then swap each dot for the character whose density matches its brightness. Print them in a grid and the picture reappears, made of text.
Trunk → Branches
Trunk: Translate each pixel’s brightness into a character whose visual density matches, and the arrangement of characters reproduces the image. Branches:
- Brightness per pixel = average the R, G, B values (0–255); fancier grayscale formulas exist but the average is good enough for this effect [2:16, 2:28].
- Keep a “density” string of characters ordered brightest→darkest (or reverse); its ordering is the entire mapping — the character index is where a pixel’s brightness lands in the string [1:21].
- Walk the pixel array using
col + row * width * 4to get the red value (green/blue are +1/+2), after callingloadPixels()[7:58, 9:08]. - Map brightness (0–255) onto the string length, then
floor()it to a valid integer index; if the picture looks inverted, reverse the mapping direction [11:20, 11:43, 12:34]. - Switch from canvas to DOM (
noCanvas, drop the draw loop for a static image) so the art is real HTML text you can copy-paste elsewhere [13:26, 13:33]. - Fixed problems: use a fixed-width font so columns align [15:56]; render spaces as the
HTML entity or leading spaces collapse [16:42]; use CSSline-heightto pull rows closer since characters are taller than wide [17:20]. - Go real-time: replace the image with a video capture, resize small (e.g. 48×48), restore
draw(), and update ONE div’s text (with<br>between rows) each frame instead of creating new divs [18:34, 19:06, 19:40].
Key Patterns
- Pattern: Quantize a continuous signal into an ordered lookup table (brightness → density-ranked characters) works because you collapse an infinite range into a small indexed set — transfers to color palettes, sentiment buckets, difficulty tiers, any “map a number to a label” UI.
- Pattern: Prototype on the easy case, then generalize by swapping the source (static image → video capture, same pixel loop) works because the transform is decoupled from the input — transfers to building any pipeline against a fixture before wiring the live feed.
- Pattern: Mutate one persistent DOM node per frame instead of creating nodes works because it avoids unbounded growth and reflow churn — transfers to any render loop touching the DOM (canvas overlays, live dashboards).
Stress Test
- Hidden assumption: A monospaced font and near-square pixel-to-character cells; with a proportional font or default line spacing the image smears and columns drift.
- Best counterargument: Averaging RGB is a naive luminance measure — perceptual weighting (≈0.21R + 0.72G + 0.07B) gives noticeably better contrast, and the author admits the average is a shortcut.
- Fails when: Input resolution is too high (one character per pixel makes a wall of text) or too low (unrecognizable); the effect only reads at a tuned, deliberately small resolution.
- Fact vs opinion: “The average is good enough” is the author’s aesthetic judgment for this demo, not a rule — for photographic fidelity a weighted luminance is measurably better. “DOM vs canvas both have value depending on application” is opinion offered as guidance.
In Practice
- Build the image-to-ASCII sketch end to end — [1:39]
- Load a small image (e.g. 20×20) in
preload, callloadPixels()[9:08] - Per pixel: index
col + row*width*4, average R+G+B for brightness [7:58] floor(map(brightness, 0, 255, 0, len))into a density string; reverse if inverted [11:20]- Switch to DOM:
noCanvas, Courier font, for spaces, tune CSS line-height [16:42]
- Load a small image (e.g. 20×20) in
- Swap the still image for a video/webcam capture, restore
draw(), and update one div’s text per frame to get live ASCII video — [18:34] - Compare naive RGB-average brightness against weighted luminance (0.21R+0.72G+0.07B) on the same image to see how much contrast improves — [2:28]
- Tune the effect by editing only the density string (more spaces = thresholding to brightest pixels) and by using a non-square resolution to keep aspect ratio — [20:21]