Skip to content

GlyphHotspot

<GlyphHotspot> renders a real, absolutely-positioned <div> over the ASCII render, tracking a 3D anchor through the live camera. The <div> is pointer-events: auto inside an otherwise transparent hit layer — events fire on it like any normal DOM element.

PropTypeDefaultDescription
idstringrequiredStable identifier for this hotspot
atVec3requiredWorld-space anchor [x, y, z]
size[number, number][1, 1]Hitbox size in character cells
onClickMouseEventHandlerClick handler
aria-labelstringAccessibility label
classNamestringCSS class
childrenReactNodeRendered inside the <div> (use for tooltips/badges)

On every render pass the anchor is projected through the current camera and the resulting cell is written straight to the element’s inline transform — one assignment per hotspot, no DOM rebuild and no second projection.

While the user drags, the same projection runs per pointermove, so the hotspot stays glued to its anchor through the whole gesture.

import {
GlyphCamera,
GlyphScene,
GlyphMesh,
GlyphOrbitControls,
GlyphHotspot,
} from "@glyphcss/react";
import { cubePolygons } from "@glyphcss/core";
const cube = cubePolygons({ center: [0, 0, 0], size: 1, color: "#4488ff" });
export function HotspotDemo() {
return (
<GlyphCamera rotX={25}>
<GlyphScene mode="solid" cols={100} rows={30}>
<GlyphOrbitControls />
<GlyphMesh polygons={cube}>
{/* Hotspot on the top face */}
<GlyphHotspot
id="top"
at={[0, 0.5, 0]}
size={[5, 3]}
aria-label="Top face"
onClick={() => alert("top face clicked")}
>
<span className="badge">Top</span>
</GlyphHotspot>
{/* Hotspot on the right face */}
<GlyphHotspot
id="right"
at={[0.5, 0, 0]}
size={[5, 3]}
aria-label="Right face"
onClick={() => alert("right face clicked")}
>
<span className="badge">Right</span>
</GlyphHotspot>
</GlyphMesh>
</GlyphScene>
</GlyphCamera>
);
}

A hotspot whose projected cell is not visible (behind the camera, or occluded) is hidden with display: "none" rather than faded — the renderer stages the style and applies it in the same single write as the rest of the frame. There is no opacity keyframe to hook a CSS transition onto; animate the element yourself if you want a fade.

at is a world-space Vec3. String anchors ("vertex:42", "face:roof", "centroid") are not implemented — resolve the coordinate yourself from the polygon data and pass the result:

const centroid = polygons
.flatMap((p) => p.vertices)
.reduce((a, v, _i, all) => [a[0] + v[0] / all.length, a[1] + v[1] / all.length, a[2] + v[2] / all.length], [0, 0, 0]);