Skip to content

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>).

 
Loading…
PropTypeDefaultDescription
mode"wireframe" | "solid" | "voxel" | "ink""solid"Render mode
colsnumber80Grid width in character columns
rowsnumber24Grid height in character rows
cellAspectnumber2.0Character cell height ÷ width
glyphPalettestring"default"Named glyph palette
autoSizebooleanfalseMeasure 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
wireframeJunctionsbooleanfalseResolve 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 }[]undefinedSolid-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
colorTolerancenumber0Merge adjacent cells into one <span> while their colors stay within this redmean distance (range 0765, not 0255) — 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
atlasPalettereadonly string[]undefinedPin 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
fontAtlasGlyphFontAtlasGLYPH_FONT_ATLASWhich 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
smoothShadingbooleanfalseGouraud shading from averaged vertex normals. Off by default — the faceted look is part of glyph’s identity
creaseAnglenumber60Max angle (degrees) between adjacent faces still smoothed together when smoothShading is on
interactiveDownscalenumber1Render 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
trackOpaqueCoveragebooleanfalseForce 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
useColorsbooleantrueEmit color spans in the output
directionalLightGlyphDirectionalLightDirectional light for solid mode
ambientLightGlyphAmbientLightAmbient fill for solid mode
shadowGlyphShadowOptionsundefinedShadow-map config. undefined = off. Set alongside castShadow/receiveShadow on meshes
transformCellsTransformCellsundefinedTransform the completed cell grid before its single <pre> write
classNamestringCSS class on the outer host
styleCSSPropertiesInline styles on the outer host
childrenReactNode<GlyphMesh>, controls, hotspots

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:

OptionDefaultWhat it does
doubleSidedfalseShade back faces instead of culling them — needed for open meshes and flat planes viewed from behind
supersample1Rasterize at the cell grid and box-filter down, for coverage antialiasing. charMode: "halfblock"/"quadrant" force an even supersample of at least 2 internally
depthEpsilon0Depth-test tolerance for coplanar surfaces
temporalBlend0Reprojection TAA: blends the ramp index and RGB against the previous frame. solidWeightRamp and charMode: "halfblock"/"quadrant" are no-ops while it is active

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.

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>
);
}

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>
);
}
FieldTypeDefaultDescription
colorstring"#000000"Shadow tint hex color
opacitynumber0.25Darkness 0..1 toward color
liftnumber0.05Depth 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
maxExtendnumber2000Accepted but not read. The light-space volume is fitted to the casters’ own bounds; this value has no effect on any render
MethodDescription
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