Draw a shape. Hear the sound it would actually make.
Eigendrum treats whatever you draw as an ideal drumhead clamped at its rim, solves the Laplacian eigenvalue problem on that exact region with real finite elements, and synthesises the frequencies it finds. Nothing is sampled and no overtone is faked - draw a circle and the overtones come out as ratios of Bessel function zeros, because that is what a circular drum does.
Then it lets you hit it. Strike different places and the timbre changes, because striking a spot drives each mode in proportion to how much that mode moves there. Hit a line where a mode stands still and you cannot excite it at all.
You can also watch it: the real vibration modes ripple across the shape, with pale channels along the curves where the surface never moves. Those are nodal lines, the mathematical ancestors of the sand figures Ernst Chladni was drawing in 1787.
Left: mode 9 of a star. Right: something drawn by hand, struck once.
Neither has a closed-form spectrum. Both were solved from the outline alone.
Live at eigendrum.com.
No dependencies, no build step, no backend. Clone it and open index.html, or:
npm run serve # http://localhost:8080
npm test # 57 tests, including the accuracy proofs below- Strike it anywhere. Click or tap the plate. Where you hit changes the timbre, and the readout names the loudest mode along with any that stayed silent because the mallet landed on their nodal line.
- Hear one mode by itself. Press any row in the mode list. No mallet can do that, since a real strike always wakes many modes at once, but it is the only way to hear what a single eigenvalue sounds like, and it is what makes the mixture legible afterwards.
- Draw your own outline, or pick from eleven built-in forms, including both halves of the isospectral pair.
- Or write the outline as an equation.
r(t)in polar, or a parametricx(t), y(t)pair, withtsweeping one full turn. This reaches shapes a hand cannot trace accurately - eleven even lobes, a superellipse partway between a circle and a square- and it makes a shape something you can vary: change one number and hear what moved. Expressions are parsed, never evaluated as JavaScript, because a formula arriving from somebody else's link is untrusted input.
- Retune what the shape does not decide. Absolute pitch and ring-out are yours. Mallet width changes how sharply the strike is localised, and so which modes it can reach. The overtone ratios are never adjustable, because those belong to the outline.
- See the mesh the solver actually used, flexing with the membrane.
- Take it with you. Copy a link that carries the shape in the URL fragment, save
the strike as a
.wav, or the plate as a.png. A formula travels as the formula (#f=p:1 + 0.3cos(5t)), so the link is readable, editable in the address bar, and survives any later change to how curves are sampled. - Keyboard throughout. Tab to the plate and press Enter or Space to strike it at
the marked point. Every form, mode and control is reachable and labelled, contrast
meets WCAG AA, and
prefers-reduced-motionholds the peak displacement instead of animating.
The shape stays in your browser. It lives in the URL fragment, which browsers never send to a server, and analytics is configured to report the origin and path only so the shapes you make are not recorded. There is no application backend: the mesh, the solve and the audio all run on your machine. Local clones and the GitHub Pages mirror load no analytics or ads at all.
t runs from 0 to tau in radians, one full turn. Size is irrelevant, since every
outline is scaled to unit area before solving - what you hear is the shape and not
the size - so r = 0.001 and r = 5000 are the same drum.
| notation | meaning | example |
|---|---|---|
| polar | r(t), the radius at angle t |
1 + 0.3cos(5t) |
| parametric | x(t), y(t) |
3cos(t) - cos(3t), 3sin(t) - sin(3t) |
Operators are + - * / % ^ with the usual precedence, ^ right associative.
Brackets group, |x| is absolute value, and implicit multiplication is accepted, so
2t, 3cos(t) and 2(1 + t) all mean what they look like.
Available: pi tau e phi, and sin cos tan asin acos atan atan2 sinh cosh tanh exp log ln log2 log10 sqrt cbrt abs sign floor ceil round hypot pow mod min max clamp,
plus square and tri - a square wave and a triangle wave of period tau, which is
how you get teeth and facets without a piecewise notation.
Three kinds of formula are refused rather than answered, and each says which it is:
- one that has no value somewhere on the sweep,
- one whose curve crosses itself, since a crossing outline has no interior to solve
on (a negative
ris the usual cause, and it is named as such), - one too thin to mesh honestly. A hair-thin sliver has no interior nodes across its narrow direction, so it would come back with numbers, and they would be wrong.
An implicit form F(x, y) = 0 is deliberately absent. It needs contour tracing and a
rule for which contour you meant, which is a different job from parsing an expression.
Expressions are compiled by a recursive-descent parser in src/math/expr.js, never
by eval or new Function. That is not stylistic: shapes travel in the URL
fragment, so an expression is untrusted input arriving from a link somebody else
wrote, and handing that to a JavaScript evaluator would make every shared drum a
script-injection vector.
Mark Kac asked exactly that in a famous 1966 paper. If you know every frequency a drumhead can produce, can you deduce its outline?
In 1992 Carolyn Gordon, David Webb and Scott Wolpert answered no, by constructing two different shapes with identical spectra. Both are built into Eigendrum as Kac drum I and Kac drum II. Each is made from the same seven right-isosceles triangles, rearranged. One looks like a hook and the other like an arrow. They enclose the same area and the same perimeter, and every single frequency matches.
Both drums are solved, and the app reports the agreement it measured rather than asserting the theorem. The partner drum's spectrum is drawn above the same axis as this one, so you can see the ticks coincide.
Switch between them and listen. This is not an approximation that happens to come out close:
k drum I drum II difference
1 2.54398772 2.54398772 0.00000%
2 3.66297335 3.66297335 0.00000%
3 5.19087452 5.19087452 0.00000%
...
12 15.95243552 15.95243552 0.00000%
Both drums have only axis-aligned and 45-degree edges on integer coordinates, and the mesher reproduces both of those directions exactly, so the two discrete problems are isospectral in exact arithmetic too. Run it yourself:
node tools/isospectral.mjsA membrane clamped at its boundary can only vibrate in certain shapes at certain frequencies. They are the solutions of
−∇²u = λu inside Ω, u = 0 on ∂Ω
Each eigenfunction u is a standing wave; each eigenvalue λ gives a frequency
proportional to √λ. For almost every shape there is no formula, so Eigendrum
solves it numerically:
- Mesh. Overlay a lattice of right-isosceles triangles, keep the triangles whose centroid is inside, project the resulting boundary onto the true outline, then repair it (slide boundary nodes along the outline to even out their spacing, drop the degenerate splinters that snapping leaves behind, smooth the interior).
- Assemble. P1 linear elements give the stiffness matrix
Kand the consistent mass matrixM. Dirichlet conditions are imposed by never assembling rows for boundary nodes. - Solve. The lowest 16 eigenpairs of
Kφ = λMφ, by block inverse iteration with a Rayleigh–Ritz projection. Inverse iteration because we want the bottom of the spectrum, and plain Lanczos converges to the top. - Listen. Frequencies from
√λ, per-mode amplitudes from projecting the mallet onto the mode shapes, then a sum of decaying sinusoids.
The step from projection to amplitude is where a struck membrane gets its voice, and it is easy to get wrong. Three factors apply, and only the last is a choice:
- Mass normalisation.
c_k = ∫φ_k gis the modal coefficient only when the modes are orthonormal in the mass inner product. The solver normalises them to unit peak instead, for the colour map's sake, so the projection is divided by∫φ_k². That varies by a factor of about two across the first sixteen modes of a disk. 1/ω_k. A mallet delivers an impulse of force, which sets the membrane's initial velocity, not its displacement. Solvingu_k(0) = 0,u_k'(0) = a_kgivesu_k(t) = (a_k/ω_k) sin ω_k t, a 6 dB/octave rolloff.- Contact time. No beater is an impulse. A force pulse lasting
Tcannot pump a mode whose period is far shorter thanT, modelled here as a one-pole rolloff fixed at a ratio of the fundamental so the timbre does not shift with the pitch control.
Damping is Rayleigh damping, C = αM + βK, which is the standard proportional
model for a system like this one and in modal coordinates reads
1/τ_k = α + βω_k². Loss growing with the square of frequency is why a drum's
high inharmonic partials vanish in tens of milliseconds while the fundamental
rings on, and that fast darkening is most of what makes a drum read as a pitched
thud rather than a chord. The brightness control moves weight between the two
terms.
The eigensolver needs a few hundred solves of K y = b, so K is reordered with
reverse Cuthill–McKee and factorised once with a banded Cholesky. After that each
solve is two triangular sweeps. A 2000-unknown drum solves in about 700 ms in a
browser worker.
A handful of shapes have spectra that can be written in closed form, and the test
suite checks the solver against them on every change. This is measured, not
asserted - reproduce it with npm run bench:
| shape | exact spectrum | 1200 nodes | 2600 | 6000 |
|---|---|---|---|---|
| unit square | π²(m² + n²) |
0.846% | 0.375% | 0.160% |
| rectangle 1.5 × 0.8 | π²(m²/a² + n²/b²) |
1.141% | 0.548% | 0.233% |
| right triangle | square modes with m ≠ n |
1.103% | 0.519% | 0.227% |
| unit disk | squared zeros of J_m |
0.946% | 0.437% | 0.192% |
Worst relative error over the lowest 8 modes. The errors fall in the ratio
1 : 0.47 : 0.21 against predicted h² ratios of 1 : 0.471 : 0.207 - clean
second-order convergence.
Two further checks worth naming:
- Every error is positive. A conforming finite element method minimises the Rayleigh quotient over a subspace of the true space, so it can never undershoot. An eigenvalue below the exact one would mean a bug, not a coarse mesh, and the suite asserts it never happens.
- The strike model reproduces physics nobody coded in. Striking a circle dead centre excites the radially symmetric fundamental hard but leaves the next two modes essentially silent, because they have a nodal diameter straight through the centre. That falls out of the projection, and it is a test.
Being clear about this is the point of the project:
Determined by the shape, and not adjustable. The frequency ratios. The mode shapes. Which modes a given strike position can excite. And the pitch the fundamental lands on, relative to the reference below: shapes are scaled to equal area before solving, so what is left in lambda_1 is shape rather than size, and it varies by about six semitones across the built-in presets. By Faber-Krahn the disk is the lowest of them all, so a round drum really is the deepest drum of its area.
Not determined by the shape, so exposed as controls. The wave speed
c = sqrt(T / rho), which is tension and density: the pitch slider sets it by
naming the note a unit-area disk would sound. Every other outline is then placed
above that by its own lambda_1, so the slider is a reference rather than the
fundamental you hear. How fast each overtone fades (material and air).
Modelled, and neither of the above. The mallet: its width, which is a control, and its contact time, which is fixed. Both decide how much of each mode a strike can reach, never at what frequency a mode sits.
index.html the whole page: shell markup, the About dialog, and the head's
canonical tag, social cards and JSON-LD. No application logic
robots.txt crawl directives, answer engines allowed on purpose
sitemap.xml one URL, because shapes travel in the fragment and fragments are
not separate resources
llms.txt a plain-prose summary for answer engines, with the measured
accuracy figures and the Kac story
styles/ all styling, plus the typeface as a base64 data URI
src/math/ linalg, sparse CSR, banded Cholesky + RCM, eigensolver, Bessel,
closed-form spectra, the expression parser
src/geom/ polygon utilities, the mesher, equations to outlines
src/fem/ P1 assembly, and the pipeline that ties it together
src/audio/ modal synthesis, WAV encoding, note naming
src/app/ DOM, canvas rendering, input, presets, sharing
src/worker/ runs the mesher and solver off the main thread
tools/ dev server, accuracy bench, isospectral check, browser smoke tests
tests/ node --test
docs/ the images this README embeds
src/math, src/geom and src/fem never touch the DOM, which is why they can be
tested in Node and run in a worker. The worker owns every expensive step, so the
interface stays responsive while a drum solves.
The typeface is embedded as a data URI rather than linked, because Chrome refuses
font subresources over file:// and this has to work from a bare filesystem.
npm run serve # dev server on :8080
npm test # unit tests, including the accuracy proofsPuppeteer is a dev dependency, used only by the browser tests, and never loads in the browser. The shipped app has zero runtime packages: every application module and local asset resolves inside this repo. On the deployed Vercel site, the two first-party Vercel telemetry scripts load from the platform endpoints.
See CONTRIBUTING.md for the rest of the scripts (accuracy benchmarks, the isospectral check, browser smoke tests) and how advertising is wired up on the deployed site.
Copy the repo to any static host. There is no build step, no server-side anything, and no environment to configure. GitHub Pages, Netlify, S3, a USB stick.
The canonical host is eigendrum.com. index.html declares
<link rel="canonical"> pointing there, and robots.txt, sitemap.xml and
llms.txt all name it. That matters because the repo still deploys to GitHub Pages
as well: the redirect off baselashraf81.github.io/eigendrum is written in
JavaScript, which no crawler that skips scripts will ever run, so without the
canonical tag the two hosts compete as duplicates and neither earns the credit. If
you fork this to your own domain, change the host in those four places.
- M. Kac, Can One Hear the Shape of a Drum?, American Mathematical Monthly 73 (1966). JSTOR
- C. Gordon, D. Webb, S. Wolpert, One cannot hear the shape of a drum, Bulletin of the AMS 27 (1992).
- T. Driscoll, Eigenmodes of Isospectral Drums, SIAM Review 39 (1997). SIAM - the source of the coordinates used for the two Kac drums.
- Hearing the shape of a drum on Wikipedia, for the wider history.
Free to use, with no account and nothing to install. The deployed site is ad-supported
to cover the cost of the domain; see privacy.html for what runs and
why, or CONTRIBUTING.md for how it's wired into the code. None of it
applies to a local clone: npm run serve strips ad and analytics tags from every page.
If you'd rather it stayed ad-free, or just want to put something toward it: ko-fi.com/baselashraf. For anything wrong with the maths or the interface, open an issue. For advertising or partnership enquiries: u2679054@uel.ac.uk.
MIT. See LICENSE



