Glyph Effects
Matrix rain replaces the cube’s surface with animated text. Drag to orbit. Install @glyphcss/effects alongside glyphcss or your framework binding; effects change appearance without changing the mesh.
Mount an effect
Section titled “Mount an effect”import { createGlyphScene, createGlyphPerspectiveCamera } from "glyphcss";import { cubePolygons } from "@glyphcss/core";import { GlyphEffects } from "@glyphcss/effects";
const host = document.querySelector<HTMLElement>("#scene")!;const scene = createGlyphScene(host, { camera: createGlyphPerspectiveCamera({ zoom: 80 }), mode: "solid", autoSize: true,});scene.add(cubePolygons({ center: [0, 0, 0], size: 1 }));const rain = scene.addEffectLayer({ effect: GlyphEffects.matrixRain, blend: "replace", params: { glyphs: "HOLA", space: "auto", colorMode: "monochrome" },});let raf = 0;function frame(now: number) { rain.params.time = now / 1000; raf = requestAnimationFrame(frame);}raf = requestAnimationFrame(frame);
// Call when the host unmounts:function dispose() { cancelAnimationFrame(raf); rain.dispose(); scene.destroy();}import { useEffect, useRef } from "react";import { GlyphEffectLayer, type GlyphEffectLayerHandle } from "@glyphcss/react";import { GlyphEffects } from "@glyphcss/effects";import type { GlyphEffectParamsOf } from "glyphcss";
export function Rain() { const ref = useRef<GlyphEffectLayerHandle<GlyphEffectParamsOf<typeof GlyphEffects.matrixRain>>>(null); useEffect(() => { let raf = 0; const frame = (now: number) => { if (ref.current) ref.current.params.time = now / 1000; raf = requestAnimationFrame(frame); }; raf = requestAnimationFrame(frame); return () => cancelAnimationFrame(raf); }, []); return <GlyphEffectLayer ref={ref} effect={GlyphEffects.matrixRain} blend="replace" />;}Place <Rain /> inside a GlyphScene containing a mesh. Unmounting disposes the layer.
<script setup lang="ts">import { onMounted, onBeforeUnmount, ref } from "vue";import { GlyphEffectLayer, type GlyphEffectLayerExposed } from "@glyphcss/vue";import { GlyphEffects } from "@glyphcss/effects";import type { GlyphEffectParamsOf } from "glyphcss";
const rain = ref<GlyphEffectLayerExposed<GlyphEffectParamsOf<typeof GlyphEffects.matrixRain>> | null>(null);let raf = 0;onMounted(() => { const frame = (now: number) => { if (rain.value) rain.value.params.time = now / 1000; raf = requestAnimationFrame(frame); }; raf = requestAnimationFrame(frame);});onBeforeUnmount(() => cancelAnimationFrame(raf));</script>
<template> <GlyphEffectLayer ref="rain" :effect="GlyphEffects.matrixRain" blend="replace" /></template>Mount inside GlyphScene; unmounting disposes the layer.
<glyph-effect-layer id="rain" blend="replace"></glyph-effect-layer><script type="module"> import "glyphcss/elements"; import { GlyphEffects } from "@glyphcss/effects"; const element = document.querySelector("#rain"); element.configure({ effect: GlyphEffects.matrixRain }); const layer = await element.whenReady(); let raf = 0; function frame(now) { layer.params.time = now / 1000; raf = requestAnimationFrame(frame); } raf = requestAnimationFrame(frame); // On teardown: cancelAnimationFrame(raf), then remove the element.</script>Place the element inside glyph-scene with a mesh. Definitions are JavaScript properties, never JSON attributes.
Effects own no clock: update params.time in seconds. Use layer.setParams({...}) for batches. Animation libraries can target the stable layer.params object directly.
Composition and mapping
Section titled “Composition and mapping”const cube = scene.add(cubePolygons({ center: [0, 0, 0], size: 1 }));const layer = scene.addEffectLayer({ effect: GlyphEffects.scan, target: cube, blend: "over",});| Option | Behavior |
|---|---|
blend: "replace" | Empty effect cells hide the base surface |
blend: "over" | Paint active cells over the base texture |
target: "surfaces" | Visible geometry; default target |
target: "viewport" | Includes the base output’s background |
target: mesh or [meshA, meshB] | Only those meshes; solid mode only |
Mesh targets are fixed at mount; dispose and remount to retarget. Removed meshes stop matching. Layers compose in order; later scene-wide layers can fill earlier carve holes.
space | Mapping |
|---|---|
"auto" | Authored UVs, otherwise generated surface mapping, otherwise scene coordinates |
"surface" | Generated per-face mapping; ignores authored UVs |
"scene" | Projected 2D coordinates |
"object" | Mesh-local 3D field; needs solid mode, otherwise falls back to surface/scene |
matrixRain defaults to "object"; flowText, scan, and fieldSynth default to "auto". scale changes pattern frequency, not grid density; retune it when switching to object space. Generated flow follows downhill per face; flat caps get a deterministic direction.
Text effects require printable single-cell glyphs. Combining marks, emoji, and double-width characters are rejected.
Stock effect parameters
Section titled “Stock effect parameters”import { getGlyphEffect, defaultGlyphEffectParams, GlyphEffectCatalog } from "@glyphcss/effects";const scan = getGlyphEffect("scan")!;const defaults = defaultGlyphEffectParams(scan);const schema = scan.parameterSchema;Every stock effect takes time. Inspect parameterSchema for ranges and defaults, or iterate GlyphEffectCatalog. direction accepts up, down, left, or right.
| Effect | Main parameters and defaults |
|---|---|
matrixRain | glyphs: "HOLA", direction: "down", speedMin: 5, speedMax: 12, trail: 14, density: 0.55, seed: 1, colorMode: "original", color: "#00ff66", headColor: "#d8ffe4" |
flowText | glyphs: "HOLA", direction: "right", speed: 6 cells/s |
scan | speed: 10 cells/s, width: 3, spacing: 28, color: "#ffffff" |
wipe | progress: 0.5, softness: 0.04, direction, invert: false |
scramble | glyphs: "@#$%&*+=?", amount: 0.35, rate: 10 Hz, seed: 1 |
glitch | glyphs: "#%/=+!?", amount: 0.28, rate: 12 Hz, bandSize: 4, seed: 1, color: "#ff4fd8" |
noiseDissolve | progress: 0.5, softness: 0.08, scale: 0.22, seed: 1 |
ripple | glyphs: "*+", speed: 6, frequency: 0.5, width: 0.18, amount: 0.85, color: "#72d9ff" |
Rain’s colorMode: "monochrome" uses color and a bright headColor; "original" preserves model colors. wipe and noiseDissolve are progress-driven masks: use blend: "replace" so masked cells disappear.
Field synth
Section titled “Field synth”const synth = scene.addEffectLayer({ effect: GlyphEffects.fieldSynth, blend: "replace", params: { field1: "radial", wave1: "sin", freq1: 4, speed1: 0.6, amp1: 1, field2: "angular", wave2: "saw", freq2: 6, speed2: 0.3, amp2: 1, combine: "multiply", color: "#ffcf5a", colorB: "#ff4fa3", gradient: 0.6, },});Drive synth.params.time with the same animation loop. Each voice samples a spatial field through a waveform. Try GlyphEffects.fieldSynth.presets ({ name, params } pairs) or design a patch in the synth workbench.
| Controls | Meaning |
|---|---|
field1..9 | radial, angular, spiral, linearX, linearY, diagonal, noise; linearZ requires object space |
wave1..9 | sin, triangle, saw, square, step; step is non-periodic |
freqN, speedN, ampN | Cycles/unit, cycles/second, mix weight; ampN: 0 skips a voice |
dutyN, phaseN | Square-wave high fraction (0.5), phase in cycles (0) |
scale, originU, originV | Global frequency (2) and center (0.5, 0.5) |
angleN, originUN, originVN, originWN | Per-voice rotation in degrees and origin offsets; default 0 |
combine | add, multiply, max, min, difference, argmax |
gain, bias | Contrast and brightness: clamp01(bias + gain * combined * 0.5) |
glyphs | Dark-to-dense ramp, default " .:-=+*#%@" |
color, colorB, gradient | Two-color gradient; gradient: 0 uses only color |
voiceColors, colorN | Per-voice colors instead of the gradient; default off |
lit | Surface lighting amount: 1 by default, 0 unlit |
argmax selects the winning voice; amplitudes become comparison weights. Linear fields ignore origins: shift them with phaseN. Object-space radial fields are spherical; angular/spiral remain in XY. noise evolves over time. Object-space scan/flow text use triplanar mapping.
Voice layers
Section titled “Voice layers”synth.setParams({ layer1: 1, layer2: 2, layerCombine2: "max", layerBlend2: "multiply" });Assign voices to layer1..9 (default 1), choosing layers 1..3. Each layer has layerCombineL, layerThresholdOnL/layerThresholdL (off/0, range -3..3), layerInvertL, layerBlendL (multiply), and layerAmpL (1). Populated layers cannot use argmax in a multi-layer patch.
Volumetric fields (space: "object")
Section titled “Volumetric fields (space: "object")”synth.setParams({ space: "object", render: "carve", field1: "menger", wave1: "step", freq1: 3, amp1: 1, iter1: 3, amp2: 0, combine: "min",});| Option | Behavior / limitation |
|---|---|
gyroid, menger, sierpinski | Distance fields; iterN: 1..4 (3) controls recursive detail; use waveN: "step" for solid membership |
render: "paint" | Default surface texture |
render: "carve" | Interior walls/holes; requires object space and preferably full-opacity replace |
render: "xray" | Integrated density; requires object space and subcellRes: "1x1"; xrayGain: 4 (0..16) controls contrast |
marchSteps, marchFade | Sampling budget (48, maximum 256), carve depth fade (1) |
subcellRes: "1x1" | Default ramp glyphs |
subcellRes: "2x4" | Braille dots; ignores ramp; surface mapping can shear |
subcellRes: "ink" | ASCII contours; ignores ramp; inkLevels: 4 (1..12) in paint, inkSpacing: 0.25 in carve |
Carve contours/dots respect mesh boundaries. Xray ignores voiceColors; thresholded fields show structure more clearly. Nonzero speedN animates volumetrics; reset/wrap time for looping step-wave erosion.
Program builder and program-as-data
Section titled “Program builder and program-as-data”import { buildGlyphFieldProgram } from "@glyphcss/effects";const program = buildGlyphFieldProgram({ domain: "3d", layers: [{ voices: [{ field: "menger", wave: "step", freq: 3, iter: 3 }], combine: "min" }],});scene.addEffectLayer({ effect: GlyphEffects.fieldSynth, program, blend: "replace", params: { space: "object", render: "carve" },});Programs remove the nine-voice cap. program overrides voice/layer params; mapping, rendering, and output still use params. colorProgram independently supplies color voices. Both are immutable after mount, supported in React/Vue/HTML, and unavailable in workbench URLs or static exports.
Use validateGlyphFieldProgram to validate authored data before calling evaluateGlyphFieldProgram yourself.
GlyphRamps
Section titled “GlyphRamps”import { GlyphRamps, calibrateGlyphRamp } from "@glyphcss/effects";const { ramp } = calibrateGlyphRamp({ font: { family: "Menlo, monospace", size: 32 }, steps: 12 });synth.setParams({ glyphs: ramp });// Or use GlyphRamps.Blocks directly.Named ramps: Fade, Blocks, Shades, Dots, Binary, ASCII, Hatch, Stars, Digital. Calibration measures your font; measureGlyphInkCoverage(glyph, { font }) measures one glyph. Both need browser Canvas 2D, or a supplied canvasFactory outside the DOM. Results are plain strings usable in effects or static compilation.
Exports and limits
Section titled “Exports and limits”import { isGlyphFieldSynthStaticExportSupported } from "@glyphcss/effects";const supported = isGlyphFieldSynthStaticExportSupported(synth.params);Only field synth supports effect-only static export through buildGlyphFieldSynthStaticExport. Object-space, carve/xray, and program-authored patches require the interactive export. Check support rather than duplicating restrictions; other static compile paths do not evaluate effects.
Scene-image sampling/displacement, scratch graphs, and surface-key/UV-footprint fields are unsupported and reject explicitly.