# ASCII engine

How the asciicn engine turns a field of tones into real glyphs, the two renderers that draw them, and the registry items you install.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. The asciicn agent skill, for building any ASCII effect on the engine, is at [/.well-known/agent-skills/asciicn/SKILL.md](/.well-known/agent-skills/asciicn/SKILL.md).

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.



```tsx
import { AsciiGrid, mapTones } from "@/lib/ascii";
import { AsciiShader } from "@/lib/ascii/shader";
import { toSvg } from "@/lib/ascii/export";
```

## From tones to glyphs [#from-tones-to-glyphs]

Drawing a frame takes three steps, and every renderer follows them in the same order:

1. **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](/docs/engine/charsets#sub-cell-charsets): 2×2 for quadrants, 2×4 for Braille. Each sample gets a tone and, optionally, a colour.
2. **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 above `threshold`. The `edge` charset runs a Sobel filter and draws `- / | \` along strong edges.
3. **Paint.** Each glyph is drawn in its cell, coloured by the [colour mode](/docs/engine/color-modes): 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](/docs/engine/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 [#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](/docs/engine/write-your-own) 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`.

```tsx
<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 [#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.

```tsx title="examples/engine-pre.tsx"
import { AsciiPre, BOX, eighthsBar, MutableFrame } from "@/lib/ascii";

// A status panel built with MutableFrame and rendered as real text on the
// server. Select it: it copies as text.
const panel = () => {
  const width = 38;
  const frame = new MutableFrame(width, 7, { color: true });
  const box = BOX.rounded;
  const dim: readonly [number, number, number] = [130, 130, 150];
  frame.write(0, 0, `${box.tl}${box.h.repeat(width - 2)}${box.tr}`, dim);
  for (let y = 1; y < 6; y += 1) {
    frame.put(0, y, box.v, dim);
    frame.put(width - 1, y, box.v, dim);
  }
  frame.write(0, 6, `${box.bl}${box.h.repeat(width - 2)}${box.br}`, dim);
  frame.write(2, 0, " ascii-core ", [255, 176, 0]);
  const rows = [
    ["cpu", 0.62, [51, 255, 102]],
    ["gpu", 0.84, [56, 189, 248]],
    ["mem", 0.37, [255, 176, 0]],
  ] as const;
  for (const [index, [label, value, color]] of rows.entries()) {
    frame.write(2, index + 2, label, dim);
    frame.write(7, index + 2, eighthsBar(value, 22), color);
    frame.write(30, index + 2, `${Math.round(value * 100)}%`.padStart(4));
  }
  return frame;
};

export default function EnginePre() {
  return <AsciiPre frame={panel()} cell={16} className="select-text" />;
}
```

```tsx
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 [#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.

```tsx title="examples/engine-fills.tsx"
import { FILL_TEXTURES, fillStyle } from "@/lib/ascii";

// Every fill texture as a CSS mask on a plain <div>: the dense edge at the
// bottom, thinning upwards. No canvas, renders on the server.
export default function EngineFills() {
  return (
    <div className="grid w-full grid-cols-2 gap-4 p-4 sm:grid-cols-4 lg:grid-cols-7">
      {FILL_TEXTURES.map((texture) => (
        <figure key={texture} className="flex flex-col gap-2">
          <div
            className="h-40 w-full rounded-sm"
            style={fillStyle(texture, { cell: 12, length: 12 })}
          />
          <figcaption className="text-muted-foreground font-mono text-xs">
            {texture}
          </figcaption>
        </figure>
      ))}
    </div>
  );
}
```

| 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                                  |

```tsx
<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 [#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                                                                                                                                                     |

```bash
npx shadcn@latest add https://asciicn.dev/r/ascii-core.json
```

Components 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.