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 using 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's what a circular drum actually 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 # 61 tests, including the accuracy proofs below- Click or tap the plate to strike it anywhere. 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.
- Press any row in the mode list to hear that one mode by itself. 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.
- Write the outline as an equation instead:
r(t)in polar, or a parametricx(t), y(t)pair, withtsweeping one full turn. This reaches shapes a hand cannot trace accurately, like eleven even lobes or 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. - Move the parts that aren't fixed by the outline. The pitch reference and ring-out are yours. Mallet width changes how local the strike is, and which modes it reaches. The overtone ratios stay tied to the outline.
- Watch the mesh the solver actually used, flexing with the membrane.
- 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. - Use it from the keyboard. 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.
Your shape never leaves your browser. It lives in the URL fragment, which browsers never send to a server, and the site's analytics only logs which page you're on, never what you drew. There's no backend: meshing, solving, and audio all happen on your machine. A local clone or the GitHub Pages mirror doesn't load analytics or ads at all.
t runs from 0 to tau in radians, one full turn. Every outline gets scaled to
unit area before solving, so size doesn't matter: what you hear is the shape, not
the size, and 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 it: 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 it: P1 linear elements give the stiffness matrix
Kand the consistent mass matrixM. Dirichlet conditions are imposed by never assembling rows for boundary nodes. - Solve it: 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 to it: 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. - The
1/ω_krolloff. 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, which is 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.
This matters because it tells you which parts of the sound come from the outline and which are settings.
The shape decides the frequency ratios, the mode shapes, and which modes a given strike position can wake up. It also decides where the fundamental sits relative to the pitch slider: every shape gets scaled to the same area before solving, so whatever's left in lambda_1 is genuinely about the shape rather than its size. That swings by about six semitones across the built-in presets, and by Faber-Krahn's inequality the disk always comes out lowest: a round drum really is the deepest drum for its area. None of this is adjustable, because none of it should be.
What you do get to touch: the wave speed c = sqrt(T / rho) (tension and
density), which the pitch slider sets by naming the note a unit-area disk would
ring at. Every other shape then lands above or below that on its own, so the
slider is a reference point, not a promise about what you'll hear. You also
control how fast the overtones fade (material and air).
The mallet sits outside both categories: its width is a control, its contact time is fixed, and it only changes how much of each mode a strike can reach, never what frequency that mode rings at.
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. No longer deployed on vercel.
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.
Two real numbers in the footer, both hidden until they resolve rather than shown as placeholders, and both only run on the deployed host.
- Visits since launch is an aggregate page-load count stored in Cloudflare D1.
Each production page load increments it once through
/api/visit;/api/visitsreads the current total. It stores no cookie, IP address, or browser identifier, so it counts visits rather than unique people. If the database is unavailable, the footer stays hidden instead of showing a made-up number. - People here right now comes from a completely separate WebSocket server in
presence-server/, deployed on its own VPS rather than on Cloudflare Pages, because counting concurrent connections needs a persistent process. Seepresence-server/README.mdfor how it is deployed and wired up. If that server goes down, the count just disappears from the footer; the rest of the site does not depend on it.
Eigendrum has been covered across Hacker News, Reddit, YouTube, and international technology publications:
| Source | Coverage |
|---|---|
| Clive Thompson | Linkfest #48 feature by Wired & NYT Magazine contributing writer |
| Wikipedia | Hearing the shape of a drum external links |
| Hacker News | Front page, twice. 200+ points. |
| kottke.org | Editorial post by Jason Kottke. |
| Nele Hirsch | Mastodon post by notable German educator & open-education advocate |
| r/InternetIsBeautiful | "Draw any shape and hear what it would sound like" |
| r/oddlysatisfying | "Every shape you draw becomes a drum" |
| Sidebar.io | Curated design & dev newsletter |
| remio.ai | English analysis: physics accuracy |
| YouTube (×2) | Two independent video reviews |
| drweb.de | Major German web design editorial |
| trendshift.io | Trending repositories chart |
| Website Design Awards | Winner's badge |
| Vostok Magazine | Russian editorial |
| diginews.fr | French developer digest |
| ds106 / NomadWarMachine | TDC5346 educational drum simulation |
Plus coverage in Spanish, Chinese, Japanese, Russian, and Dutch aggregators; two Facebook group posts; Mastodon and Bluesky reposts by kottke.org.
- 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. No ad network currently runs on
the deployed site; the reserved ad slots instead carry a plain sponsorship pitch with
no third-party script. See privacy.html for what actually 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 like to put something toward the domain and hosting: ko-fi.com/baselashraf. For anything wrong with the maths or the interface, open an issue. For hiring or partnership enquiries: bosbos.basel@gmail.com.
See LICENSE. Code from before this licence took effect remains available under the MIT licence to whoever already had it. Code from here forward is free to use, copy, modify, and redistribute for non-commercial purposes, with visible credit to Basel Ashraf and Eigendrum and no presenting a copy as an unrelated, original work. Commercial use needs a separate licence from Basel Ashraf. The Eigendrum name and logo are not part of this grant either way; see TRADEMARK.md.



