# ASCII canvas

A glyph pad you paint with the pointer: ink spreads, curls and fades into glowing trails, and clicks send ripples that bend them.

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

> **This is an example, not the only option.** It is one component built on the asciicn engine. Install it when it is what you were asked for. For any other look, don't stretch its props to fit: write your own field on `AsciiShader` ([Write your own](/docs/engine/write-your-own.md)), or draw a CPU grid with `AsciiGrid`, and read this example's source for technique. Its full source is in [https://asciicn.dev/r/ascii-canvas.json](https://asciicn.dev/r/ascii-canvas.json), under `files[].content`.

## Installation [#installation]

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

## Usage [#usage]

```tsx
import { AsciiCanvas } from "@/components/asciicn/ascii-canvas";

export function Playground() {
  return <AsciiCanvas brushSize={2} decay={2.5} className="h-96 w-full" />;
}
```

The pad keeps a grid of ink, one value per sample. Every 60th of a second the ink fades, spreads to its neighbours and drifts on a slow, curling flow. The tones go through the charset like any other field, so the brightest ink draws the densest glyph and a fading tail thins out to dots.

Dragging lays ink along the path. A mouse also paints faintly when it just moves over the pad, unless motion is off. A click or tap drops a ripple: a ring that spreads across a water surface over the ink, bends the trails as it passes and glints on its crest.

Each stroke takes the next colour from `inks`. Where strokes overlap, their colours mix.

When nobody has touched the pad for four seconds, a slow ghost brush draws on its own, and drops a ripple every few seconds. Turn it off with `idle={false}`. The pad always opens with a short stroke already on it, so it is never blank.

## Examples [#examples]

### Colours in Braille [#colours-in-braille]

Five series colours, one per stroke, on 2×4 Braille dots. The trails fade slowly and curl more.

```tsx title="examples/ascii-canvas-spectrum.tsx"
import { AsciiCanvas } from "@/components/asciicn/ascii-canvas";

export default function AsciiCanvasSpectrum() {
  return (
    <AsciiCanvas
      charset="braille"
      inks={["blue", "purple", "pink", "orange", "green"]}
      brushSize={1.2}
      decay={3.2}
      swirl={0.6}
      cell={10}
      className="h-80 w-full max-w-2xl"
    />
  );
}
```

### Behind a heading [#behind-a-heading]

A custom ramp under a heading, with no hint and no clear button. The text lets pointer events through, so the pad under it still paints.

```tsx title="examples/ascii-canvas-hero.tsx"
import { AsciiCanvas } from "@/components/asciicn/ascii-canvas";

export default function AsciiCanvasHero() {
  return (
    <section className="relative h-96 w-full max-w-3xl overflow-hidden border border-border">
      <AsciiCanvas
        chars=" .·:+x*X#"
        decay={1.2}
        clearButton={false}
        hint={false}
        aria-label="Decorative glyph pad behind the heading. Drag to paint."
        className="absolute inset-0"
      />
      {/* The copy lets pointer events through, so the pad under it still paints. */}
      <div className="pointer-events-none relative flex h-full flex-col items-start justify-end gap-2 p-8">
        <p className="font-mono text-muted-foreground text-xs">
          release 4.2 · out now
        </p>
        <h2 className="max-w-md text-balance font-semibold text-3xl text-foreground tracking-tight">
          Leave a mark on the page.
        </h2>
      </div>
    </section>
  );
}
```

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `charset` | `"standard" \| "dense" \| "blocks" \| "quadrants" \| "braille" \| "cp437" \| "petscii" \| "katakana" \| "dots" \| "edge"` | `"standard"` | Which glyphs draw the ink. Braille and quadrants paint finer trails. |
| `chars` | `string` |  | A custom ramp from empty to dense, such as " .:+#". Wins over charset. |
| `brushSize` | `number` | `1.25` | Brush radius in cells. |
| `decay` | `number` | `1.8` | Seconds for a trail to fade to about a third of its brightness. |
| `diffusion` | `number` | `0.5` | How fast ink spreads into its neighbours, 0 to 1. |
| `swirl` | `number` | `0.35` | How much trails curl and drift as they fade, 0 to 1. |
| `ripples` | `boolean` | `true` | Clicks, taps and the space key send ripples. |
| `idle` | `boolean` | `true` | A ghost brush paints after four seconds without input. |
| `inks` | `AsciiColor[]` | `["--ascii-ink"]` | Ink colours, as palette names ("blue"), CSS variables or CSS colours. Each new stroke takes the next one. |
| `clearButton` | `boolean` | `true` | Show a clear button in the bottom-right corner. |
| `hint` | `string \| false` | `"drag to paint · click to ripple"` | A short how-to in the top-left corner, shown until the first stroke. false hides it. |
| `background` | `"paper" \| "transparent"` | `"paper"` | transparent draws glyphs only, over whatever is behind. |
| `cell` | `number` | `12` | Glyph cell height in CSS pixels. |
| `speed` | `number` | `1` | Simulation speed. 0.5 makes everything fade and spread at half speed. |
| `animate` | `boolean` | `true` | false freezes fading, spreading, ripples and the idle brush. Painting still works. |
| `aria-label` | `string` | `describes the pad and its keys` | What screen readers announce when the pad has focus. |
| `handle` | `RefObject<AsciiHandle \| null>` |  | Filled with a handle for export: the current frame as text, ANSI, SVG or PNG. |

## Notes [#notes]

* The pad takes focus with Tab. The arrow keys paint from the centre, Shift paints in longer steps, and Space or Enter sends a ripple. The clear button is a real button.
* Touch paints too. The pad sets `touch-action: none`, so dragging on it doesn't scroll the page; scroll from outside it.
* With reduced motion, or `animate={false}`, painting still works, but ink stays where you put it: no fading, spreading, ripples or ghost brush. Clear empties it at once.
* The loop runs at up to 60 fps and pauses offscreen and in hidden tabs. The idle brush only draws while the pad is on screen.
* The simulation runs on the CPU over the sample grid. A 700 × 400 pad is about 3,000 samples in the classic charset and 25,000 in Braille, well within a frame.
* Resizing the pad, or switching between charsets with different sample grids, starts a fresh pad.