SIGNAL · CREATIVE

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.

4 MIN

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 * 4 to get the red value (green/blue are +1/+2), after calling loadPixels() [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 CSS line-height to 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, call loadPixels() [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, &nbsp; for spaces, tune CSS line-height [16:42]
  • 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]

navigate · open · esc close