Guide · how it was made
Solving the shapes a circle is allowed to make
EIGENMODE is a study of the vibration modes of a clamped circular drumhead — its Bessel eigenmodes. A live three.js membrane displaces itself by the exact mode equation while WebAudio voices each shape's own inharmonic pitch. Its single job: let you see and hear the finite alphabet of shapes one drum can hold.
The figure
Mode J₂₍₁₎ — two diameters, one clover
A static rendering of the resting mode: two nodal diameters split the head into four lobes that alternate in phase. Gold pushes toward you, violet away; the dark seams are the nodal lines that never move, and the rim is clamped flat.
Palette
Six tones, argued from the membrane
The two brights are reserved for lines and marks only — nodal grid, rim and antinode peaks. Body copy runs on a derived light ink that clears 4.5:1 comfortably on the dark ground, so nothing legible ever leans on an accent.
Type
A resonant serif, a laboratory mono
Technique
How the membrane is solved
The surface is a custom polar BufferGeometry — a ring-and-sector grid clamped
at radius 1. Each vertex is displaced by the drumhead equation
u = J_m(α·r)·cos(mθ)·cos(ωt), where J_m is the Bessel function of the
first kind (an ascending power series in code), α is the n-th zero of
J_m, and the rim stays flat because J_m(α)=0 by definition. Modes are
ordered by their zero α, so stepping through them walks the drum's real overtone series —
ratios 1.000, 1.593, 2.136, 2.295… which are audibly inharmonic. Vertex colour is taken from the
static shape's sign (gold for positive lobes, violet for negative), so nodal lines fall out as
dark seams. A coarse deforming line grid and additive glow-points on a procedural canvas sprite
mark the structure and the pulsing antinodes; WebAudio strikes a sine plus two inharmonic
partials with an exponential decay for the membrane's voice.
- Membranethree.js MeshStandardMaterial, per-vertex Bessel displacement, gold+violet point lights, per-frame normals.
- Nodal gridCoarse ring/spoke LineSegments in the violet mark token, riding the same displacement — the "mode lines."
- AntinodesAdditive Points on a procedurally-drawn radial sprite, placed at local maxima, brightening with displacement.
- VoiceWebAudio struck tone (sine + 2 inharmonic partials, exp decay), initialised on first gesture; visual runs silently.
- ResilienceDPR capped at 2, resize-aware, rAF paused on hidden tab, WebGL context-loss guard and a static SVG fallback.
Process
Three iteration passes
Copy over the live membrane was losing contrast, so hero and console text gained soft dark
scrims (text-shadow) to hold legibility without hiding the surface. A thin canvas sliver was
bleeding through the gap above the footer — closed by dropping the footer's top margin. The
two brights were demoted to strict line/mark tokens and a derived #CBCDDA body
ink (~11:1) was minted for all copy.
Added the pulsing antinode glow-points — gold where the surface rises toward you, violet where it falls away — riding the peaks and brightening at maximum displacement, which makes the physics legible at a glance. Each mode change now fires a subtle strike-overshoot (amplitude jumps to 1.35× and eases back) synced to the audio pluck, so the membrane visibly reacts to being struck.
Confirmed prefers-reduced-motion renders a settled full-amplitude mode — a real
shape, never a blank — with no animation loop. Verified 375px has no real overflow (wordmark
and nav sit inside the viewport box, scrollWidth clean). Re-checked DPR cap, resize, hidden-tab
pause and context-loss guard, and a headless Chrome run at 7s came back with zero console
errors while a driven mode-step changed the rendered pattern.