Skip to content

Hit Layer Interactivity

The glyphcss hit layer is sparse — you opt-in to interactivity by registering hotspots at specific 3D anchors. No polygon-per-DOM-node overhead.

A polycss-style “one DOM node per polygon” model gives you per-polygon events but balloons the DOM for large meshes (a 10k-triangle GLB → 10k <div>s). glyphcss takes the opposite trade: a single <pre> for visuals, and only the hotspots you explicitly register become DOM nodes.

For most real apps this is what you want. You don’t need click handlers on every roof shingle; you need them on the front door.

import { GlyphCamera, GlyphScene, GlyphMesh, GlyphHotspot } from "@glyphcss/react";
import { octahedronPolygons } from "@glyphcss/core";
const shape = octahedronPolygons({ center: [0, 0, 0], size: 1, color: "#ffcc44" });
<GlyphCamera rotX={25}>
<GlyphScene mode="solid" cols={80} rows={24}>
<GlyphMesh polygons={shape}>
<GlyphHotspot id="top" at={[0, 1, 0]} onClick={() => alert("top vertex")}>
<span className="badge">Top</span>
</GlyphHotspot>
<GlyphHotspot id="bottom" at={[0, -1, 0]} onClick={() => alert("bottom vertex")}>
<span className="badge">Bottom</span>
</GlyphHotspot>
</GlyphMesh>
</GlyphScene>
</GlyphCamera>

Each <GlyphHotspot> becomes:

<div class="glyph-hotspot" data-hotspot-id="top"
style="position: absolute; left: …; top: …; width: 1ch; height: 2ch">
<span class="badge">Top</span>
</div>

The height is size[1] × cellAspect ch — 2ch at the default aspect — so the box covers a whole character cell. left/top are reassigned inline each render from the same camera projection that produced the character grid, anchored at the cell centre — the .glyph-hotspot rule applies transform: translate(-50%, -50%). There is no role; add your own semantics if the hotspot is interactive.

at is a world-space Vec3. String anchors like "vertex:42" are not implemented — compute the coordinate from your polygon data and pass it.

A hotspot whose projected cell is not visible is hidden with display: "none", staged and applied in the same single write as the rest of the frame — not faded via opacity. Animate the element yourself if you want a transition.