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. WherefontStretchis 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, soopszis 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: reducethe 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 resourcetransferSizeentries — 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 thewghtandwdthaxes 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