eigendrum

a study of the vibrating plane

try an equation

reference

Writing a drum as an equation

Besides tracing an outline with a pointer, you can write one. This reaches shapes no hand traces accurately, and it makes a shape something you can vary: change one number and hear what moved.

There are two notations. In both, t sweeps from 0 to tau - one full turn, in radians.

notationmeaningexample
polar r(t), the radius at angle t 1 + 0.3cos(5t)
parametric x(t), y(t) 3cos(t) − cos(3t)

Size is irrelevant. Every outline is scaled to unit area before solving, because what you are meant to hear is the shape and not the size - so r = 0.001 and r = 5000 are the same drum, and produce the same frequency ratios.

Operators

+ − * / % ^ with the usual precedence, and ^ is right associative. Brackets group. |x| is absolute value. Implicit multiplication is accepted, so 2t, 3cos(t) and 2(1 + t) all mean what they look like.

One exception, and it is worth knowing because it will bite you. A bar is both an opening and a closing delimiter, so | cannot also start an implicit product. Write 2|t| as 2*|t|. Ordinary written mathematics has the same ambiguity and resolves it by context; a parser with one token of lookahead does not have that luxury.

Available functions and constants

Constants: pi, tau, e, phi.

Functions: 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 two that are not standard library functions but earn their place: square and tri, a square wave and a triangle wave of period tau. They are how you get teeth and facets without needing a piecewise notation.

Worked examples

A five-lobed flower

r = 1 + 0.3cos(5t)

The classic starting point. The 5 sets the number of lobes and the 0.3 sets how deep they are. Change the 5 to an 11 and you get eleven even lobes - a shape that is essentially impossible to trace accurately by hand, and one whose spectrum is visibly different.

A superellipse, part way between a circle and a square

r = (|cos(t)|^4 + |sin(t)|^4)^(−0.25)

Raise the exponent and it approaches a square; lower it and it relaxes toward a circle. This is the most direct way to hear what "cornerness" does to a spectrum, because you can move continuously between two shapes that both have closed-form answers.

A nephroid, parametric

x = 3cos(t) − cos(3t)  ·  y = 3sin(t) − sin(3t)

Parametric reaches closed curves that polar cannot express at all - anything where the radius is not a single-valued function of angle. Kidney shapes, eggs, and lopsided blobs live here.

A toothed wheel

r = 1 + 0.12square(9t)

Sharp steps rather than smooth lobes. Worth comparing directly against 1 + 0.12cos(9t), which has the same number of features and the same amplitude but no corners - the difference in the high end of the spectrum is the corners talking.

Three kinds of formula that get refused

Each refusal says which one it is, and each exists for a reason rather than out of caution.

An implicit form F(x, y) = 0 is deliberately absent. It needs marching squares, contour tracing, and a rule for deciding which contour you meant - a different job from parsing an expression, and a much larger one.

Formulas travel as text

A written shape is stored in the URL as its own source, not as encoded geometry:

eigendrum.com/#f=p:1 + 0.3cos(5t)

So a shared link is something you can read, retype, and edit in the address bar, rather than a few hundred characters of base64. It also means the link survives any later change to how curves are sampled, which an encoded outline would not. Editing the hash directly works - the page listens for it.

Why expressions are never evaluated as JavaScript

Expressions are compiled by a recursive-descent parser. There is no eval, no new Function, and no template-driven code generation anywhere in the path.

That is not stylistic preference. Shapes travel in the URL fragment, which means an expression is untrusted input arriving from a link somebody else wrote. Handing that to a JavaScript evaluator would turn every shared drum into a script-injection vector. A parser over a closed vocabulary of about thirty functions cannot execute anything - the worst a hostile formula can do is fail to parse.

There is a related subtlety worth recording. The lookup tables for functions and constants are created with a null prototype. Before that, an expression containing the identifier constructor would find a match on Object.prototype and resolve as though it were a defined constant. Nothing executable escaped, because the evaluator only ever does arithmetic and the result was simply NaN - but "the layer above happens to be harmless" is not a security property.

Open the five-lobed flower Then change the 5 to an 11 and listen to what moved.