# Glyph Effects

```ts
import { createGlyphPerspectiveCamera, createGlyphScene, createGlyphOrbitControls } from "glyphcss";
import { resolveGeometry } from "@glyphcss/core";

const host = document.getElementById("scene");
const camera = createGlyphPerspectiveCamera({ rotX: 65, rotY: 0, zoom: 0.3, distance: 0, perspective: 32000, stretch: 1 });
const scene = createGlyphScene(host, { camera, autoSize: true, mode: "solid" });
scene.output.style.fontSize = "10px";
scene.output.style.lineHeight = "1";
scene.fit();
const polygons = resolveGeometry("cube", { size: 100 });
scene.add(polygons);
createGlyphOrbitControls(scene, { drag: true, wheel: true });
```



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

```ts
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();
}
```

  

```tsx
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.

  

```vue
<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.

  

```html
<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

```ts
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

```ts
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



```ts
import { createGlyphPerspectiveCamera, createGlyphScene, createGlyphOrbitControls } from "glyphcss";
import { resolveGeometry } from "@glyphcss/core";

const host = document.getElementById("scene");
const camera = createGlyphPerspectiveCamera({ rotX: 65, rotY: 0, zoom: 0.3, distance: 0, perspective: 32000, stretch: 1 });
const scene = createGlyphScene(host, { camera, autoSize: true, mode: "solid" });
scene.output.style.fontSize = "10px";
scene.output.style.lineHeight = "1";
scene.fit();
const polygons = resolveGeometry("icosahedron", { size: 100 });
scene.add(polygons);
createGlyphOrbitControls(scene, { drag: true, wheel: true });
```



```ts
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](/synth).

| 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

```ts
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"`)

```ts
synth.setParams({
  space: "object", render: "carve",
  field1: "menger", wave1: "step", freq1: 3, amp1: 1, iter1: 3,
  amp2: 0, combine: "min",
});
```

<a id="sdf-fields-gyroid-menger-sierpinski"></a>

| 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

```ts
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

```ts
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

```ts
import { isGlyphFieldSynthStaticExportSupported } from "@glyphcss/effects";
const supported = isGlyphFieldSynthStaticExportSupported(synth.params);
```

Only field synth supports [effect-only static export](/guides/compile#effect-only-static-export-field-synth) through `buildGlyphFieldSynthStaticExport`. Object-space, carve/xray, and program-authored patches require the [interactive export](/guides/compile#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.
