Everything the player draws — the shapes, the frequencies, the way the sand moves — comes out of two eigenvalue problems and one special function. Here is where each number in js/cymatics.js comes from.

A standing wave, and why sand finds it

A plate or a membrane obeys a wave equation: a rule for how its displacement u(x, y, t) accelerates in response to how it is curved. A standing wave is a solution that does not travel — it factors as u(x, y, t) = φ(x, y) cos(ωt), a fixed shape φ that scales up and down in place. Substituting that guess turns the time derivative into −ω² and collapses the wave equation into an eigenvalue problem in two variables: find the shapes φ and the frequencies ω for which the object can ring at all. Those shapes are the modes, and the curves where φ = 0 are the nodes — they never move, at any t, so a grain of sand that gets close to one has nothing left to push it away.

The square plate: a fourth-order equation

A membrane resists being curved because it is stretched, like a drum skin. A plate resists because it is stiff, like a sheet of steel, and stiffness responds to the curvature of the curvature — a fourth derivative, not a second one. Sophie Germain worked out the equation for that in 1815:

D ∇⁴u = −ρh ∂²u / ∂t²

where ρ is density, h is thickness, and D = Eh³ / (12(1−ν²)) is the flexural rigidity, built from Young's modulus E and Poisson's ratio ν — exactly the four numbers sitting in the PLATE constant and the rigidity() function in the code. For a product mode φ = cos(mπx/L) cos(nπy/L) on a square of side L, the Laplacian returns −λ²φ with λ² = π²(m²+n²)/L², and applying it twice for the biharmonic operator squares that eigenvalue again. Plugging back into Germain's equation and solving for the frequency gives

f = π(m²+n²) / (2L²) · √(D / ρh)

which is squareFrequency(m, n, side) in the code, term for term. The important shape of that formula is that frequency climbs with m²+n², not with m+n: a stiff plate's overtones are spaced very differently from a membrane's, which is the whole reason the two objects in this player sound and look so different for the same slider position.

The true boundary conditions for a free plate edge are messier than a clean product of cosines — they require the bending moment and the shear to vanish along the border, not the displacement. The cosine product is the classic simplification used to reproduce Chladni's figures in closed form, and it is why the code's own comment calls it "Chladni's square plate," not an exact free-plate solution. What it gets right is the part that matters for the picture: for m ≠ n, swapping the two mode numbers gives a different shape at the same frequency — m²+n² does not care which is which. Two degenerate modes at one frequency mean any mix of them is also a valid mode at that frequency, and which mix actually appears depends on exactly where the bow excites the plate. That is what the Superposition slider is: it sweeps

cos(nπx) cos(mπy) + t · cos(mπx) cos(nπy),  t ∈ [−1, 1]

through every figure that one frequency can produce. When m = n the two terms are identical, there is nothing to superpose, and the slider does nothing — which is exactly what squareAmplitude checks before bothering with the second term.

The drumhead: Bessel's equation

A membrane has tension but no stiffness, so it obeys the plain second-order wave equation ∂²u/∂t² = c²∇²u, with c the speed of a wave crossing the stretched skin. Separating variables in polar coordinates u = R(r) Θ(θ) cos(ωt), the angular part must repeat every , which forces Θ(θ) = cos(nθ) for an integer n. What is left for the radial part is Bessel's equation of order n:

r²R″ + rR′ + (k²r² − n²)R = 0

Its solution that stays finite at the center, r = 0, is the Bessel function of the first kind, Jn(kr) — the other independent solution blows up there and is discarded, since a drumhead has no hole in the middle. Clamping the rim, R(a) = 0, means k·a has to land on one of Jn's zeros: call the s-th positive one αn,s. Then k = αn,s/a, and since ω = ck, the frequency is

f = c αn,s / (2πa)

which is circleFrequency(n, s, radius), with MEMBRANE.speed standing in for c and radius = size / 2. The mode shape itself, Jnn,sr/a) cos(nθ), is what circleAmplitude evaluates: n counts the nodal diameters, where cos(nθ) crosses zero, and s counts the nodal circles, where Jn does. Unlike the plate, this frequency grows with the Bessel zero itself, not its square — a membrane's overtones are much more tightly packed than a plate's.

Bessel functions, computed from nothing

There is no elementary closed form for Jn, so it is built straight from its power series:

Jn(x) = ∑k=0 (−1)k / (k! (k+n)!) · (x/2)2k+n

besselJ walks that sum term by term, updating each term from the last with a single multiply-and-divide rather than recomputing factorials, and stops once a term is too small to move the total — in practice a few dozen terms for the values this player needs.

The zeros αn,s have no formula either. besselZeros finds them the way you would with a pencil and a table of values: step along x in small increments, watch for the sign of Jn to flip, and once it does, bisect that bracket sixty times to pin the crossing down to full floating-point precision. It is slow work done once — the first nine zeros of each order are cached in ZEROS the first time they're asked for, since every animation frame reuses them.

From a formula to a grain of sand

None of the sand simulation needs a closed form — it only asks the mode shape a local question. For a grain at (x, y), the player samples |φ| at four nearby points to get a finite-difference gradient, then divides the amplitude there by the length of that gradient. That quotient behaves like "distance to the nearest node" whether the wave is steep, near the middle of a drumhead, or shallow, near its rim, which a raw amplitude value would not. Each grain nudges itself downhill along that gradient, with more jitter the further it still has to go, and stops once it's within a hair's width of zero — which is the whole physical story: sand is thrown from where the plate moves and left alone where it does not.

The constants behind the numbers

The frequencies you hear are for specific, idealised objects, set at the top of js/cymatics.js: a steel plate (E = 200 GPa, ρ = 7850 kg/m³, ν = 0.3, h = 1 mm) and a membrane with wave speed c = 100 m/s, about right for a drum head under playing tension. Change those four plate numbers or that one membrane number and every frequency in the spectrum moves with them — the shapes would not change at all, since they come from the mode numbers alone, only where each one sits on the frequency slider would.