FPS
measuring
ms/frame p95
measuring
KB transfer
—

Type Pressure Field

A lattice of variable-font glyphs treated as a deformable surface. The pointer is a pressure source: nearby letters gain weight and width, swell in size and lift off their baseline. Move the pointer — or focus the field and use the arrow keys — to disturb it, hold still to deepen the well, leave the canvas and the springs settle it back to rest.

How this is built

Pressure model
Each cell reads a Gaussian falloff around the pointer, scaled by an intensity term. Intensity is driven by smoothed pointer speed and deepened by dwelling — a stationary pointer ramps to its strongest well over about 1.2 seconds. Leaving the canvas drops the intensity to zero.
Spring return
Every cell holds one damped harmonic oscillator (stiffness 105, damping 13) integrated at a fixed 1/120 s step with a 4-step-per-frame ceiling, so the motion is identical on 60 Hz and 120 Hz displays. Its displacement drives four things at once: weight 120–950, width from ultra-condensed to semi-expanded, up to 34% size gain, and a baseline lift.
Axis quantization
Weight is rounded to steps of 20 and applied through the canvas font string. Width is applied through ctx.fontStretch, which the Canvas API exposes only as keywords — there is no percentage form — so width lands on 6 keyword steps. Where fontStretch is unsupported, the glyph is scaled horizontally around its own centre instead. Both are quantized so neighbouring cells share a font string and hit the same glyph raster cache. Canvas cannot address the optical-size axis at all, so opsz is pinned and out of scope.
Keyboard
The field is a focusable control, so the pointer is not the only way to press it. Arrow keys move a pressure point by 32 px, or 128 px with Shift held, and pressing it as they go; Space or Enter holds the pressure where it is, which is what lets the dwell ramp deepen the well without a mouse; Escape, or moving focus away, releases it and the springs settle. Both routes feed the same intensity term, so a key and a mouse produce the same field.
Reduced motion
Under prefers-reduced-motion: reduce the animation loop never starts, the field takes neither pointer nor key input, and it is not a focus stop. It renders once as an authored composition of 3 fixed pressure points, and the HUD reports static instead of a frame rate. The preference is watched live, so changing it switches modes without a reload.
Measurement
The HUD reads the experiment’s own animation loop. FPS is a rolling average over a fixed time window; ms/frame is the p95 of inter-frame deltas over a fixed frame window, not the reciprocal of the frame rate. Both windows are declared in lib/perf/frame-metrics.ts, which every route in the lab shares, so they are not restated here. KB transfer sums the navigation and resource transferSize entries — real bytes over the wire, so a warm cache legitimately reports a small number.
Font
Roboto Flex by the Roboto Flex Project Authors, licensed under the SIL Open Font License 1.1, which permits subsetting and self-hosting. Self-hosted as a 34 200-byte woff2 (public/fonts/roboto-flex-v1.woff2, measured on this repository 2026-09-02) carrying only the wght and wdth axes and Basic Latin, built from the upstream variable TTF with:
SRC='RobotoFlex[GRAD,XOPQ,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,opsz,slnt,wdth,wght].ttf'

# 1. upstream variable TTF, SIL Open Font License 1.1
curl -gsSLO "https://raw.githubusercontent.com/google/fonts/main/ofl/robotoflex/$SRC"

# 2. keep wght + wdth, pin every other axis at its default
uvx --from 'fonttools[woff]' fonttools varLib.instancer "$SRC" \
  wght=100:1000 wdth=25:151 \
  opsz=14 GRAD=0 slnt=0 XOPQ=96 YOPQ=79 XTRA=468 \
  YTUC=712 YTLC=514 YTAS=750 YTDE=-203 YTFI=738 \
  -o instanced.ttf

# 3. subset to Basic Latin + common punctuation, emit woff2
uvx --from 'fonttools[woff]' fonttools subset instanced.ttf \
  --unicodes='U+0020-007E,U+00A0,U+00B7,U+2013,U+2014,U+2018,U+2019,U+201C,U+201D,U+2026' \
  --layout-features='kern,liga,calt' \
  --flavor=woff2 --output-file=roboto-flex-v1.woff2