Compiling to Static
glyphcss’s renderer is pure: rasterize(scene) → string takes geometry +
camera + grid and returns the <pre> text — no DOM, no WebGL. So a scene can be
rendered at build time, on a server, or in a worker, not just in the browser.
Because the render is text, it inlines straight into HTML with zero runtime.
There are two flavors:
- Static — a frozen
<pre>of ASCII. No JavaScript ships at all. - Interactive — a self-contained snippet that ships only the control you declared (orbit / zoom / pan / fpv) plus a decimated mesh.
Static compile
Section titled “Static compile”Everything is built on one pure function, compileScene, which reproduces
createGlyphScene’s exact render (same defaults) without a DOM:
import { compileScene } from "glyphcss";
const { html, inner, cols, rows } = compileScene({ polygons, // a Polygon[] cols: 80, rows: 24, // library defaults // camera, mode, glyphPalette, useColors, lights… (same options as createGlyphScene)});// html === '<pre class="glyph-output">…ascii…</pre>'The output is byte-identical to the runtime render for the same inputs.
Vite plugin
Section titled “Vite plugin”The @glyphcss/compile package ships a Vite plugin that compiles a mesh import
to its baked <pre> at build time:
import { glyphcssCompile } from "@glyphcss/compile/vite";
export default { plugins: [glyphcssCompile()] };import dog from "./dog.glb?glyph&autoCenter=1&rotX=60&rotY=45&cols=80&rows=30";
document.querySelector("#app")!.innerHTML = dog; // the <pre> string — no runtimeQuery params map to the options below. Works in any Vite pipeline — Astro, vanilla Vite, or Vite-React (import the string and inject it).
In Astro
Section titled “In Astro”Astro is built on Vite, so the plugin works once you register it:
import { defineConfig } from "astro/config";import { glyphcssCompile } from "@glyphcss/compile/vite";
export default defineConfig({ vite: { plugins: [glyphcssCompile()] } });---import dog from "../models/dog.glb?glyph&autoCenter=1&rotX=60";---<Fragment set:html={dog} />But the most idiomatic Astro path needs no plugin at all — Astro frontmatter
runs in Node at build, so compileFile works there directly:
---import { compileFile } from "@glyphcss/compile";import { fileURLToPath } from "node:url";
const { html } = await compileFile( fileURLToPath(new URL("../models/dog.glb", import.meta.url)), { autoCenter: true, rotX: 60, rotY: 45 },);---<Fragment set:html={html} />Both run at build and emit the <pre> into the page HTML with zero JS — the
Astro equivalent of GlyphSceneStatic. A dedicated Astro integration isn’t
needed; it would only add sugar (auto-registering the plugin and a typed
<GlyphScene src="…" /> component).
The universal escape hatch — works in any pipeline (Hugo, Eleventy, CI, a Makefile):
Install it (npm i -g @glyphcss/compile → the glyphcss command, or npx @glyphcss/compile …), then:
glyphcss cube --auto-center # a primitive shape → color ASCIIglyphcss dog.glb --auto-center # a mesh file → ANSI color in the terminalglyphcss --polygons-json '[{"vertices":[[0,0,0],[2,0,0],[1,2,0]],"color":"#f00"}]'glyphcss dog.glb -f full -o dog.html # full HTML documentInput is a mesh file (.obj/.glb/.gltf/.vox/.stl), a primitive shape
name (cube, sphere, icosahedron, torus, cone, … — 44 shapes), or
custom polygons (--polygons FILE.json / --polygons-json '…').
Output -f, --format picks the encoding: ansi (truecolor terminal), text
(plain), html (a <pre>), or full (HTML doc). The default depends on the
destination — terminal → ansi, -o file → html, piped → text. With no
--cols/--rows it auto-fits the grid + zoom to the content, cropped tight
(give just one and the other adapts to show the whole model).
Use it from a coding agent
Section titled “Use it from a coding agent”glyphcss ships a skill so Claude Code, Cursor, Codex, and other AI coding
agents can drive this CLI — ask “render a cube in the terminal” and the agent
runs glyphcss cube --auto-center. See Coding agents
for setup.
Node API
Section titled “Node API”import { compileFile, loadMeshFromFile } from "@glyphcss/compile";
const { html, cols, rows } = await compileFile("dog.glb", { autoCenter: true });compileFile loads the mesh from disk (reusing the library’s loadMesh — format
dispatch, sibling .mtl, optimization) and hands it to compileScene.
SSR / SSG components
Section titled “SSR / SSG components”React and Vue both export GlyphSceneStatic, which calls compileScene at render
time and outputs the <pre>:
import { GlyphSceneStatic } from "@glyphcss/react"; // or @glyphcss/vue
<GlyphSceneStatic polygons={polygons} cols={80} rows={24} autoCenter rotX={60} rotY={45} />Where it renders decides whether it’s static. Because compileScene runs at
render time, you only get a zero-runtime static <pre> when the component is
rendered at build or on the server:
- ✅ SSG / SSR — an Astro island without a
client:directive, a Next.js Server Component (orgetStaticProps+renderToString),vite-ssg, Gatsby, Remix,renderToStaticMarkup. The<pre>is baked into the HTML and, if not hydrated, no glyphcss JS ships. - ❌ Plain client-side React (a default CRA / Vite
npm run build) renders in the browser —compileSceneexecutes client-side and glyphcss is in the bundle. You still get the<pre>with no interactive machinery, but it isn’t “compiled to static.” For that, use the Vite plugin instead, which bakes the string at build in any Vite app.
Two caveats for true static output: the polygons must be available where it
renders (load them at build with loadMeshFromFile and pass them in — loading via
loadMesh in the browser reintroduces a runtime), and the island must not be
hydrated. For an interactive scene, use GlyphScene.
Options
Section titled “Options”Defaults are the library defaults (createGlyphScene). A loaded mesh is not
recentered or auto-fit unless you ask — pass autoCenter + a camera to frame it.
| Option | Query / CLI flag | Default |
|---|---|---|
| Camera angle | rotX rotY / --rot-x --rot-y | 65 / 45 |
| Zoom | zoom / --zoom | 0.65 |
| Projection | projection=orthographic / --ortho | perspective |
| Grid | cols rows cellAspect / --cols … | 80 / 24 / 2.0 |
| Render mode | mode / --mode | solid |
| Palette | palette / --palette | default |
| Colors | colors=0 / --no-colors | on |
| Recenter mesh | autoCenter=1 / --auto-center | off |
| Mesh optimize | meshResolution / --mesh-resolution | lossy |
| Smooth shading | smoothShading creaseAngle / --smooth | off / 60 |
| Back faces | doubleSided / --double-sided | culled |
| Supersample | supersample / --supersample | 1 |
| Semantic output | glyphOutput / --glyph-output (with sceneManifest + dictionary) | visible |
charMode, hiddenLines, solidWeightRamp, and colorTolerance are pure
functions of geometry + camera, so a static bake reproduces them exactly.
colorTolerance is the odd one out mechanically — it operates on the
already-rasterized cell grid as a post-hoc span merge, while the other three
change rasterization itself — but all four are equally reproducible at build
time. compileScene and GlyphSceneStatic (React + Vue) accept all four.
@glyphcss/compile’s CLI, Vite plugin and compileFile do not accept
them yet; call compileScene directly if you need them in those pipelines.
buildGlyphFramesExport, buildGlyphInteractiveExport, and
buildGlyphFieldSynthStaticExport (@glyphcss/effects) accept none of the
four either — a colorTolerance value set through the /synth page’s UI (it
persists to the URL) is silently dropped by that page’s CodePen export.
wireframeJunctions and per-mesh density/transparent are runtime-only
in every path — the static bake takes a flat polygon list and cannot represent
per-mesh detail layers.
Textures: per-cell texture sampling needs browser image decoding, so the static compile renders from material / vertex colors.
Interactive export
Section titled “Interactive export”The static path produces a frozen frame. To ship interaction, declare which
interactions you want and buildGlyphInteractiveExport (pure, browser-safe)
produces a self-contained snippet — glyphcss from a CDN + the mesh inlined:
import { buildGlyphInteractiveExport, glyphCodepenPrefill } from "glyphcss";
const { html, pen, polygonCount } = buildGlyphInteractiveExport(polygons, { interactions: ["orbit", "zoom"], autoCenter: true,});// pen → { html, css, js } ready for a CodePen prefill (glyphCodepenPrefill)An optional effect: { id, params, blend?, timeScale? } mounts a live stock
@glyphcss/effects layer in the snippet: it adds a second CDN import, resolves
the effect by id at runtime, and — when timeScale > 0 — appends a small
requestAnimationFrame loop driving params.time. Only stock effects work here;
a custom defineGlyphEffect cannot cross the CDN boundary. glyphcss itself
never imports @glyphcss/effects, so that dependency only points one way.
The capability manifest (interactions) drives two things:
- Runtime — only the declared control is imported, so the snippet tree-shakes. An orbit-only export ships less than an fpv one.
- Mesh —
decimatePolygonssimplifies the geometry to a budget tied to the interaction (coarse for orbit; finer when zoom / fpv let the camera approach). The ASCII grid is coarse, so sub-cell geometry is invisible anyway.
interactions | Ships |
|---|---|
[] | static scene, no control |
["orbit"] | orbit control, coarse mesh |
["orbit","zoom"] | + wheel zoom, finer mesh |
["pan","zoom"] | map controls |
["fpv"] | first-person controls, finest mesh |
The CLI exposes the same via --interactions orbit,zoom.
Static encoding
Section titled “Static encoding”When emitting a fully static <pre>, encodeStaticGlyphHtml(inner, mode) chooses
how colors and whitespace are encoded — the best choice is model-dependent:
| Mode | What it does |
|---|---|
classes (default) | dedupes colors into .cN{…} rules + class spans, trims trailing spaces — smallest for typical dense, many-color renders |
grid | CSS-grid-places each run by column/row — no literal spaces in the markup; wins for sparse renders with big gaps |
inline | one style="color:…" per run — simplest |
Baked frames (rotate, no mesh)
Section titled “Baked frames (rotate, no mesh)”For orbit-class motion you can do better than shipping a mesh + runtime: pre-bake a turntable of frames and cycle them with pure CSS — no mesh, no glyphcss, no JS at all.
import { buildGlyphFramesExport } from "glyphcss";
const { html, pen } = buildGlyphFramesExport(polygons, { frameCount: 36, // 10° steps over 360° autoCenter: true, rotX: 65, cols: 120, rows: 48, // fixed grid; frames stack into one <pre>});// pen.css holds a `@keyframes … steps(36)` loop; pen.js is emptyEach frame is a faithful compileScene render (colors baked per face for textured
meshes). The gallery’s CodePen export exposes this as Static → Rotate.
Trade-offs: discrete angles (smoothness ∝ frameCount), fixed resolution, and
payload = frameCount × frame. It only covers orbit (1 axis) — pan / fpv have too
many camera states to bake, so those still use the interactive
mesh + runtime path.
Effect-only static export (field synth)
Section titled “Effect-only static export (field synth)”For a scene whose camera and mesh are fixed and where only a field-synth
texture animates, buildGlyphFieldSynthStaticExport bakes the static base grid
plus each covered cell’s resolved domain coordinate once, then ships a tiny
hand-written field-synth evaluator that recomputes the pattern every frame —
zero imports, zero CDN, no glyphcss at runtime.
import { buildGlyphFieldSynthStaticExport } from "@glyphcss/effects";
const { html, pen } = buildGlyphFieldSynthStaticExport(polygons, { params, // the live field-synth patch blend, // the layer's REAL blend, not the definition's defaultBlend loopSeconds: 8, cols: 120, rows: 40,});Payload is fixed regardless of loop length and the motion is continuously
smooth — unlike a frame roll, whose payload grows with frameCount. Two bakes
shrink it further for the common flat-surface case: when every covered cell’s
domain coordinate fits an affine function of (col, row), the per-cell
coordinate table (most of the payload) collapses to six fitted scalars; and
when the effect covers every cell with blend: "replace" at opacity 1, the
base grid is skipped too. A curved or partially covered surface keeps the full
table.
Field synth only: another effect id would need its own exported coordinate
resolver plus a hand-written port of its per-cell math, since there is no way to
ship an arbitrary GlyphEffectProgram.evaluate() without shipping the effect
runtime with it.
This exporter ports field synth’s 2D machinery — layers, duty, phase,
per-voice angleN/originUN/originVN, subcellRes: "2x4"/"ink", and the
SDF fields and step wave
(gyroid/menger/sierpinski, at real-renderer exact parity) — but rejects
the volumetric branch and carve/xray
explicitly (space: "object", render: "carve"/"xray", or an active voice
using linearZ/a nonzero originWN): a per-cell-per-frame march is a
different export design this affine-fit/coordinate-table bake can’t fake. It
also rejects a program option
(program-as-data) up
front, before the flat-param merge/bake — an unbounded field has no schema
this bake can serialize. The originWN reject is waived for an active SDF
voice, since that family reads originW even in the 2D branch. Check
isGlyphFieldSynthStaticExportSupported(params) before calling; a
volumetric or carve/xray patch (with no program) exports through the
interactive export instead, which ships and evaluates
the live effect runtime rather than a baked approximation.