Every asciicn component draws characters. A picture becomes a grid of glyph cells, each cell gets a tone between 0 (empty) and 1 (densest ink), and the engine picks the glyph that matches that tone. It installs into lib/ascii as plain source you own.
import { AsciiGrid, mapTones } from "@/lib/ascii";
import { AsciiShader } from "@/lib/ascii/shader";
import { toSvg } from "@/lib/ascii/export";From tones to glyphs
Drawing a frame takes three steps, and every renderer follows them in the same order:
- Sample. The box is split into glyph cells, about twice as tall as they are wide. Each cell holds one sample, or a small grid of them for sub-cell charsets: 2×2 for quadrants, 2×4 for Braille. Each sample gets a tone and, optionally, a colour.
- Map. Tones are graded (contrast, brightness, gamma), optionally inverted, then turned into glyphs. A ramp such as
" .:-=+*#%@"picks the level nearest the tone. Braille and quadrants light one dot per sample abovethreshold. Theedgecharset runs a Sobel filter and draws- / | \along strong edges. - Paint. Each glyph is drawn in its cell, coloured by the colour mode: one ink, the source colour, the nearest ANSI colour, or a ramp read by tone.
mapTones in lib/ascii/frame.ts is the reference for step 2. The GPU shader implements the same rules in GLSL, with one difference: it spaces ramp glyphs by the ink they measure in your font, where mapTones spaces them evenly. Pass mapTones the GPU's spacing with positions (see Charsets) and a frame read back from the GPU and one mapped on the CPU agree glyph for glyph.
With few glyphs, a smooth gradient breaks into bands. Set dither to a matrix such as "bayer4" and a tone between two glyphs picks the denser one in a share of the cells that matches how far along it is. Turn on Dither levels in the demo to see the bands soften.
Two renderers
| You want | Use | Drawn with |
|---|---|---|
| A full-bleed animated field, written in GLSL | AsciiShader | WebGL2 and a glyph atlas |
| Anything computed in JavaScript: 3D, sims, media | AsciiGrid | Canvas2D and the same atlas |
| Static art, banners, server-rendered fallbacks | AsciiPre | Real text in a <pre> |
| Glyph-filled buttons, bars and panels | fillStyle, FillPattern | CSS masks and SVG patterns |
AsciiShader runs your field function on the GPU for every sample, every frame. It is the fastest option by far and suits backgrounds and scenes. Write your own field walks through one.
AsciiGrid calls a draw(context) function you write in TypeScript. You fill context.frame with glyphs, by hand (frame.write, frame.put) or by building a tone field and calling mapTones. It paints at device resolution, pauses offscreen and follows the theme. Painting copies each glyph out of the shared glyph atlas and tints the whole frame in one pass, so a truecolor frame with thousands of distinct colours costs the same as a one-colour frame.
Both renderers draw blocks, shades, quadrants, Braille, dots and box lines from the same geometry in lib/ascii/glyphs.ts, snapped to whole device pixels. They never depend on which glyphs your font covers, and neighbouring blocks meet without seams. Letters, digits and katakana come from --ascii-font.
<AsciiGrid
className="h-64"
colored
draw={({ frame, columns, rows, time }) => {
for (let x = 0; x < columns; x += 1) {
const y = Math.round(rows / 2 + Math.sin(x * 0.2 + time) * (rows / 3));
frame.put(x, y, "█", [51, 255, 102]);
}
}}
/>Text without a canvas
AsciiPre renders a frame as real text, with one <span> per colour run. It needs no JavaScript in the browser, so it works in server components, emails and RSS. Readers can select and copy it.
const frame = new MutableFrame(38, 7, { color: true });
frame.write(2, 2, eighthsBar(0.62, 22), [51, 255, 102]);
<AsciiPre frame={frame} cell={16} />;It also takes a plain string: <AsciiPre frame={"+--+\n|hi|\n+--+"} />.
Glyph fills
Interface pieces fill shapes with tiled glyphs instead of flat colour. fillStyle returns inline styles that mask any element to a glyph texture. FillPattern and AsciiPattern are SVG <pattern>s for filling SVG shapes. All of them render on the server and follow the theme through CSS variables.
| Texture | Glyphs | Looks like |
|---|---|---|
shade | █▓▒░· | The classic ANSI gradient, densest at edge |
ascii | #*+=-:. | The same ramp in ASCII |
braille | ⣿ to ⠂ | Fine dots, a light texture for large fills |
blocks | █▛▚▘ | Chunky quadrant steps |
dots | ●•· | Halftone |
hatch | ╱ | Unbroken diagonal lines |
solid | none | Flat colour |
<div className="h-24" style={fillStyle("braille", { color: "green", edge: "left" })} />
<div className="h-6" style={{ background: "var(--ascii-ink)", ...asciiMaskStyle(0.5) }} />Ramp textures fade from from at the edge to to over length cells, or take a level for one even density. The older names gradient, dotted and hatched still work and draw shade, an even field of dots, and hatch. Glyphs in fills are drawn from geometry, because CSS masks can't load your web font. A custom ramp passed as chars with letters falls back to the system monospace font.
BloomBox, BloomGroup and bloomFilter add a phosphor glow in the fill's colour. Its strength follows --ascii-glow, so glows can be strong in dark mode and faint in light mode.
Registry items
| Item | Contains |
|---|---|
ascii-core | Charsets, colour modes, frames and mapTones, cell metrics, glyph geometry and atlas, AsciiGrid, AsciiPre, CrtOverlay, media sampling, glyph fills, theme and motion hooks |
ascii-shader | AsciiShader, the WebGL2 renderer |
ascii-export | .txt, .ans, .svg, .png and video export |
ascii-tokens | The --ascii-* CSS variables |
pnpm dlx shadcn@latest add https://asciicn.dev/r/ascii-core.jsonComponents pull in what they need as registry dependencies, so adding any component installs the engine with it. Components that only draw text or interface pieces never ship WebGL code.