# Export

Save any ASCII frame as plain text, ANSI escape art, vector SVG, high-DPI PNG or video, with ascii-export.

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

`ascii-export` turns frames into files. Every renderer fills a `handle` ref you pass it, and the handle gives you the current frame, its colours and its cell size: everything the exporters need. Try each button on the live scene below.



## Installation [#installation]

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

## Usage [#usage]

```tsx
import { useRef } from "react";
import type { AsciiHandle } from "@/lib/ascii";
import {
  downloadBlob,
  downloadText,
  toAnsi,
  toPng,
  toSvg,
  toText,
} from "@/lib/ascii/export";

const handle = useRef<AsciiHandle | null>(null);

<AsciiImage src="/portrait.jpg" handle={handle} />;

const save = async () => {
  const live = handle.current;
  const frame = live?.capture();
  if (!(live && frame)) return;

  downloadText(toText(frame, { trimEnd: true }), "portrait.txt");
  downloadText(toAnsi(frame), "portrait.ans");
  downloadText(
    toSvg(frame, {
      ink: live.colors().ink,
      metrics: live.metrics() ?? undefined,
    }),
    "portrait.svg",
    "image/svg+xml"
  );
  downloadBlob(await toPng(live, { scale: 3 }), "portrait.png");
};
```

`capture()` returns `null` until the renderer has drawn its first frame. `toText`, `toAnsi` and `toSvg` are pure string builders: they run in scripts, on the server and in tests. The PNG, video and file helpers need a browser.

## Which format [#which-format]

| Format | Function                         | Use it for                                                        |
| ------ | -------------------------------- | ----------------------------------------------------------------- |
| Text   | `toText`                         | READMEs, code comments, terminals, anywhere colour doesn't matter |
| ANSI   | `toAnsi`                         | Colour art for terminals: `cat art.ans`, MOTDs, CLI banners       |
| SVG    | `toSvg`                          | Sharp at any size, editable in Figma, small for mono frames       |
| PNG    | `toPng`                          | Social images and anywhere SVG text isn't supported               |
| Video  | `createRecorder`, `recordCanvas` | Loops of animated scenes for posts and docs                       |

## Text [#text]

`toText(frame, { trimEnd })` joins rows with `\n`. `trimEnd: true` drops trailing spaces from every line, which keeps files small and editors quiet.

## ANSI [#ansi]

`toAnsi` writes SGR escape codes before each run of same-coloured glyphs and a reset at the end of every line, so a terminal that cuts a line short never bleeds colour into your prompt. The output ends with `\x1b[0m`.

```ts
toAnsi(frame); // 24-bit colour: \x1b[38;2;r;g;bm
toAnsi(frame, { mode: "ansi16", ansi: "vga" }); // classic codes 30-37 and 90-97
toAnsi(frame, { ink: "#33ff66", paper: "#020a04", background: true });
```

| Option       | Default        | Meaning                                                   |
| ------------ | -------------- | --------------------------------------------------------- |
| `mode`       | `"truecolor"`  | `truecolor` or `ansi16`                                   |
| `ansi`       | `"vga"`        | `ansi16` table: `vga` or `xterm`                          |
| `ink`        | terminal's own | Colour of glyphs without their own, `#rrggbb` or bytes    |
| `paper`      | none           | Background colour, used with `background`                 |
| `background` | `false`        | Paint `paper` behind every cell                           |
| `trimEnd`    | `false`        | Drop trailing spaces (ignored when painting a background) |

## SVG [#svg]

`toSvg` writes one `<text>` per row, a `<tspan>` per colour run, and a `textLength` on every row. `textLength` pins each row to exact columns, so the art lines up even when the viewer substitutes a font with a different advance. Blocks, shades, Braille, quadrants, dots and box lines are written as vector paths instead of text, one path per colour, using the same geometry the renderers draw. They join without seams and need no font. Pass `shapes: false` to keep them as text.

```ts
const svg = toSvg(frame, {
  metrics: handle.current?.metrics() ?? undefined, // cell width and height; default 6×12
  ink: "#e4e4e7",
  paper: "#09090b", // or null for transparent
  font: "JetBrains Mono, monospace",
});
```

Colours here must be real CSS colours, not variables, because the file leaves the page. `handle.colors()` returns the renderer's resolved ink and paper.

## PNG [#png]

`toPng` takes a canvas, a handle or a frame:

* **A handle without `scale`** saves the live canvas as it is, including any CRT effects.
* **A handle with `scale`** re-renders the captured frame at that many device pixels per CSS pixel, in the handle's colours and font. `scale: 3` gives a crisp image for social cards.
* **A frame** is re-rendered at `scale` (default 2) with the `ink`, `paper`, `cell` and `font` you pass.

`renderFrame(frame, options)` returns the canvas itself if you want to draw on it first. Images are capped at 16,384 pixels on their longest side.

## Video [#video]

`createRecorder(canvas)` gives you `start()` and `stop()` for a record button. `stop()` resolves with the video. `recordCanvas(canvas, { durationMs })` records for a fixed time.

```ts
const recorder = createRecorder(handle.current.canvas, {
  fps: 60,
  onProgress: (ms) => setElapsed(ms),
});
recorder.start();
// later
const video = await recorder.stop();
downloadBlob(video, `scene.${videoExtension(video.type)}`);
```

The recorder picks the first type the browser supports: VP9 WebM, VP8 WebM, then MP4 (Safari). `pickVideoType()` tells you which. The default bitrate of 8 Mbps keeps small glyphs sharp. Keep the renderer animating while it records: a paused canvas records a still, and renderers pause when scrolled offscreen.

## Files and clipboard [#files-and-clipboard]

| Helper                                | Does                                                             |
| ------------------------------------- | ---------------------------------------------------------------- |
| `downloadBlob(blob, filename)`        | Saves a blob through a temporary link                            |
| `downloadText(text, filename, mime?)` | Saves text, UTF-8 plain text by default                          |
| `copyText(text)`                      | Copies to the clipboard; resolves `false` if the browser refuses |

## Static art without exporting [#static-art-without-exporting]

If the art never changes, skip the export step: build a frame with `MutableFrame` or `mapTones` and render it with [`AsciiPre`](/docs/engine/ascii-engine#text-without-a-canvas). It is real text on the server, so search engines and screen readers can read it, and readers can copy it straight from the page.