Field 150 · The Making Of

Fresnel

How a single fragment shader turns two coherent point sources into a living pattern of constructive fringes and destructive nulls — and how three iteration passes made it read.

The concept / 01

Two waves, added. Where their crests meet, the space between them fills with rules.

Fresnel is a study in superposition. Two point sources ripple outward across a dark optical bench; wherever the path from one is a whole number of wavelengths longer than the path from the other, the crests reinforce into a bright antinode. A half-wavelength off, they annihilate into a black null.

The audience is anyone who has heard "the double-slit experiment" and never watched it move. The page's single job: make the invisible arithmetic of interference visible, tangible, and adjustable — drag a source and watch the fringe spacing breathe.

Palette / 02

Optics-black, one teal for the wave, and a near-white reserved for the antinode.

The field is drawn in light on black, so the ground is nearly the darkest usable value and the accent is spent only where the physics is brightest. Body copy runs on a derived cool ink — never the teal line/mark colour — to hold contrast on the dark bench.

Optics-black#070A10 · ground

The unlit bench. Nulls fall to this, so the fringe contrast is maximal.

Wave#57C6D8 · line/mark

The travelling crest. A cool cyan reads as coherent, monochromatic light — used only for wavefronts and marks, never body text.

Antinode#DCEFF2 · highlight

Constructive maxima and headings. The brightest value, held in reserve for the strongest reinforcement.

Aperture-ink#B7C6CE · body

Derived body ink, ~9:1 on the ground. Legible copy without borrowing the wave accent.

Type / 03

A geometric display face for the name; a monospace for every measured value.

Sora
Display · 300 / 800

Clean, slightly technical geometry. The 800-weight wordmark carries the boldness; the 300-weight tagline stays quiet beneath it — one moment of scale bravery, disciplined everywhere else.

IBM Plex Mono
Instrument · 400 / 500

Every readout — wavelength, separation, fringe pitch — is set in tabular monospace so the numbers tick without reflowing. It reads as lab instrumentation, not marketing.

The signature / 04

One fragment shader, evaluated per pixel: the sum of two travelling waves.

GLSL two-source interference field

WebGLGLSL fragment shaderfull-screen trianglepointer dragno libraries

A single full-screen triangle runs a fragment shader that, for every pixel, measures the distance to each source (d1, d2), computes each wave as amp · cos(k·d − ω·t) with a 1/√(1+d) falloff, and simply adds them. The instantaneous sum squared gives the shimmer; a fixed envelope |cos(½·k·(d1−d2))|² keeps the nodal hyperbolas permanently black so the fringes stay legible even in a frozen frame. Two uniforms carry the source positions; dragging rewrites them each pointer-move, and the whole pattern re-knits in real time. The wavelength slider maps to the wavenumber k, so shorter waves pack the fringes tighter. No three.js, no GSAP — raw WebGL.

Reading the numbers

Path difference Δr = r₁ − r₂ sets everything: bright where it equals a whole number of wavelengths, dark at a half-integer. The corner readout derives wavelength, source separation and angular fringe pitch live from the shader's own uniforms, so the panel and the picture can never disagree.

Iteration log / 05

Built, then combed three times — craft, depth, hardening.

Pass 1 — Craft

Making the drag actually grab

Driving the signature immediately surfaced two silent defects that a load-only probe would have missed:

  • The content layer (.wrap, pointer-events auto) sat above the canvas and swallowed every pointerdown — the sources looked draggable but weren't. Moved hit-testing to a window capture-phase handler in field-space, filtering out real links and inputs.
  • layoutSources() ran before the first resize, so it placed the pair using an aspect of 1 and they drifted left of centre. Re-ordered init so the layout runs once the true aspect is known — the sources now sit centred with the correct separation readout.
  • Tuned the brightness curve (steady + 1.7·inst, soft tone-map) so antinodes glow without clipping and nulls stay ink-black.
Pass 2 — Depth

Tactile feedback and a second read

  • Added a grab response: while a source is held, a u_grab uniform lifts its core glow ~70%, so the pixel you are dragging visibly answers the cursor.
  • Leaned into the physics as the second-read detail — the permanent bright central maximum (equal path difference) anchors the composition, and the nulls resolve into true hyperbolas the longer you watch.
  • Added an emitter pulse and IBM Plex Mono live readouts (λ, separation, fringe pitch) that update on every drag and slider move, plus arrow-key control of a source for keyboard users.
Pass 3 — Hardening

375px, reduced motion, and the Chanel rule

  • Mobile legibility: on narrow screens the copy column overlaps the bright central max, so the hero scrim swaps from a left-radial to a full vertical gradient and the body ink lifts to near-white — copy stays readable over the field.
  • Resize recovery: a narrowing viewport could clamp both sources onto the same edge and collapse the separation to zero; the resize handler now re-centres them if that happens.
  • Reduced motion renders one settled interference frame and stops the loop — a static pattern, never a blank. DPR capped at 2, rAF paused on document.hidden, WebGL context-loss falls back to a CSS gradient.
  • Removed one ornament (Chanel rule): the hard emitter ring around each source doubled the core halo, so it was cut — the soft glow alone marks the point sources.