# ashbloom.camera.rack
The lens and the ears of Ashbloom's 25 set-ups.
The set-ups themselves are not here. They are data — the `pos` / `look` / `fov` /
`ease` keys on each row of `film.SHOTS` — because the cutscene Runtime under
`AshbloomDirector` is what poses the camera, and it reads that table directly.
This module owns the two things that ride on those set-ups and cannot be
expressed as a row in a table.
## The lens
`sensorHeight` is full frame, so every shot's focal length falls out of the field
of view it was composed at:
```
f = 12 / tan(fov / 2) -- 24° -> 56 mm, 14° -> 98 mm, 68° -> 18 mm
```
which keeps the lens honest through shot 13's 30° → 40° ramp, where a
hand-written focal length would drift away from the framing mid-shot. Focus is
racked to the subject: the distance from the shot's eye to the point it is
looking at, re-evaluated every frame, so a shot that dollies pulls focus as it
travels.
`RACK[n]` states only what the optics cannot derive — the aperture, where the
plane of focus sits along the eye → subject ray, and the blur ceiling:
| | shots | f-stop |
|---|---|---|
| macro | 2, 9, 12, 15, 20, 23 | f/1.4 – f/1.8 |
| close | 6, 8, 13, 16, 18, 24 | f/2.0 – f/2.8 |
| middle | 3, 10, 11, 19 | f/4 |
| wide | 1, 4, 14, 21, 25 | f/5.6 |
| very wide | 5, 7, 17, 22 | f/8 |
It drives `@builtin::systems.depthOfField.depthOfField` directly rather than
through a `DepthOfField` component. The component pushes its fields only when
they CHANGE, so the two do not fight; but one authoring surface per system is the
rule, and for this system the camera slice is it.
## The ears
The scene's single `AudioListener` is re-posed on every cut, so the soundscape
turns with the film.
It does **not** re-parent to the cutscene camera. That camera is temporary and is
despawned when the playback releases its stage, which would take the world's only
listener with it.
One departure from riding the camera exactly: two set-ups stand a quarter of a
kilometre outside the crater, and a listener out there hears no garden. The ears
are pulled along the eye → subject ray until they are within 30 m of what the
shot is looking at. Close shots are unaffected; the wides hear the bowl they are
framing.
## The guards
A camera inside the rock renders a flat single-colour frame and every read-back
in the engine reports it as correct, so two checks answer for it:
```lua
local rack = require("ashbloom.camera.rack")
rack.probe(110) -- eye / at / fov / focal length / f-stop at a film time
rack.clearances() -- how far each of the 25 eyes clears the crater surface
```
`clearances()` asks `ashbloom.crater.craterGeo.groundY` — the same profile and
warp the crater mesh was cut from — at thirteen points across every shot. Under
0.40 m is inside the 34 cm ash mantle. The stage repeats the check on each cut
and says so in the log, and it warns once per anchor when a shot anchor moves
more than 2 m from where the set-ups were composed.
## Joining in
```lua
rack.install() -- registers the "camera" stage at order 15; idempotent
```
`AshbloomCamera.component` calls it, so a freshly loaded world has the lens and
the listener without anyone requiring this module by hand.