The bottom section of a fire escape at night: the counterbalanced stair, the one that hangs out of reach over the sidewalk until somebody puts their weight on it. Brick wall, a lit window with a television going in it, the lowest landing, the hinge at its outer edge, the cast-iron block on its arm, and a person who needs to get down.
Three loops:
- wait — the stowed stair, holding. Nothing moves but the television.
- drop — a step onto the top tread, and the solver takes over. Two seconds, and it turns into a staircase on the way down.
- seize — the same step, thirty-four years later. The hinge gives up four degrees of free play and then stops. They climb out along it anyway, chasing leverage, and end up standing at the far end four metres over the pavement still nine kilograms short.
What’s different about this one
Every other sprite in this catalog is a list of hand-drawn poses, and an
animation is an order to play them in. This one has no poses of the stair at
all. It has a state variable and a linkage — one angle, theta, and a
rasteriser that will draw the stringers, the eight treads, the handrail, its
balusters and the counterweight arm at any value of it you like.
So the exported frames are not artwork. They are the solver’s output, sampled
at 7 frames per second of the solution and played back at 7 fps, which is why
the drop accelerates: nobody eased it. The first three frames barely move and
the last three cover half the arc, because that is what falling looks like when
the torque driving it is still growing. Change the counterweight in RIG and
every PNG in export/ changes, because they were never drawn.
Two details fall out of the geometry rather than being keyed:
- The treads. They are rigid with the stringer and were hung level at the
deployed pitch, so their tilt off horizontal is exactly
deployed − theta. Stowed, that is 83°: the thing hanging over the sidewalk is not a staircase, it is a ladder, and it does not look like one either. It becomes stairs by rotating under you, which is the whole trick of the object. - The pose.
poseFor(theta)reads the angle and picks climbing, riding or standing. The figure changes what their body is doing because the stair changed what it is, not because a frame said so.
The lighting is two rim passes and nothing else — no falloff, no gradient. A
solid pixel gets a cool edge if the sky is directly above it and a warm edge if
the step toward the window is empty. An edge either sees a light or it does not.
That is what keeps a swinging steel stair readable against a dark sky at 52
pixels wide, and it means the stair’s highlights travel as it rotates, for
free, because which edges can see the window is a function of theta too.
The mechanism
A fire escape’s last flight has exactly one job: say yes to weight from above and no to everything else. It does that job with a lever, and the lever is in a bind.
You can only reach the top tread. The stowed stair hangs with its foot 4.2 m over the sidewalk — that is the entire security model, and it is why the thing exists in this shape. So the only part of it anybody can step onto from the landing is the end nearest the hinge, which is the end with almost no leverage. A person standing there works a 0.36 m arm against the stair’s own 1.52 m.
Which forces the balance to be set fine. It has to tip for a child on the worst moment arm in the machine, so the empty stair can only be holding itself up by a whisker:
The balance, at the stowed angle
| term | N·m | note |
|---|---|---|
| stair, own weight | +879.4 | trying to fall |
| counterweight | −971.3 | trying to hold |
| net — the margin | −91.8 | what the whole machine holds itself up by |
| a 78 kg person on the top tread | +225.6 | and that is the design load |
The margin is 10.4% of the stair’s own moment. Everything below is a consequence of that number having to be small.
Two things follow from that 10.4%, and they are the piece.
One: you cannot refuse the weather
Anything spread over the stair — snow, ice, a pigeon convention — does not act at the top tread. It acts at the mean tread, 1.63 m out, which is 4.5 times the arm the design had to answer to. So the balance that lets a child down also lets seven kilograms of snow down, and there is no setting of the block that fixes it:
The trade you cannot win
A load spread over the treads acts at the mean tread, not the top one, so it out-levers the design load by 4.53×. Raising the block to refuse the weather raises what a person has to weigh by the same factor.
| block | margin | spread load it refuses | person needed on the top tread |
|---|---|---|---|
| 88 kg | 20 N·m | 1.5 kg | 7 kg |
| 92 kg | 61 N·m | 4.7 kg | 21 kg |
| 95 kg | 92 N·m | 7.0 kg | 32 kg |
| 98 kg | 122 N·m | 9.3 kg | 42 kg |
| 102 kg | 163 N·m | 12.5 kg | 56 kg |
| 106 kg | 204 N·m | 15.6 kg | 71 kg |
| 112 kg | 266 N·m | 20.3 kg | 92 kg |
There is no row where it refuses a winter and still answers to a child.
This is not a flaw in the tuning. It is a ratio of two lever arms, fixed by where a person can stand versus where snow can lie, and no counterweight can change it. It is the entire argument for a hold-down latch: the second job needs a second mechanism, because the first one is already at its limit.
Two: paint is a counterweight nobody specified
Friction at the hinge enters that same 92 N·m balance, on the same side, in the same units. Every few years somebody paints the building, which includes painting the hinge, and the film bridges the gap and sets. Rust grows into what is left. None of it is on any drawing of the machine, none of it is what anyone means by wear, and it is the only term in the linkage that grows.
What it does is not jam the stair. It moves the place where the stair will finally let go further out along its own length:
What paint does
Breakaway torque at a hinge nobody has moved, and the same number priced two other ways: as counterweight somebody would have had to bolt on to do the same damage, and as where a 78 kg adult now has to stand.
| years | breakaway | same as adding | release point for 78 kg | tread |
|---|---|---|---|---|
| 0 | 0 N·m | 0 kg of block | 0.15 m | 1 |
| 3 | 78 N·m | 8 kg of block | 0.27 m | 1 |
| 6 | 198 N·m | 19 kg of block | 0.46 m | 2 |
| 9 | 342 N·m | 33 kg of block | 0.69 m | 2 |
| 12 | 504 N·m | 49 kg of block | 0.95 m | 3 |
| 15 | 681 N·m | 67 kg of block | 1.23 m | 4 |
| 18 | 871 N·m | 85 kg of block | 1.54 m | 5 |
| 21 | 1073 N·m | 105 kg of block | 1.86 m | 6 |
| 24 | 1285 N·m | 126 kg of block | 2.20 m | 7 |
| 27 | 1506 N·m | 147 kg of block | 2.55 m | 8 |
| 30 | 1736 N·m | 170 kg of block | 2.92 m | 8 |
| 33 | 1975 N·m | 193 kg of block | 3.30 m | off the end |
| 36 | 2221 N·m | 217 kg of block | 3.69 m | off the end |
| 40 | 2560 N·m | 250 kg of block | 4.23 m | off the end |
The release point moves out at about one tread every 3.9 years, and runs off the end of the stair in year 31.1. Nothing breaks in year 32. Nothing looks different from the sidewalk.
Read that fourth column as a location on a real object. In year 6 you step on and walk down. In year 18 you have to get to the fifth tread — and remember which way that is, because the stowed stair slopes up: finding leverage means climbing out over the sidewalk, on a ladder, in whatever emergency brought you out of the window. In year 27 it is the last tread. And in year 32 the answer comes back 3.30 m on a 3.20 m stair, which is not a hard place to stand. It is not a place.
Nothing breaks that year. The block is the size it always was. The margin is the number the installer chose. From the sidewalk it looks exactly like a fire escape.
Who can release it, new
| person | has to get this far out | which tread |
|---|---|---|
| 18 kg | 0.63 m | tread 2 |
| 25 kg | 0.46 m | tread 2 |
| 32 kg | 0.36 m | the first one — it goes as they step on |
| 45 kg | 0.25 m | the first one — it goes as they step on |
| 60 kg | 0.19 m | the first one — it goes as they step on |
| 78 kg | 0.15 m | the first one — it goes as they step on |
| 100 kg | 0.11 m | the first one — it goes as they step on |
The lightest person who can release it standing on the top tread: 31.7 kg.
A 32 kg child releases it on the tread they step onto; anyone lighter has to get to the second. That is the load case the balance is really tuned for, and it is why the margin had no room to be generous in the first place.
The drop itself
| quantity | value |
|---|---|
| moment of inertia about the hinge | 361 kg·m² |
| time from the step to the pavement | 2.03 s |
| foot speed on arrival | 6.89 m/s |
| the same foot, simply dropped | 9.09 m/s |
| so the linkage takes off | 24% |
That is the counterweight’s entire contribution to the landing: it removes 24% of a four-metre fall and the rubber buffer at the foot has to eat the rest. The block is a latch, not a brake — by the bottom of the arc its arm is vertical and it is holding nothing at all.
The seize loop runs at 34 years, three past the line. The figure climbs to 3.05 m — the last tread there is — and is still 9.6 kg short. The hinge gives up 3.98° of free play and that is every millimetre of travel in the loop.
The rig, in metres and kilograms
| quantity | value | why it is that |
|---|---|---|
| stringer, hinge to foot | 3.20 m | one storey of bottom flight |
| hinge above the sidewalk | 2.38 m | forced: L·sin(48°), the deployed pitch |
| foot above the sidewalk, stowed | 4.21 m | forced: the -35° stowed angle. This is the whole security model |
| stair section | 72 kg | two stringers, eight treads, a rail |
| its centre of mass | 1.52 m out | near enough the middle of a uniform thing |
| counterweight | 95 kg | sized below — the only free parameter in the machine |
| its arm | 1.05 m | clocked 42° off the stringer, so it is vertical at full deployment |
| top tread | 0.36 m out | the only tread reachable from the landing |
| mean tread | 1.63 m out | what a spread load gets to use instead |
These are a model, not a survey of anybody’s building — plausible numbers for a
narrow bottom flight, chosen so the arithmetic lands where real installations
land. The one invented curve is the breakaway torque, 17.6 · years^1.35: its
shape is the claim — paint over paint, so resistance climbs faster than
linearly — and its constants are calibrated to the one anecdote everybody who
has tried a neglected fire escape already has. Treat the years axis as ordinal.
The columns beside it are honest arithmetic on whatever it says.
The demo
Three sliders and one button. The slider that matters is paint, and the thing to watch is not the scene — it is the marker on the flat strip under it, which is the release point. Drag from 0 to 40 and watch it walk down the stair and off the end. Press step on at any setting to see what that means for the person: a ride, or a climb that ends standing on the last tread of something that is not going to move.
The demo does not re-implement the physics. Every parameter change re-runs the
same simulate() the exported frames came out of, and playback is stateAt()
against wall-clock time, so what you see in the demo is the same solution the
sprite sheet is a sampling of.
Reuse
export/stair-sheet.png— 1612×60 strip, thirty-one 52×60 frames in sheet order:wait-0..3,drop-0..1,drop-f0..13,drop-w0..2,seize-0..1,seize-f0..5.stair-{wait,drop,seize}-*.png— the individual frames.stair@6x.png— prescaled 6× hero shot for docs and previews.
Render at integer scales with nearest-neighbour filtering or the rim pass smears
into the gradient it exists to avoid. wait plays at 3 fps, drop at 7,
seize at 4 (ANIMATIONS carries all three). drop and seize are one-shots
by intent; the demo plays either and falls back to wait.
If you want the linkage rather than the pictures, call paintScene(ctx, scene)
with any theta you like — that is what the demo does, sixty times a second.
Source
No .aseprite — the canonical source is source/last-flight.mjs: a
framework-free ES module holding the building as a character map, five figure
poses, the rig, the torque model, the solver and the rasteriser. Edit it and
regenerate everything with:
node source/render.mjs # export/, media/, thumb.png, demo/last-flight.js
node scripts/screenshot-demo.mjs # the demo screenshots, and the demo smoke test
node scripts/measure.mjs # every table above
(All three need site/node_modules installed — they resolve Playwright from
there.) render.mjs also rewrites the demo’s bundled copy of the module, so it
cannot drift from source/ (ADR-0002).
Gotchas
demo/last-flight.jsis generated. Editsource/last-flight.mjsand re-render.validateGrids()throws on ragged rows and unknown palette characters, and on three things that are not art at all: a deployed foot that does not reach the pavement the character map drew, a stair that leaves the frame at either end of its arc, and a balance that either will not hold itself up or will not answer to a 40 kg child. The geometry and the physics have to agree about what building this is.- The figure never rotates. A person on a tilting stair stays vertical; only
their footing goes round the arc.
paintPersonplaces their feet at the stringer point and draws them upright, which is both correct and the reason this piece needs no rotated sprites. - The counterweight tells you the state of the machine on its own. Stowed, the
block sits silhouetted in the lit window; deployed, it is straight up over the
hinge in the dark. You can read
thetaoff it without looking at the stair. - Frame count is not fixed.
DROP_SAMPLESisround(solution_time × 7), so making the stair heavier makes the fall longer makes the sheet wider. If you are consumingstair-sheet.pngby index, useFRAME_ORDER, not a number.
Port notes
paintScene writes into a plain RGB buffer and only touches a 2D context at the
blit, so the linkage and the rim passes port anywhere you can address pixels —
same shape as Gaffer’s light pass, one step further in that the geometry is
computed rather than stored. The torque model is twenty lines of trigonometry
with no dependencies at all. In DragonRuby, keep the building as strings and
either run the same rasteriser into args.outputs.solids or, since the frames
are static, just consume the sheet with source_x = frame * 52.