The portrait on this site's homepage is not an image. It is a few thousand characters in a <pre> tag, computed in your browser from the actual photo. Here is the whole trick.
Luminance to characters
Every ASCII converter is the same three steps:
- Downscale the image to a tiny grid (one pixel per character cell).
- Compute perceived brightness per pixel:
0.2126R + 0.7152G + 0.0722B. - Map brightness to a character ramp, dark to dense:
" .:-=+*#%@".
On a dark background you invert the ramp — bright pixels get dense characters, because dense characters put more lit-up pixels on screen.
The aspect ratio trap
Monospace characters are roughly twice as tall as they are wide. Sample the image on a square grid and your output looks vertically stretched, like everyone's first attempt. Fix: sample half as many rows as the square grid would give you.
const cols = 96;
const rows = Math.round((img.height / img.width) * cols * 0.5);
Making it move
A static ASCII portrait is a gimmick. An animated one is a statement. On mount, every cell starts as a random glyph and resolves to its final character over about a second — each cell gets a random resolve time, so the image condenses out of noise. It costs one requestAnimationFrame loop and rerendering a string. Text is cheap; that's the point of ASCII.
Respect the reader
Wrap the whole thing in a prefers-reduced-motion check and render the final frame immediately for anyone who asked for less motion. Terminal cosplay is never an excuse for a hostile page.