game.camera.framing
Where the camera has to stand, for this screen, right now.
local Framing = require("game.camera.framing")
local f = Framing.solve(Core.state.topY) -- reads the live viewport
local px, py, pz = Framing.station(f) -- where the camera goes
local ax, ay, az = Framing.target(f) -- what it looks at
Nothing here reads the world or holds state. Hand it a top-course height —
and, if you like, a screen shape other than the one the engine is running at —
and it hands back a Frame: a distance, a height, an aim, a lens, and the
numbers it worked them out from. That is what lets the follow camera, every
staged beat and an off-line capture all agree about where "the camera" is.
Why it is a solve and not an offset
The game is played on a screen of unknown shape. The crane swings a course
SLIDE_REACH metres either side of the tower and the whole point of the game
is timing the drop off that swing, so the swing has to be on screen or there is
nothing to play. A fixed camera offset that frames it on a widescreen monitor
loses both ends of it on a phone held upright — the tall frame is barely half
as wide at the same lens.
So the station is solved each frame from three requirements:
| Requirement | What sets it |
|---|---|
| The whole swing across the frame | SLIDE_REACH and the slab's own width, projected onto the camera's right vector |
| The top course at a fixed height up the frame | ANCHOR — 0.36, the same on every screen |
| Sky above it for the dawn | HEAD_ROOM |
The camera takes whichever distance satisfies all three. Where even the far end of the range cannot — an ultra-tall phone — the lens widens instead of the camera retreating out of the world.
Measured at the stations it produces, the top course sits at 0.364 of the frame height and the swing's worst corner at 0.89 of the half-width, unchanged from an aspect of 0.46 to one of 2.37.
The near corner
Framing.corners reduces the swing to the sixteen corners the shot has to
hold, and each one to two numbers: how far ACROSS the frame it sits, and how
far TOWARD the lens. The second is the one that is easy to leave out. A slab at
the near end of a slide stands metres closer to the camera than the tower does,
and a perspective frame magnifies it by exactly that ratio — solving the
distance against the tower axis alone frames the swing on paper and pushes its
near corner off the edge of a widescreen frame, which is a corner of stone
leaving the picture every second course.
Where the camera stands
BEARING puts it near +Z rather than out on the +X diagonal. Courses alternate
which lozenge edge they slide along, and Framing.lateral() measures how much
of each slide reads as movement across the frame: near +Z the two axes read at
0.72 and 0.94, while out on the diagonal a U-slide reads at 0.05 — almost pure
depth, so half of all courses would swing straight at the lens and appear not
to move at all. The number is computed from the lozenge's own edge vectors, so
it stays true if the lozenge is retuned.
Scoped to this part · feeds back into the world's score.