The framing prologue built the centred position `p` by scaling with the
source's aspect, then straightened, then permuted the quarter turns. On a
landscape frame those are one space and it worked. Once a turn has swapped
the axes — the rotate button, or a file whose EXIF tag says the camera was
held sideways — they are not: `p` was measured with the source's ruler on a
frame that is no longer that shape, stretching one axis against the other by
(w/h)², which is 2.25 on a 3:2 photograph.
The quarter-turn permutation happened to undo that stretch, so rotation alone
looked right, which is how this survived. The straighten in between did not,
and a rotation in a space whose axes carry different scales is a shear.
`p` is now built in `frame_aspect` — the frame as the user sees it — and the
permutation becomes the one place the two rulers meet: each axis divided by
the aspect it is read from, multiplied by the aspect it is written to.
Asserted on pixels rather than on the generated WGSL, because reading the
shader and reasoning about which space `p` lives in is how the wrong formula
got written in the first place: a disc, straightened by 20° on a turned 3:2
frame, must come back circular by every route to a swapped frame — the
button, the tag, and the two composed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>