Refraction Material Lab

A rasterized wall of variable type sits under 3 transmissive panels. Move and resize them, then sweep the material controls: thickness bends the path, roughness blurs the sample, dispersion splits it by wavelength, and light angle changes the highlight.

FPS
measuring
ms/frame p95
measuring
KB transfer
—

Material

Drag a panel, or Tab to it and use the arrow keys. Hold Shift with the arrows to resize it.

0.65
0.12
0.28
42°

How this is built

Material path
Three.js r185 renders 3 fixed MeshPhysicalMaterial panels over an offscreen Canvas 2D typography texture. Transmission is 1, IOR is 1.5, and thickness (0.05–2), roughness (0–1) and dispersion (0–1) are live controls. One directional light follows the angle control (0–360°); the Roboto Flex wall uses no additional fonts. The glass is tone-mapped, so its colour drifts from the palette by design.
Following the theme
The wall is a raster, so repainting it on every frame of a theme morph would be the most expensive thing on the route — but repainting it only at the end made the switch land as a jump. The destination raster is painted once per switch, into the same offscreen canvas, and the two uploads cross-fade along the tween’s own easing. Per frame that is one uniform write, and the blend is taken back to sRGB-encoded values so its midpoint stays on the perceptual path the tokens walk.
Performance
The device pixel ratio is capped at 2 and transmission resolution at 0.5. There is no post-processing. The scene renders only for a state or resize change; an active drag alone runs the animation loop. One offscreen canvas and one 2D context serve the type wall for the scene’s lifetime. Geometry, textures, render lists, and the renderer are disposed on unmount, and context loss is handled through restore events.
Reduced motion and state
With prefers-reduced-motion: reduce, the scene remains interactive but every pointer and keyboard change renders discretely; the HUD reports static. Panel layout and all material values are saved to lab.refraction-lab.v1. An earlier build wrote refraction-lab:v1; that key is still read as a fallback and deliberately left in place, so reverting this build finds the same panels. Reset is the one action that clears both. Storage reads and writes are guarded so a blocked or full store cannot break the instrument.
Measurement
The HUD reads only frames rendered by this scene: metrics.tick(now) is called after each rendered frame, including the drag loop. Its averaging and percentile windows are declared in lib/perf/frame-metrics.ts, which every route in the lab shares, so they are not restated here. While the instrument is idle, its frame readout honestly remains in its measuring state rather than implying a loop that never stops.