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.
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | required | Stable identifier for this hotspot |
at | Vec3 | required | World-space anchor [x, y, z] |
size | [number, number] | [1, 1] | Hitbox size in character cells |
onClick | MouseEventHandler | — | Click handler |
aria-label | string | — | Accessibility label |
className | string | — | CSS class |
children | ReactNode | — | Rendered inside the <div> (use for tooltips/badges) |
Tracking math
Section titled “Tracking math”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.
Full examples
Section titled “Full examples”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> );}<template> <GlyphCamera :rot-x="25"> <GlyphScene mode="solid" :cols="100" :rows="30"> <GlyphOrbitControls /> <GlyphMesh :polygons="cube"> <GlyphHotspot id="top" :at="[0, 0.5, 0]" :size="[5, 3]" aria-label="Top face" @click="onTopClick" /> <GlyphHotspot id="right" :at="[0.5, 0, 0]" :size="[5, 3]" aria-label="Right face" @click="onRightClick" /> </GlyphMesh> </GlyphScene> </GlyphCamera></template>
<script setup lang="ts">import { GlyphCamera, GlyphScene, GlyphMesh, GlyphOrbitControls, GlyphHotspot,} from "@glyphcss/vue";import { cubePolygons } from "@glyphcss/core";
const cube = cubePolygons({ center: [0, 0, 0], size: 1, color: "#4488ff" });function onTopClick() { alert("top face clicked"); }function onRightClick() { alert("right face clicked"); }</script>import { createGlyphScene } from "glyphcss";import { cubePolygons } from "@glyphcss/core";
const host = document.querySelector<HTMLElement>("#scene")!;const scene = createGlyphScene(host, { mode: "solid", cols: 100, rows: 30 });
scene.add(cubePolygons({ center: [0, 0, 0], size: 1, color: "#4488ff" }));
const topHotspot = scene.addHotspot( { id: "top", at: [0, 0.5, 0], size: [5, 3] }, () => alert("top face clicked"),);
const rightHotspot = scene.addHotspot( { id: "right", at: [0.5, 0, 0], size: [5, 3] }, () => alert("right face clicked"),);
// Remove a hotspot when no longer needed:// topHotspot.remove();Visibility
Section titled “Visibility”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.
Anchors
Section titled “Anchors”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]);