For the complete documentation index, see 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.

Export

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

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

pnpm dlx shadcn@latest add https://asciicn.dev/r/ascii-export.json

Usage

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

FormatFunctionUse it for
TexttoTextREADMEs, code comments, terminals, anywhere colour doesn't matter
ANSItoAnsiColour art for terminals: cat art.ans, MOTDs, CLI banners
SVGtoSvgSharp at any size, editable in Figma, small for mono frames
PNGtoPngSocial images and anywhere SVG text isn't supported
VideocreateRecorder, recordCanvasLoops of animated scenes for posts and docs

Text

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

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.

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 });
OptionDefaultMeaning
mode"truecolor"truecolor or ansi16
ansi"vga"ansi16 table: vga or xterm
inkterminal's ownColour of glyphs without their own, #rrggbb or bytes
papernoneBackground colour, used with background
backgroundfalsePaint paper behind every cell
trimEndfalseDrop trailing spaces (ignored when painting a background)

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.

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

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

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

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

HelperDoes
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

If the art never changes, skip the export step: build a frame with MutableFrame or mapTones and render it with AsciiPre. 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.