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 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.
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.
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 2π, 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,
Jn(αn,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.
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.
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 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.