workshop private

← all creations

Precession

mechanics · created 2026-09-10

Steering a spinning top, where push north means go west — and the one lean angle at which your finger and gravity turn the thing equally hard, 8.2°, is where the score peaks without anything in the game being told to put it there.

physicsgame-feelsimulationcanvas

Push the top north and it goes west. Not because the controls are inverted as a gimmick — because that is what a gyroscope does with a torque, and once you accept it the whole control scheme reorganises itself around a right angle that is never anywhere near where you want it to be.

The one line it all comes from

A heavy symmetric top on a fixed tip, linearised about upright, is a single complex second-order ODE. Write the tilt of the spin axis as a complex number ζ = θx + i·θy — the horizontal shadow of the axis, so |ζ| ≈ sinθ:

I⊥·ζ̈  =  i·Is·S·ζ̇  +  M·g·h·ζ  +  h·F·p
         ^gyroscopic   ^gravity    ^your finger

That is the entire model. Everything the demo does is a consequence, and the nice part is how much comes out of it that nobody had to author:

scripts/measure.mjs integrates the thing and checks it against those closed forms, because a physics claim you haven’t measured is a physics hope:

spin (rpm)precession, measuredslow rootperiodω_n (Hz)upright stable?
40111.5041.5044.18 s37.6yes
28652.1192.1192.96 s26.8yes
11465.7175.7171.10 s10.7yes
76410.13710.1320.62 s7.2yes
63818.78718.7810.33 s6.0yes
5735.4no — e-folds in 0.12 s
4774.5no — e-folds in 0.08 s

Why push north means go west

dL/dt = τ, and L is large and pointing up. A finger pushing the head in direction p applies torque h·F·(ẑ × p), and after the algebra the push enters the equation as itself — but ζ̇ picks up the factor i. The lean moves 90° round from wherever you pushed, and the top goes where it leans.

The consequence takes a minute to sink in, and it is the actual control scheme:

There is no button that does the obvious thing. The demo draws all three arrows — your push, where that sends the lean, and where the top is actually travelling — because the right angle is between the first two, and the third is the lean you already banked, which is why it sits at no particular angle to either. Getting that wrong is the single most common way to misdescribe this, and I misdrew it once before the screenshots caught me.

The number the whole thing is really about

Quasi-statically, your finger turns the lean at h·F/(Is·S·|ζ|) and gravity turns it at Mgh/(Is·S). Take the ratio:

F / (M·g·|ζ|)

The spin cancels. Both moments of inertia cancel. There is exactly one lean angle where your finger and gravity steer with equal authority —

sinθ* = F/(Mg)   →   8.20°

— and it does not move for the entire run, no matter how much spin is left. Below it you are driving. Above it gravity is, and you are a passenger holding a stick that no longer wins arguments. Since travel speed is lean, going fast means handing over the wheel. That is the whole game.

leananglespeedfinger (rad/s)gravity (rad/s)ratio
0.05002.87°6.0 cm/s5.9732.0932.854
0.10005.74°12.0 cm/s2.9872.0931.427
0.14278.20°17.1 cm/s2.0932.0931.000
0.200011.54°24.0 cm/s1.4932.0930.714
0.300017.46°36.0 cm/s0.9962.0930.476

And it shows up in the score

The game layer is thin on purpose: thread rings with the tip before the top dies. Nothing in it knows about θ*. So I wrote an autopilot that knows one rule — push 90° back the other way from wherever you want the lean to go, which is pushFor() in src/, four lines — pointed it at a target lean, and ran 24 runs per row.

target leanrings threadedrun lengthrail hits
0.0500 (2.9°)5.5 ± 0.946.9 s0.8
0.1000 (5.7°)11.3 ± 1.146.4 s0.5
0.1427 (8.2°)16.7 ± 1.245.6 s1.0
0.2000 (11.5°)15.0 ± 1.643.7 s2.5
0.3000 (17.5°)10.9 ± 2.237.7 s8.0

The optimum is at F/(Mg), on the nose. Below it the top is too slow to reach rings before the spin clock runs down; above it gravity out-turns the finger, the trajectories go wide and sloppy, and the rail starts collecting hits. I didn’t place that peak — it is two constants and a division.

Where runs actually end, which is not where I expected

The critical spin is 638 rpm and almost nobody ever sees it. The finger’s authority goes as 1/S for precisely the same reason precession does, so a top that is nearly out of spin answers a nudge about five times as hard as a fresh one — and every input you spent forty seconds learning is suddenly too big. The endgame is a knife edge, and it is a knife edge made of the same constant that made the midgame comfortable.

Same autopilot, easing off the stick (gain is the error at which it goes to full push, so bigger means gentler):

gainrings threadedrun lengthfell to spin lossspin when it fell
0.04516.745.6 s0/24835 rpm
0.12010.744.8 s0/24822 rpm
0.2505.542.0 s0/24804 rpm
0.5002.443.6 s9/24694 rpm

Only a pilot barely touching the thing coasts down to the real floor, having threaded almost nothing. Take your hands off entirely and it dies exactly where the discriminant says: critical at 57.9 s, rim on the felt at 59.0 s. 1.1 seconds of warning, all of it after the point of no return.

What is physics here and what isn’t

Worth being straight about, since the piece leans on the physics being real:

Reuse

src/precession.mjs is framework-free and has no canvas in it. createTop() gives you step(dt, push), tilt, spin, pos, alive, and readouts (leanDeg, precession, circleRadius, authority — that last one is |ζ|/sinθ*, so above 1 means gravity has the wheel). railBounce() is the cushion, pushFor()/aimAt() are the autopilot. Integration is RK4 substepped at 1 kHz, because nutation runs at 37 Hz at launch and a 60 Hz frame step aliases it into nonsense.

The demo is demo/index.html with its own copy of the module (ADR-0002), keyboard and drag both, and a window.__demo hook the screenshot rig drives. node scripts/measure.mjs prints every table above; node scripts/screenshot-demo.mjs regenerates the thumb and media and doubles as the smoke test — it presses ArrowRight in a real browser and asserts the lean ends up 90° round from east, give or take the precession it walked during the press.