Installation
pnpm dlx shadcn@latest add https://asciicn.dev/r/ascii-portrait.jsonUsage
import { AsciiPortrait } from "@/components/asciicn/ascii-portrait";
export function TeamCard() {
return (
<AsciiPortrait
src="/team/ada.jpg"
alt="Ada, our head of research, smiling at the camera"
className="aspect-[4/5] w-72"
/>
);
}AsciiPortrait is AsciiImage set up for faces. It samples the photo the same way, then shapes the tones before glyphs are chosen:
- A grade of
contrast: 1.2andgamma: 0.9separates skin from shadow without crushing either. detailsharpens each sample against the cell around it, so eyes, lips and the line of the nose survive the cell grid.- With
edgeson, a Sobel filter finds the strongest contours and thins them to one sample wide. In Braille and quadrants they light dots on top of the tones. In a ramp charset such asstandardthey become- / | \strokes.
Braille is the default because it draws 2×4 dots per cell. That is eight times the detail of a ramp at the same cell size, which matters most on a face.
On mount the portrait develops: the contours come in first, like a sketch, and the tones rise through them over developDuration. Hover the portrait to see the photo left of the pointer. With compare="drag" the split stays where it is dropped, which suits touch screens.
Examples
Colour, with a split you drag
colorMode="truecolor" takes each cell's colour from the photo. compare="drag" starts the split at the middle.
Strokes and phosphor
On the left, the classic ramp with contour strokes drawn as - / | \ glyphs. On the right, Braille on the P3 amber phosphor.
Props
| Prop | Type | Default |
|---|---|---|
| src Photo URL. Files on another origin need CORS headers, or the canvas can't read their pixels. | Type: string | Default – |
| alt Who the portrait shows. Read out by screen readers. Pass "" for a decorative portrait. | Type: string | Default – |
| charset Which glyphs draw the tones. | Type: "standard" | "dense" | "blocks" | "quadrants" | "braille" | "cp437" | "petscii" | "katakana" | "dots" | "edge" | Default "braille" |
| chars A custom ramp from empty to dense, such as " .oO@". Wins over charset. | Type: string | Default – |
| colorMode mono draws in one ink. truecolor takes each cell's colour from the photo. duotone reads tones through ramp. | Type: "mono" | "truecolor" | "ansi16" | "duotone" | "palette" | Default "mono" |
| phosphor With mono: a CRT ink and its dark glass. | Type: "p1" | "p3" | "ibm" | "blueprint" | Default – |
| ramp With duotone: a named ramp or your own stops, dark to light. | Type: RampName | string[] | Default "mainframe" |
| ansi With ansi16: which 16-colour table. | Type: "vga" | "xterm" | Default "vga" |
| grade Tone and colour grading before glyphs are chosen. Passing your own replaces the portrait grade. | Type: { brightness?, contrast?, gamma?, saturation? } | Default { contrast: 1.2, gamma: 0.9 } |
| detail How much to sharpen features before glyphs are chosen, 0 to 1. | Type: number | Default 0.5 |
| edges Draw contour strokes over the tones: lit dots in Braille and quadrants, - / | \ glyphs in a ramp. | Type: boolean | Default true |
| edgeThreshold How strong a contour must be to draw a stroke, 0 to 1. Raise it for fewer, bolder lines. | Type: number | Default 0.16; 0.22 for edge; 0.3 for ramps |
| develop Develop from outline to full tone when the photo loads. | Type: boolean | Default true |
| developDuration Length of the develop reveal in milliseconds. | Type: number | Default 2600 |
| compare hover shows the photo left of the pointer. drag keeps the split where it is dropped. none turns it off. | Type: "hover" | "drag" | "none" | Default "hover" |
| invert true: dark pixels draw dense glyphs. false: bright pixels do. Unset, it follows the page. | Type: boolean | Default follows the theme |
| threshold Braille and quadrants: the tone that lights a dot. | Type: number | Default 0.5 |
| dither Ordered dithering between glyph levels and dots. | Type: "none" | "bayer2" | "bayer4" | "bayer8" | "blue-noise" | Default "blue-noise" for Braille and quadrants, else "none" |
| background transparent draws glyphs only, over whatever is behind. | Type: "paper" | "transparent" | Default "paper" |
| fit How the photo fills its box, as in object-fit. | Type: "cover" | "contain" | "fill" | Default "cover" |
| focus Where the photo sits when it overflows, 0 to 1 per axis. The default keeps the eyes in frame. | Type: [x, y] | Default [0.5, 0.3] |
| autoLevels Stretch the photo's tones to use the whole ramp. | Type: boolean | Default true |
| cell Glyph cell height in CSS pixels. | Type: number | Default 8 |
| animate false skips the develop reveal. | Type: boolean | Default true |
| crossOrigin Passed to the image request. | Type: "anonymous" | "use-credentials" | Default "anonymous" |
| handle Filled with a handle for export: the current frame as text, ANSI, SVG or PNG. | Type: RefObject<AsciiHandle | null> | Default – |
Notes
- The portrait is announced as an image labelled with
alt. The split handle is a slider: focus it with Tab, then move it with the arrow keys, or Home and End. Shift moves it in bigger steps. - With reduced motion, or
animate={false}, the portrait appears fully developed. The split still works. - The develop reveal is the only motion. Once it ends the portrait draws nothing until a prop or the box size changes.
- Sharpening and contour finding run on the CPU once per frame, over the sample grid. A 420 px portrait in Braille at
cell={8}is about 100,000 samples, which takes a few milliseconds. - Give the box a size, such as
aspect-[4/5] w-72. The photo fills it and never sets its own height. - The sample photos are public domain: Abraham Lincoln by Alexander Gardner (1863) and Sally Ride's 1984 NASA portrait, both from Wikimedia Commons.