GlyphScene
<GlyphScene> is the visual host. It owns the <pre> output element, the sibling
hit layer, and the measured cell metrics the projection uses.
<GlyphScene> must be a child of a camera component (<GlyphCamera>,
<GlyphPerspectiveCamera>, or <GlyphOrthographicCamera>).
| Prop | Type | Default | Description |
|---|---|---|---|
mode | "wireframe" | "solid" | "voxel" | "ink" | "solid" | Render mode |
cols | number | 80 | Grid width in character columns |
rows | number | 24 | Grid height in character rows |
cellAspect | number | 2.0 | Character cell height ÷ width |
glyphPalette | string | "default" | Named glyph palette |
autoSize | boolean | false | Measure the host element and derive cols/rows from it instead of using the fixed grid |
charMode | "ascii" | "braille" | "halfblock" | "quadrant" | "ascii" | Sub-cell character encoding. "braille" is wireframe-only; "halfblock" and "quadrant" are solid-only. No-op outside its mode. See character encoding |
wireframeJunctions | boolean | false | Resolve wireframe corners and crossings to box-drawing glyphs (┌┐└┘├┤┬┴┼─│). ASCII wireframe only. See junctions |
hiddenLines | "show" | "hide" | "show" | Hidden-line removal for wireframe and ink: "hide" drops strokes a nearer surface covers. No-op in solid. See hidden-line removal |
solidWeightRamp | { glyph: string; weight: number }[] | undefined | Solid-only shading ramp that varies font-weight as well as glyph, ordered darkest → densest. Replaces the palette’s solid ramp when set. See font-weight ramp |
colorTolerance | number | 0 | Merge adjacent cells into one <span> while their colors stay within this redmean distance (range 0–765, not 0–255) — fewer spans, faster paint, at the cost of color fidelity. 0 is off and byte-identical. NaN/negative values degrade to 0; +Infinity merges every same-glyph run in a row. No-op under glyphOutput: "semantic" — semantic colors are exact class identifiers, not shaded appearance. See spans, not just cells |
colorEncoding | "spans" | "atlas" | "spans" | "atlas" encodes each (glyph, color) pair as a Private Use Area code point against a checked-in COLR/CPAL color font, so the render is one text node with zero <span>s. "spans" is byte-identical to before the option existed. Falls back to spans, whole-scene, whenever the atlas cannot carry the frame. See color encoding |
atlasPalette | readonly string[] | undefined | Pin the ordered #rrggbb palette that colorEncoding: "atlas" slots encode against — a brand ramp or a reproducible bake. Omitted, the scene derives and pools one itself, so this does not gate the atlas. Bounded by the atlas’s slot count. See the palette |
fontAtlas | GlyphFontAtlas | GLYPH_FONT_ATLAS | Which color-font atlas to encode against — the universal one (212 glyphs / 30 palette slots) or GLYPH_FONT_ATLAS_ASCII (94 / 68), trading glyph coverage for color resolution. Fixed at scene creation; a later change is not forwarded. See choosing an atlas |
smoothShading | boolean | false | Gouraud shading from averaged vertex normals. Off by default — the faceted look is part of glyph’s identity |
creaseAngle | number | 60 | Max angle (degrees) between adjacent faces still smoothed together when smoothShading is on |
interactiveDownscale | number | 1 | Render at 1/n resolution while a control is dragging, full detail on release. Same on-screen size — this keeps high-density scenes inside the frame budget mid-gesture |
trackOpaqueCoverage | boolean | false | Force a base-layer depth raster each render so scene.getOpaqueCoverage() can publish it. A scene only builds an occlusion id-map when it has an opaque detail mesh or a foreign mask, so a scene of plain base meshes needs this to feed another scene’s setForeignOcclusion. Changes no output; costs one raster |
glyphOutput | "visible" | "semantic" | "visible" | "semantic" renders authored class labels instead of shaded glyphs and enables scene.getGlyphSemanticCellFrame(). Requires the sceneManifest + dictionary JS properties |
useColors | boolean | true | Emit color spans in the output |
directionalLight | GlyphDirectionalLight | — | Directional light for solid mode |
ambientLight | GlyphAmbientLight | — | Ambient fill for solid mode |
shadow | GlyphShadowOptions | undefined | Shadow-map config. undefined = off. Set alongside castShadow/receiveShadow on meshes |
transformCells | TransformCells | undefined | Transform the completed cell grid before its single <pre> write |
className | string | — | CSS class on the outer host |
style | CSSProperties | — | Inline styles on the outer host |
children | ReactNode | — | <GlyphMesh>, controls, hotspots |
Vanilla-only scene options
Section titled “Vanilla-only scene options”createGlyphScene accepts a few options the React and Vue components do not
expose as props. compileScene accepts the first two (doubleSided and
supersample) as well:
| Option | Default | What it does |
|---|---|---|
doubleSided | false | Shade back faces instead of culling them — needed for open meshes and flat planes viewed from behind |
supersample | 1 | Rasterize at n× the cell grid and box-filter down, for coverage antialiasing. charMode: "halfblock"/"quadrant" force an even supersample of at least 2 internally |
depthEpsilon | 0 | Depth-test tolerance for coplanar surfaces |
temporalBlend | 0 | Reprojection TAA: blends the ramp index and RGB against the previous frame. solidWeightRamp and charMode: "halfblock"/"quadrant" are no-ops while it is active |
Cell transforms and surface UVs
Section titled “Cell transforms and surface UVs”transformCells runs after rasterization, shading, and depth testing, immediately
before glyphcss stringifies the grid. The hook may mutate grid.char and
grid.color; glyphcss still performs one write to the scene’s <pre> for the
completed frame.
In solid mode, polygons with authored uvs also expose grid.surfaceUv. This is
an optional interleaved Float32Array containing the perspective-correct UV from
the depth-winning surface for each cell:
import { createGlyphScene } from "glyphcss";import type { TransformCells } from "glyphcss";
const word = Array.from("HOLA");
const mapWordToSurface: TransformCells = (grid) => { const uv = grid.surfaceUv; if (!uv) return;
for (let i = 0; i < grid.char.length; i++) { const u = uv[i * 2]; const v = uv[i * 2 + 1]; if (!Number.isFinite(u) || !Number.isFinite(v)) continue;
grid.char[i] = word[((Math.floor(u * 12) % word.length) + word.length) % word.length]; }};
const scene = createGlyphScene(host, { camera, mode: "solid", transformCells: mapWordToSurface,});surfaceUv uses [u0, v0, u1, v1, ...]. Empty cells and cells whose winning
polygon has no UVs contain NaN/non-finite coordinates, so effects must check
both components. Values remain in the polygon’s authored coordinate space;
they are not clamped to 0..1, and may tile beyond that range. Because the
mapping follows the polygon UVs rather than screen rows and columns, patterns
rotate and foreshorten with the surface. Treat the grid buffers as
callback-scoped; use rasterizeToCells when they must outlive the synchronous
transformCells call.
For reusable or animated appearance, prefer a mounted
GlyphEffectLayer. Generic layers retain the geometry raster
and avoid re-projecting the mesh on parameter-only ticks; transformCells
remains the final application-specific escape hatch and runs after those layers.
Full examples
Section titled “Full examples”import { GlyphPerspectiveCamera, 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 App() { return ( <GlyphPerspectiveCamera rotX={25} zoom={50} distance={3}> <GlyphScene mode="solid" cols={100} rows={30}> <GlyphOrbitControls drag wheel /> <GlyphMesh polygons={cube}> <GlyphHotspot id="top" at={[0, 0.5, 0]} size={[3, 2]} onClick={() => alert("top face")} /> </GlyphMesh> </GlyphScene> </GlyphPerspectiveCamera> );}<template> <GlyphPerspectiveCamera :rot-x="25" :zoom="50" :distance="3"> <GlyphScene mode="solid" :cols="100" :rows="30"> <GlyphOrbitControls drag wheel /> <GlyphMesh :polygons="cube"> <GlyphHotspot id="top" :at="[0, 0.5, 0]" :size="[3, 2]" @click="() => alert('top face')" /> </GlyphMesh> </GlyphScene> </GlyphPerspectiveCamera></template>
<script setup lang="ts">import { GlyphPerspectiveCamera, GlyphScene, GlyphMesh, GlyphOrbitControls, GlyphHotspot,} from "@glyphcss/vue";import { cubePolygons } from "@glyphcss/core";
const cube = cubePolygons({ center: [0, 0, 0], size: 1, color: "#4488ff" });</script>import { createGlyphPerspectiveCamera, createGlyphScene, createGlyphOrbitControls,} from "glyphcss";import { cubePolygons } from "@glyphcss/core";
const host = document.querySelector<HTMLElement>("#scene")!;
const camera = createGlyphPerspectiveCamera({ rotX: 25, zoom: 50, distance: 3 });const scene = createGlyphScene(host, { camera, mode: "solid", cols: 100, rows: 30,});
scene.add(cubePolygons({ center: [0, 0, 0], size: 1, color: "#4488ff" }));
scene.addHotspot( { id: "top", at: [0, 0.5, 0], size: [3, 2] }, () => alert("top face"),);
const controls = createGlyphOrbitControls(scene, { drag: true, wheel: true });
// Later, when done:// controls.destroy();// scene.destroy();<script type="module" src="https://esm.sh/glyphcss/elements"></script>
<glyph-camera> <glyph-scene mode="solid"> <glyph-mesh geometry="cuboctahedron"></glyph-mesh> <glyph-orbit-controls drag wheel></glyph-orbit-controls> </glyph-scene></glyph-camera>Shadows
Section titled “Shadows”Shadows are opt-in. Set shadow on <GlyphScene> to enable the shadow-map pass,
then flag individual meshes with castShadow and/or receiveShadow.
A mesh with both flags self-shadows. <GlyphGround /> defaults to receiveShadow=true.
import { GlyphPerspectiveCamera, GlyphScene, GlyphMesh, GlyphGround,} from "@glyphcss/react";
const shadow = { color: "#000000", opacity: 0.25, lift: 0.05 };const directionalLight = { direction: [0.5, 0.7, 0.5], intensity: 1 };
export function ShadowDemo() { return ( <GlyphPerspectiveCamera rotX={45} rotY={30} zoom={50} distance={5}> <GlyphScene mode="solid" cols={100} rows={30} directionalLight={directionalLight} shadow={shadow} > <GlyphMesh geometry="dodecahedron" color="#4488ff" castShadow receiveShadow /> <GlyphGround /> </GlyphScene> </GlyphPerspectiveCamera> );}<template> <GlyphPerspectiveCamera :rot-x="45" :rot-y="30" :zoom="50" :distance="5"> <GlyphScene mode="solid" :cols="100" :rows="30" :directional-light="directionalLight" :shadow="shadow" > <GlyphMesh geometry="dodecahedron" color="#4488ff" cast-shadow receive-shadow /> <GlyphGround /> </GlyphScene> </GlyphPerspectiveCamera></template>
<script setup lang="ts">import { GlyphPerspectiveCamera, GlyphScene, GlyphMesh, GlyphGround,} from "@glyphcss/vue";
const shadow = { color: "#000000", opacity: 0.25, lift: 0.05 };const directionalLight = { direction: [0.5, 0.7, 0.5], intensity: 1 };</script><glyph-perspective-camera rot-x="45" rot-y="30" zoom="50" distance="5"> <glyph-scene mode="solid" cols="100" rows="30" directional-intensity="1" shadow shadow-color="#000000" shadow-opacity="0.25" shadow-lift="0.05" > <glyph-mesh geometry="dodecahedron" color="#4488ff" cast-shadow receive-shadow ></glyph-mesh> <!-- There is no "ground" geometry name: `GlyphGround` is a React/Vue component only. From HTML, use a large flat mesh as the receiver. --> <glyph-mesh geometry="cube" scale="12 12 0.1" position="0 0 -2" receive-shadow ></glyph-mesh> </glyph-scene></glyph-perspective-camera>GlyphShadowOptions fields
Section titled “GlyphShadowOptions fields”| Field | Type | Default | Description |
|---|---|---|---|
color | string | "#000000" | Shadow tint hex color |
opacity | number | 0.25 | Darkness 0..1 toward color |
lift | number | 0.05 | Depth bias — prevents self-shadow acne on flat lit surfaces. A WORLD-UNIT length, so the default assumes a room-scale scene; a scene whose unit is (say) an Earth radius must set its own, or every shadow is biased away |
maxExtend | number | 2000 | Accepted but not read. The light-space volume is fitted to the casters’ own bounds; this value has no effect on any render |
Lifecycle methods (vanilla)
Section titled “Lifecycle methods (vanilla)”| Method | Description |
|---|---|
scene.add(polygons, transform?) | Register a mesh, returns a GlyphMeshHandle |
mesh.setPolygons(polygons) | Replace a mesh’s geometry in place |
mesh.setTransform(transform) | Replace a mesh’s transform |
mesh.dispose() | Remove a mesh |
scene.addHotspot(opts, onClick?) | Register a hotspot overlay, returns a GlyphHotspotHandle |
hotspot.setAt(at) | Move a hotspot’s 3D anchor without touching its element — the element, its listeners and anything you wrote on it survive, and only the projected position changes |
hotspot.remove() | Remove a hotspot overlay |
scene.setOptions(partial) | Update any scene option and trigger a re-render |
scene.getOptions() | Return a snapshot of current options |
scene.rerender() | Force an immediate re-rasterize |
scene.addEffectLayer(opts) | Mount an ordered appearance program over the retained grid, returns a GlyphEffectLayerHandle (see Glyph Effects). Changing the handle’s params / opacity / blend / order / enabled recomposes without re-projecting the mesh |
scene.setInteracting(active) | Tell the scene a gesture is in progress, so interactiveDownscale applies. The bundled controls call this for you — use it for custom interaction sources |
scene.getOpaqueCoverage() | The last committed render’s opaque per-cell coverage at the output grid — cells owned by the base grid or an opaque detail layer, foreign stamps excluded. null when that render built no occlusion id-map (see trackOpaqueCoverage). solid mode only |
scene.setForeignOcclusion(coverage) | Consume another scene’s getOpaqueCoverage() result: every layer of this scene blanks under the covered cells, so two scenes stacked over one host share a single occlusion domain. null clears. Re-publishing identical bytes costs a comparison, not a render. solid mode only |
scene.getGlyphSemanticCellFrame() | Immutable snapshot of the last committed base grid’s polygon → surface → instance → class lineage. null unless glyphOutput: "semantic" |
scene.destroy() | Remove the scene DOM and clear all meshes |