Effects
Effects are per-pixel GPU programs applied to a decorative layer, one that is
inert, aria-hidden and has no interactive elements. Keep artwork in its own
decorative Surface next to the interactive one. In ordinary DOM the layer simply
shows its original content.
<Surface id="art" decorative layout={{ x: 360, y: 20, width: 400, height: 300, overflow: "hidden" }}> <Illustration /></Surface>const handle = gpu.setEffects("art", [ { kind: "ripple", parameters: { amplitude: 6 }, time: { seconds: 0, playing: true }, bounds: "layer", interaction: "decorative", media: "identity", fallback: "static" },]);handle.pause(); handle.seek(1.5); handle.play(); handle.cancel();bounds, interaction, media and fallback are explicit on purpose: they state
that the effect stays inside the layer, never touches interactive content, keeps
the media as is, and falls back to the static original. A chain has 1–3 effects,
each kind once. Sampling effects (ripple, chromatic) come before colour effects
(dissolve, custom).
| Effect | Parameters | What it does |
|---|---|---|
ripple |
amplitude 0–12 px |
Radial wave that displaces where pixels sample from (refraction) |
chromatic |
displacement 0–12 px |
Shifts the red and blue channels apart |
dissolve |
amount 0–1, speed 0–4 |
Animated noise mask over alpha |
custom |
your own, up to 4 | Your WGSL colour function (below) |
gpu.setRipple(id, { amplitude, seconds, playing }) is a shortcut for a single
ripple; setRipple(id, null) clears it.
Your own WGSL
Section titled “Your own WGSL”import { defineEffect } from "@underglass/react";
export const sepia = defineEffect({ name: "sepia-glow", parameters: { amount: { min: 0, max: 1 } }, geometry: "color", bounds: "layer", capabilities: ["webgpu"], wgsl: ` let tone = vec3f(dot(color.rgb, vec3f(0.393, 0.769, 0.189)), dot(color.rgb, vec3f(0.349, 0.686, 0.168)), dot(color.rgb, vec3f(0.272, 0.534, 0.131))); return vec4f(mix(color.rgb, tone, parameters.x), color.a);`,});
gpu.setEffects("art", [{ kind: "custom", definition: sepia, parameters: { amount: 0.8 }, time: { seconds: 0 }, bounds: "layer", interaction: "decorative", media: "identity", fallback: "static" }]);The body is the inside of
fn(color: vec4f, local: vec2f, extent: vec2f, seconds: f32, parameters: vec4f) -> vec4f:
coloris the layer’s pixel.localis its position in CSS pixels;extentis the layer size.parametersholds your values in declaration order.
It may change colour and alpha but not geometry: use ripple or chromatic before
it for displacement.
- Definitions are trusted application code: up to 4 parameters and 4096 characters. Never build WGSL from user input.
- A new definition can be applied while GPU is on. It compiles in the background and appears when ready; the previous frame stays meanwhile. A compile error returns the app to DOM with the compiler message as the reason.
- Changing parameters or time never recompiles.
Live media
Section titled “Live media”Anything that paints inside the decorative layer is captured, including CSS
animations and <video> (a camera stream works too). The showcase’s webcam pond
refracts a camera image together with HTML text and CSS-animated fish.
Materials on interactive layers
Section titled “Materials on interactive layers”A material relights a layer without moving any pixel, so it can sit on a layer with real controls, permanently. It is a second slot next to the effect chain, applied first:
const sheen = defineEffect({ name: "sheen", geometry: "color", bounds: "layer", capabilities: ["webgpu"], parameters: { strength: { min: 0, max: 1 } }, wgsl: "return vec4f(color.rgb * (1.0 + parameters.x * (1.0 - local.y / extent.y)), color.a);",});const material = gpu.setMaterial("card-7", { kind: "custom", definition: sheen, parameters: { strength: 0.5 }, interior: { max: 0.04 }, bounds: "layer", interaction: "material", media: "identity", fallback: "static",});material.update({ strength: 0.2 }); // no recompilematerial.cancel();- The layer’s controls keep taking clicks, focus and typing. Nothing is made inert.
- Contrast is protected. The runtime calls your WGSL with a neutral grey, not
the layer’s pixels, and turns the result into one brightness factor per point,
limited to
interior.max(0 to 0.1). Text and the background behind it change by the same factor, so a material can shade a card but cannot wash out its text. - A material stays on the layer when the page returns to ordinary DOM (which shows the original) and applies again when GPU comes back. It ends when you cancel or dispose it, set another material on the layer, or remove the layer.
- Custom materials need WebGPU and cannot be combined with build-time compiled programs, like custom effects.
The built-in relief material is the other kind. Instead of WGSL it takes a scene in
the Strata Rendering Model’s format: relief
primitives (raised keys, wells, grooves, knobs, rings) lit from one light, with bevels,
shadows, finishes and focus edges. Strata’s GPU presentation builds these scenes for
you; handle.update(scene) changes states such as hover and press without
recompiling.
The objects material takes a version 2 scene: the same, plus real 3D objects (keys
that travel when pressed, wells, bezels, spherical knobs) seen through a perspective
camera. Their tops stay exactly on the captured labels, and nothing is ever drawn
outside the control it belongs to, so clicks still land on the real elements.
The instrument material takes a version 4 scene: each control becomes real hardware
(knob caps, fader carriages, key caps, switch levers) built from profiles and drawn as
lit meshes after its layer. Set each object’s t from the control’s native value with
handle.update(scene): the parts move with no capture and no change to hit testing.
A part that could leave its control’s box at any point of its travel is drawn flat
instead, and state().materials says which and by how much
(API).
Transition effects on interactive layers
Section titled “Transition effects on interactive layers”A layer with real controls can carry an effect while it is in a transition:
dissolving away when it is removed, materialising when it appears, or a shockwave when
it lands. Declare interaction: "inert" instead of "decorative":
const inert = { bounds: "layer", interaction: "inert", media: "identity", fallback: "static" } as const;const handle = gpu.setEffects("card-7", [ { kind: "ripple", parameters: { amplitude: 8 }, time: { seconds: 0, playing: true }, ...inert }, { kind: "dissolve", parameters: { amount: 0, speed: 0 }, time: { seconds: 0 }, ...inert },]);// Every frame of the transition:handle.update([{ amplitude: 8 * (1 - t) }, { amount: t, speed: 0 }]);// When it ends (or remove the layer):handle.cancel();- While the handle is active, the layer’s content is
inert: no clicks, focus or typing reach it, and focus inside it is lost. The layer’s own box still catches clicks, so a click on distorted pixels never reaches a control inside or under it. - Ending the handle (
cancel,dispose, a newsetEffects, removing the layer or returning to ordinary DOM) makes the content interactive again. updatechanges parameters every frame without recompiling anything.- The layer’s boundary element must not itself be a control.
Surfacerenders a<section>, which is fine. - In ordinary DOM nothing is drawn, but the content is still inert until the handle ends, so a transition behaves the same in both modes. With reduced motion the effect shows its original pixels.