@ascii-fx/compiler
v0.9.1
Published
Build-time compiler for ASCII FX: deterministic font rasterization, glyph atlases, .asciip/.asciif generation.
Maintainers
Readme
@ascii-fx/compiler
Build-time only: turns a font into a glyph profile the matcher can use, and an image into a precompiled frame. Node, never the browser.
pnpm add -D @ascii-fx/compilerMost people never import this directly — @ascii-fx/vite wraps it as a plugin, and the runtime packages can compile a profile themselves at startup. Reach for it when you want to compile ahead of time, or in a build that isn't Vite.
import { readFile } from 'node:fs/promises'
import { buildProfile } from '@ascii-fx/compiler'
const font = new Uint8Array(await readFile('GeistMono-Regular.ttf'))
const { profile, binary } = buildProfile({ font, characters: ' .:-=+*#%@' })
await writeFile('public/default.asciip', binary)Deterministic on purpose
Rasterization is an in-house fixed-point scanline rasterizer over fontkit's glyph outlines — no system rasterizer anywhere in the path. The same font and the same options produce byte-identical .asciip output on macOS, Linux, and CI.
Glyphs that are one code point the font maps rasterize straight from that glyph's outline. Anything else (a conjunct, a base letter with marks, an Arabic presentation form the font leaves unmapped) is shaped by fontkit first and the shaped run rasterized as one outline, so those profiles depend on fontkit's shaping as well as on the font. Golden fixtures built from committed Noto subsets pin every shaped Arabic, Devanagari, Thai and combining-mark glyph, so a fontkit update that moves one fails CI and names the glyph.
That matters because a profile's fingerprint is what a .asciif frame is validated against. If rasterization drifted between machines, frames compiled on your laptop would be refused by the same code running in CI. Golden-file tests pin it.
Colour glyphs
buildChromaticProfile builds from decoded colour images rather than a font, carrying an RGBA atlas alongside the coverage plane, so the profile still works with the mask-fitting matchers:
import { buildChromaticProfile } from '@ascii-fx/compiler'
const { binary } = buildChromaticProfile({ glyphs: [{ char: '🌊', image }, ...] })Static frames
buildFrame matches an image at build time and emits .asciif, so the client ships glyph ids instead of doing any matching:
import { buildFrame, decodePng } from '@ascii-fx/compiler'
const { binary } = buildFrame({ profile, image: decodePng(png), columns: 160 })A frame records the fingerprint of the profile it was built against and refuses to decode against a different one — a stale .asciif fails loudly rather than rendering garbage.
CLI
npx ascii-fx profile build --font GeistMono-Regular.ttf --out default.asciip
npx ascii-fx profile build --font "/System/Library/Fonts/GeezaPro.ttc" --face 0 --charset arabic-joined --out arabic.asciip
npx ascii-fx frame build --image hero.png --profile default.asciip --columns 160 --out hero.asciif
npx ascii-fx inspect default.asciip