Skip to content

fugitivexyz/nodal

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 

Repository files navigation

Nodal

A cymatic sand table you play in the browser.

Nodal renders ~15,000 luminous grains that walk down the amplitude gradient of a real Chladni standing-wave field and settle on its nodal lines — the dark curves where the surface doesn't move. Sweep the drive frequency with your cursor and a formless dust cloud snaps into a precise figure; a sine oscillator plays the exact frequency the grains are dancing to. It's one self-contained index.html — vanilla JavaScript, Canvas2D, and Web Audio, no build step and no dependencies. It runs offline by double-clicking the file.

It is built around one idea: the visible figure is honest physics, not a canned animation. Every nodal line a grain lands on is a zero of an equation you can read in the source, and the pitch you hear is proportional to that mode's true eigenfrequency.


The physics

Grains follow a gradient descent on the field magnitude |F|, moving along −F·∇F (which is −½∇F²). The force vanishes as F → 0, so grains decelerate and settle exactly on the nodal lines. A temperature term — random jitter scaled by |F| · amplitude — keeps antinodes (high |F|) agitated while nodes stay calm, so the lines self-assemble out of noise. Two plate geometries share this same solver:

Square plate

f(x, y) = cos(nπx)·cos(mπy) − cos(mπx)·cos(nπy)        on the unit square

36 modes (integer pairs n < m, both in 1…9), ordered by eigenfrequency ω = √(n² + m²). Displayed frequency is 90 · ω, which lands the lattice in an audible ~201–1084 Hz.

Square-plate eigenfrequencies are degenerate: distinct modes can share the same ω (e.g. (1,8) and (4,7), both √65). Those modes sit next to each other in the sorted list, so scrubbing through them morphs the geometry while the pitch holds constant — the honest way to experience degeneracy, with no extra gesture. A marker in the readout flags when you're inside a degenerate cluster.

Round plate

u(r, θ) = J_m(k·r)·cos(mθ)        on the disc,  with  k·R = α_{m,s}

The Chladni modes of a clamped circular membrane (a drumhead — a Helmholtz problem, not a biharmonic metal plate). J_m is the order-m Bessel function of the first kind, and α_{m,s} is its s-th positive zero, which fixes the wavenumber so the rim is always a node. The figures are concentric rings (one per interior zero), m nodal diameters, and the rim circle. 28 modes (m = 0…6, s = 1…4) ordered by α; displayed frequency is 53 · α, giving ~127–1077 Hz.

On honesty. The round plate is a membrane model. A real metal Chladni plate is a stiff biharmonic (∇⁴) plate with different radial solutions and frequency spacing; the membrane is the standard, honest interactive choice and is labelled as such in the source and the in-app equation card. The displayed Hz is proportional to each geometry's true eigenfrequency (a constant chosen for audibility), not an absolute physical pitch.


Playing it

  • Press and drag the plate — horizontal = pitch (which mode), vertical = intensity.
  • Release — a "harmonic stop": the figure crystallizes as the grains settle and the tone fades.
  • Hold Shift while dragging — lock intensity, sweep pitch cleanly.
  • Tap — pour a fresh clump of grains at the cursor; it disperses into the figure.
  • Listen mode — sing, whistle, or play a tone and the plate resonates at the nearest natural frequency it hears.
Key Action Key Action
G Square ⇄ round geometry L Listen (microphone)
A Auto-sweep (hands-free tour) I Show the field equation
14 Palette R Reseed grains
S Save plate as PNG H Hide all UI (clean view)
Space Mute audio ←→ ↑↓ Fine pitch / intensity

A hairline control rail (top-right ) holds the same controls plus a four-slot rack for capturing and recalling favorite plates (saved to localStorage, with per-slot geometry-aware thumbnails).

Listen mode

Detects the dominant pitch via an FFT peak (with parabolic sub-bin refinement, an octave guard, a noise gate, and median + EMA smoothing), then snaps the scrub to the nearest mode in log-frequency space. Input loudness drives the amplitude. The synth oscillator mutes while listening so you hear only your own input (and there's no acoustic feedback), and a ◉ REC glyph shows in the readout whenever the mic is live. It only requests microphone access on an explicit press, and surfaces the real error if access is denied.

Palettes

Four density ramps named after photographic processes — Bronze Dust, Selenium, Cyanotype, Platinum. Color is carried entirely by grain density; there is no second color channel.


How it works

grains (typed-array SoA) → gradient-descent step → splat into a 640² float
accumulation buffer → log/Reinhard tonemap through a 1-D palette LUT →
drawImage-upscale to the device-pixel canvas
  • Grains live in four Float32Arrays (x, y, vx, vy). Physics is closed-form per grain — cheap trig for the square plate; for the round plate, a 513-point radial lookup table of J_m(k·r) and J_m'(k·r) is built once per mode so there are zero Bessel evaluations per grain.
  • Bessel functions use Miller (downward) recurrence with Neumann normalization — the stable method for n > x — and McMahon-seeded Newton iteration for the zeros. Verified against reference values to ~1e-11.
  • Rendering is decoupled from display resolution: the accumulation buffer and tonemap run at a fixed 640², then a single drawImage upscales to the DPR canvas. The tonemap uses a fixed white-point so the figure never flickers or blows out. A frame-time guard degrades accumulation resolution first, grain count second.
  • Audio is one sine oscillator (plus a quiet octave overtone for small speakers) whose frequency equals the displayed Hz, ramped with setTargetAtTime. The AudioContext is created lazily on first gesture (autoplay-safe).
  • The round plate's disc emerges from grain density falling to zero past the rim (grains reflect off the circle), not from a mask.

Single file, ~1,050 lines, ~54 KB. Hot path measures ~2.5 ms/frame at 15k grains.


Design principles

The aesthetic is instrument-grade scientific apparatus, not an arcade demo. The plate is the only hero; controls hide in a hairline rail and equipment-style labels until you reach for them. One warm accent carries the grains; dim cyan is reserved strictly for hairline UI. No gradients-as-decoration, no glassmorphism, no emoji, a single offline monospace stack, and motion lives almost entirely in the grain physics. prefers-reduced-motion is respected.


Running it

  • Double-click index.html. It's fully self-contained and works offline over file:// (it uses a classic <script>, not an ES module, specifically so this works).
  • Or serve it: python3 -m http.server 8000 then open http://localhost:8000/.

Needs a modern browser with Canvas2D. Audio and microphone are optional — the simulation runs silently without them, and a fallback notice appears if Canvas is unavailable.


Provenance

Concept, physics, and architecture were chosen and pressure-tested with a verification-first process: the circular-membrane gradient (including the easily- mistaken sign asymmetry between ∂u/∂x and ∂u/∂y), two independent Bessel implementations cross-checked against each other and known values, and the microphone DSP recipe were all reviewed before being written, and every claim above — Bessel accuracy, the rim-is-a-node boundary condition, frame rate, and the frequency mappings — was confirmed live in the browser.

About

An interactive cymatic sand table — ~15k grains settle on the nodal lines of a real Chladni field (square plate + circular membrane). Vanilla JS, Canvas2D, Web Audio. Runs offline.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages