There is no brake on a pendulum. The trolley has one — you are holding it — but the load does not, and once you understand that, the controls reorganise themselves around a fact that is almost rude in how little it cares about effort: the swing you arrive with is set entirely by when you stopped accelerating, not by how gently you did anything.
The one line it all comes from
A load on a rope under a trolley you drive, with the rope length changing under it:
L·θ̈ + 2·L̇·θ̇ + g·sinθ + ẍ·cosθ = 0
^hoisting ^gravity ^you
Your entire input is ẍ. Not ẋ — acceleration. Cruise at any speed you
like and the load hangs dead straight behind you forever; it is only the edges
that ever do anything. Linearise and the response to one acceleration pulse of
size a held for tp is a closed form with no mercy in it:
θ(t) = -(a/g)(1 - cos ωt) during the pulse
Θ = (2a/g)·|sin(ω·tp/2)| left over after it
scripts/measure.mjs integrates the real thing and checks it, because a
physics claim you haven’t measured is a physics hope:
| tp | tp / T | measured Θ | closed form | as an angle | load excursion |
|---|---|---|---|---|---|
| 0.79 s | 0.25 | 0.08649 | 0.08650 | 4.96° | 21.6 cm |
| 1.59 s | 0.50 | 0.12210 | 0.12232 | 7.00° | 30.5 cm |
| 2.38 s | 0.75 | 0.08626 | 0.08650 | 4.94° | 21.6 cm |
| 3.17 s | 1.00 | 2.8e-4 | 0 | 0.02° | 0.7 mm |
| 3.96 s | 1.25 | 0.08669 | 0.08650 | 4.97° | 21.7 cm |
| 6.34 s | 2.00 | 5.1e-4 | 0 | 0.03° | 1.3 mm |
A pulse a whole number of periods long leaves nothing, and a is not in
that condition anywhere. Push as hard as you like for exactly one period and
the load arrives plumb. The sub-millimetre at the whole periods is not the
integrator — it stops moving below dt = 1/1000 — it is the pendulum’s period
depending on its own amplitude, and halving the push drops it about sixfold.
Two ways to land dead, and only one of them is free
A whole move is two pulses: accelerate for ta, coast, brake for ta. The
brake is the same pulse with the opposite sign, so the two phasors add, and the
residual factorises:
Θ = (4a/g)·|sin(ω·ta/2)·sin(ω·Δ/2)| Δ = accel start → brake start
| ta | Δ | Δ / T | distance | measured Θ | which factor is zero |
|---|---|---|---|---|---|
| 0.800 s | 1.200 s | 0.378 | 0.58 m | 0.16159 | neither — 40 cm of swing |
| 1.200 s | 1.800 s | 0.567 | 1.30 m | 0.22148 | neither — 55 cm of swing |
| 0.800 s | 3.172 s | 1.000 | 1.52 m | 2.0e-4 | the spacing |
| 1.576 s | 3.172 s | 1.000 | 3.00 m | 6.6e-5 | the spacing |
| 2.600 s | 3.172 s | 1.000 | 4.95 m | 3.0e-4 | the spacing |
| 3.172 s | 4.072 s | 1.284 | 7.75 m | 4.0e-4 | the pulse length |
Two zeros and they are independent. Either each pulse is a whole period long, or the two pulses are a whole period apart — and the second one costs nothing, because the coast in the middle was already free. You are not being gentle. You are braking on the beat.
That gives a rule you can actually hold in your head while playing: accelerate
for dist/(a·T) seconds, then brake when exactly one period has passed since
you first pushed. That is what the period row below is, and there is no
skill in it beyond counting.
| distance | rule | accel for | total time | load arrives |
|---|---|---|---|---|
| 1.0 m | naive | 1.29 s | 2.58 s | swinging 56 cm |
| 1.0 m | period | 0.53 s | 3.70 s | dead still |
| 1.0 m | shaped | 1.29 s | 4.17 s | dead still |
| 4.0 m | naive | 2.58 s | 5.16 s | swinging 19 cm |
| 4.0 m | period | 2.10 s | 5.27 s | dead still |
| 6.0 m | naive | 3.16 s | 6.32 s | dead still |
Read the cost off the 1 m row and the 4 m row together. The period rule spends a whole period however short the hop, so it is expensive on a short move and 0.11 s on a long one. Short hops are also where hurrying is worst: flat out, a 0.97 m hop is exactly half a period long, which is the one pulse spacing that doubles the swing instead of cancelling it. The shortest move on the board is the most dangerous one, which is not where anybody looks.
And then the 6 m row, which is the trap. By 6 m the careless move is accidentally a period long and lands perfectly. The mechanic rewards you at random for doing it wrong, which is the worst feedback you can give someone learning — see the score table at the bottom, where hurrying still places 34 crates out of 60.
The dial, which is the actual instrument
Write the swing as a phasor — where the load is, against where it is currently
trying to hang, which is not vertical while you are accelerating but tilted
by a/g:
p = (θ + a/g) + i·(θ̇/ω)
It turns clockwise at ω and keeps its length. Nothing you do changes how
much swing you have, ever, except an acceleration edge — and an edge steps it
sideways by exactly a/g, always along the real axis, always the same size.
That is the whole control scheme on one screen:
- Origin is dead still. The shaded circle around it is the landing window,
placeTol/L, so it shrinks as you pay out rope — the same swing that was landable at 1.6 m of rope is not at 4 m. - The two ghosts are where
←and→would put the phasor if you pressed now. - So the move is: watch it come round, and press when the ghost is on the origin. That is not a heuristic, it is the arithmetic.
Getting the a/g term right matters more than it looks: measure the phasor
against vertical instead of against the tilted equilibrium, and a load hanging
perfectly steady under full thrust reads as a large swing. I had that wrong
first, and the tell was a “swing” that appeared the instant a key went down.
Beside it, demo/ draws your thrust against period gridlines, because the
entire skill is the spacing between two edges measured in periods and that is
invisible unless someone draws it.
The assist is not damping
Toggle shaper and every edge you make is halved and repeated half a period
later — a zero-vibration input shaper. Two copies of every excitation, in
antiphase, sum to nothing:
| player holds thrust | raw swing | shaped swing | lag paid |
|---|---|---|---|
| 0.6 s | 17.1 cm | 0.0 mm | 1.59 s |
| 1.4 s | 30.0 cm | 0.1 mm | 1.59 s |
| 2.2 s | 25.0 cm | 0.2 mm | 1.59 s |
Half a period of lag in exchange for all of it. Worth being precise about what it is not: it is not damping, it is not a filter on the load, and it never looks at the load at all. It deliberately makes a second mistake to cancel the first, which is the same trick as the period rule wearing different clothes.
Hoisting, where everything you learned stops working
The 2·L̇·θ̇ term is the one nobody expects, and it is why the game has a wall
in it. Hauling in pumps the swing — slowly, amplitude goes as L^(−3/4), which
is the adiabatic invariant E/ω with E = ½mgLΘ². Nobody touches the trolley
in this table:
| rope | T: from → to | haul takes | Θ before | Θ after | measured | L^(−3/4) | excursion |
|---|---|---|---|---|---|---|---|
| 4.0 → 2.0 m | 4.01 → 2.84 s | 2.5 s | 0.0600 | 0.1006 | 1.676 | 1.682 | 24.0 → 20.1 cm |
| 4.0 → 1.0 m | 4.01 → 2.01 s | 3.8 s | 0.0600 | 0.1713 | 2.854 | 2.828 | 24.0 → 17.1 cm |
| 1.0 → 4.0 m | 2.01 → 4.01 s | 3.8 s | 0.0600 | 0.0220 | 0.367 | 0.354 | 6.0 → 8.8 cm |
Read the last two columns together, because they disagree and both are true:
hauling in grows the angle as L^(−3/4) while the swing measured in
centimetres, which is L times the angle, shrinks as L^(1/4). The load
gets closer to plumb and faster at the same time. I had written “hauling in
pumps the swing” in the demo caption before the table existed; the number you
land on is the excursion, and by that measure hauling in helps.
The number that actually bites is neither. It is the period. Haul from 4 m
to 1 m and T goes 4.01 → 2.01 s, so every brake you had timed is now on the
wrong beat, and the gridlines under the thrust tape quietly respace themselves
while you watch. At the game’s 0.8 m/s a haul is about one period long, which
is nowhere near slow enough for the invariant to be exact — so the table above
runs it at 0.08 m/s as well, and the slow rows land inside 0.6%.
Does any of it reach the score?
Autopilot, six pads in a seeded random order, 60 deliveries a row, the real crane — damping, speed cap, wheel stiction, all of it. It knows nothing about the scoring, only how to cross a gap. Graded on 18 cm of offset and 0.3 m/s of load speed at touchdown.
| rule | placed / 60 | skewed | dragged | mean offset | mean load speed | travel | penalties | total |
|---|---|---|---|---|---|---|---|---|
| naive | 34 | 17 | 9 | 9.8 cm | 0.197 m/s | 575 s | 65 s | 640 s |
| period | 60 | 0 | 0 | 0.4 cm | 0.005 m/s | 617 s | 0 s | 617 s |
| shaped | 60 | 0 | 0 | 0.3 cm | 0.002 m/s | 651 s | 0 s | 651 s |
| live-shaper | 60 | 0 | 0 | 0.8 cm | 0.003 m/s | 651 s | 0 s | 651 s |
Hurrying genuinely is faster in the air — 42 s over 60 crates, 0.69 s each — and it gives every second of it back 1.6× over at the pad. Fine. The number I did not expect is 34/60: hurrying is not a disaster, it is a coin flip, because the residual depends on how far you went and some of these hops happen to be a whole period wide. A player doing it entirely wrong lands more than half their crates and has no way to tell which half was luck. That is why the dial is in the demo at all — the score cannot teach this, and I only found that out by writing an autopilot bad enough to embarrass the good one.
What is physics here and what isn’t
Worth being straight about, since the piece leans on the physics being real:
- Derived, and measured against the integrator: both residual formulas, the
two independent zeros, the
2a/gceiling on what one edge can do, thea/gphasor step, theL^(−3/4)amplitude law and theL^(1/4)excursion law that follows from it. - A real limit, deliberately placed:
vMaxsits exactly ata·T(lMax), so a one-period pulse never clips the speed cap. It is not a coincidence, it is the only setting where the closed form the piece is built on stays reachable by a player. Before I pinned it there, every long pulse was silently truncated by the cap and the measurements disagreed with the algebra by a factor of two — the cap is a brake pulse you did not ask for. - A modelling choice: wheel stiction. A frictionless trolley is a double integrator, so a command asymmetric by one frame leaves a couple of mm/s that nothing removes, and the crane walks half a metre off over a run. The deadband is far below anything a move uses, so no move is distorted by it — but it is there because the drift was real and took a 48-delivery autopilot run to notice.
- A game, not a claim: the crate is a box that clips the wall and lands on a surface; it does not tumble, and pads have no sides. And the hook only re-arms once it is hauled clear of what it just set down, which is a cycle rule, not a physical one.
Reuse
src/two-pulses.mjs is framework-free with no canvas in it. createCrane()
gives you step(dt, {thrust, hoist}), swayPhasor(), loadX, loadSpeed,
period, and landing(pad). planMove(dist, L, {mode}) is the three rules —
naive, period, shaped — as a schedule of thrust edges, and
createShaper() is the live assist, which is eleven lines and does not know
what a pendulum is. pulseResidual() and moveResidual() are the closed forms
themselves, which is what makes them testable.
Integration is RK4 substepped at 1 kHz. snapSchedule() rounds edges onto the
step grid, which is not a rounding nicety: a pulse one step longer than its
brake is a different move.
The demo is demo/index.html with its own copy of the module (ADR-0002),
keyboard and on-screen buttons both, instruments in their own canvas so nothing
overlays the bay, and a window.__demo hook the screenshot rig drives.
node scripts/measure.mjs prints every table above and exits non-zero if any
of them stops agreeing. node scripts/screenshot-demo.mjs regenerates the
thumb and media and doubles as the smoke test — it drives the real keyboard,
then plays the same hop twice, on the beat and hurried, and asserts one leaves
under 4 cm of swing and the other leaves over 20.




