Core Concepts
The strip flipbook
Section titled “The strip flipbook”glyphcss does not redraw the scene every frame. At init, the renderer projects every
vertex through the camera N times (N = 60 by default), Bresenham-rasterizes each
frame into a Uint8Array stamp, and concatenates the N frames into one <pre>
separated by \n. CSS then:
- Clips the viewport to exactly
rows × cell-heightpixels (one frame’s worth). - Animates the strip’s
transform: translate3d(0, -i × frame-height, 0)withsteps(N, end)over--durseconds.
The visible frame steps through 0..N-1 cleanly. JavaScript runs once. The browser’s compositor handles the rest.
When the user drags or zooms, the strip animation pauses (via a .dragging class)
and a single live frame is rendered with rasterize(scene) per pointermove. On
release, the strip is re-baked with the new camera state.
The hit layer
Section titled “The hit layer”Glyphcss-style “every polygon is a DOM node” doesn’t fit ASCII rendering: the visible output is a single character grid, not a set of clickable polygons. Instead, glyphcss exposes a sparse hit layer: you opt-in to interactivity by registering hotspots at specific 3D anchors.
import { GlyphMesh, GlyphHotspot } from "@glyphcss/react";import { dodecahedronPolygons } from "@glyphcss/core";
const shape = dodecahedronPolygons({ center: [0, 0, 0], size: 1, color: "#cc44ff" });
<GlyphMesh polygons={shape}> <GlyphHotspot at={[0, 1.2, 0]} onClick={...}> <span className="badge">Top</span> </GlyphHotspot></GlyphMesh>Each hotspot becomes a real <div> absolutely positioned at its projected cell,
with its own @keyframes stepping through N projected positions. The strip and the
hotspot keyframes both use steps(N, end) over the same --dur, so they stay in
lockstep through rotation. Hotspots:
- Render real DOM children (use them for tooltips, badges, hover affordances).
- Fire normal DOM events (
onClick,onMouseEnter,onFocus). - Get free CSS
:hoverand:focus-visiblestyling. - Inspect in DevTools like any other element.
The camera contract
Section titled “The camera contract”The renderer and the hit layer must use the same camera.project(v, ...) call.
This is enforced by both reading from a shared GlyphCamera handle:
const camera = createGlyphPerspectiveCamera({ rotX: 25, rotY: 0, distance: 3, zoom: 50, stretch: 1.0,});
const scene = buildSceneContext({ camera, grid, wireframe: edges, mode: "wireframe" });
// Rendererconst frames = bakeFrames(scene, 60, "y");
// Hit layer — same camera, same N anglesfor (let i = 0; i < 60; i++) { camera.rotY = (i / 60) * 360; const cells = projectHotspots(hotspots, camera, cols, rows, cellAspect); // ...}If you ever find yourself threading a separate “current angle” through projection, stop: that’s how the strip and the hit layer drift apart.
Cell measurement (the recurring footgun)
Section titled “Cell measurement (the recurring footgun)”<pre> is display: block, so getBoundingClientRect().width returns the
container width, not the character width. Always measure on a hidden
<span style="display: inline-block"> instead. The probe should have the same
font properties as the strip (same font-family, same font-size,
line-height: normal).
After measuring, never leave font-size / line-height as inline styles on the
strip — the CSS variables on the scene root (--cell-fs, --cell-h) must be the
source of truth, or the strip’s rendered frame height will drift from the keyframe
translate distance and you’ll see the strip scrolling smoothly instead of stepping.