workshop private

← all creations

Two Pulses

mechanics · created 2026-09-30

A gantry crane whose load is a pendulum you can only steer by the pivot — so the swing you arrive with was decided by two acceleration edges and the gap between them, and the only cure for a swinging load is a second mistake, timed.

physicsgame-feelsimulationcanvas

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:

tptp / Tmeasured Θclosed formas an angleload excursion
0.79 s0.250.086490.086504.96°21.6 cm
1.59 s0.500.122100.122327.00°30.5 cm
2.38 s0.750.086260.086504.94°21.6 cm
3.17 s1.002.8e-400.02°0.7 mm
3.96 s1.250.086690.086504.97°21.7 cm
6.34 s2.005.1e-400.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ΔΔ / Tdistancemeasured Θwhich factor is zero
0.800 s1.200 s0.3780.58 m0.16159neither — 40 cm of swing
1.200 s1.800 s0.5671.30 m0.22148neither — 55 cm of swing
0.800 s3.172 s1.0001.52 m2.0e-4the spacing
1.576 s3.172 s1.0003.00 m6.6e-5the spacing
2.600 s3.172 s1.0004.95 m3.0e-4the spacing
3.172 s4.072 s1.2847.75 m4.0e-4the 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.

distanceruleaccel fortotal timeload arrives
1.0 mnaive1.29 s2.58 sswinging 56 cm
1.0 mperiod0.53 s3.70 sdead still
1.0 mshaped1.29 s4.17 sdead still
4.0 mnaive2.58 s5.16 sswinging 19 cm
4.0 mperiod2.10 s5.27 sdead still
6.0 mnaive3.16 s6.32 sdead 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:

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 thrustraw swingshaped swinglag paid
0.6 s17.1 cm0.0 mm1.59 s
1.4 s30.0 cm0.1 mm1.59 s
2.2 s25.0 cm0.2 mm1.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:

ropeT: from → tohaul takesΘ beforeΘ aftermeasuredL^(−3/4)excursion
4.0 → 2.0 m4.01 → 2.84 s2.5 s0.06000.10061.6761.68224.0 → 20.1 cm
4.0 → 1.0 m4.01 → 2.01 s3.8 s0.06000.17132.8542.82824.0 → 17.1 cm
1.0 → 4.0 m2.01 → 4.01 s3.8 s0.06000.02200.3670.3546.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.

ruleplaced / 60skeweddraggedmean offsetmean load speedtravelpenaltiestotal
naive341799.8 cm0.197 m/s575 s65 s640 s
period60000.4 cm0.005 m/s617 s0 s617 s
shaped60000.3 cm0.002 m/s651 s0 s651 s
live-shaper60000.8 cm0.003 m/s651 s0 s651 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:

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.