feat(camera): tilt a whole shot in 3D with one shared vanishing point - #400
Merged
Merged
Conversation
`style.depth` drove parallax — translation and zoom. To tilt a whole shot you copied the same `rotate_x`/`rotate_y` keyframes onto every group, and **each group then got its own vanishing point**, which reads as cards turning independently rather than as a camera. `camera.rotate_x`, `rotate_y` and `perspective` apply the projection **once**, about the camera's origin, and scale the rotation by each direct child's `style.depth` — the same rule parallax and `camera.focus` already follow. A plane at depth 3 swings three times as far as one at depth 1, which is where the volume comes from. All three go through `interpolate_camera_property`, so keyframes came free, and `KNOWN_CAMERA_PROPERTIES` had to learn them — the validator rejects an unknown keyframe property by design. The matrix is built with `css_perspective_m44` and `pre_concat`, the same machinery per-node `rotate_x` already uses, rather than a second hand-rolled projection. Four tests. Two fail without the tilt; two assert equality and pass both ways on purpose — a camera declaring perspective but no tilt must not move a pixel, which is what every scenario written before this does. Part 2 of the issue, camera motion blur, is not here. It means accumulating sub-frame renders along the camera's motion, which lives in the encode loop and not in the paint pass. Splitting it rather than half-doing it. Refs #362
LeadcodeDev
added a commit
that referenced
this pull request
Sep 28, 2026
…p globe Both halves of #387 — a dome of flat content and a real globe — need the same thing a per-node transform/perspective can't give: one shared vanishing point (or one shared hemisphere test) for a whole set of children, not a private one per node. style.layout-surface (cylinder|sphere) projects a container's direct children onto a curved surface at paint time only — taffy still lays out the grid flat, so nothing about layout itself changes. Each child gets a translate3d computed from its normalized offset from the container's center, run through the same css_perspective_m44/pre_concat machinery apply_plane_camera already uses for the camera's own 3D tilt (#400), just applied per child instead of per plane. Depth ordering is left at declaration/z-index order — a wide enough arc or turntable rotation can make a far cell paint over a near one, which #93 owns generally and isn't fixed here. dot_map gains projection: "orthographic", the closed-form lat/lng-to-screen projection plus the far-hemisphere visibility test a clip-path circle can't express (it just crops a flat map, it never curves). rotate.lng/lat aim the globe, limb_shading darkens towards the edge, and arcs are great-circle paths slerped in 3D and lifted off the surface, culled the same way as the dots so they don't get drawn straight through the globe. The historical flat mapping is untouched code, gated behind the projection's default, and pinned byte-identical by test. Both features are proven with pixel renders, not just the underlying geometry: a projected grid's columns are shown to differ in width where a flat grid's don't, and an orthographic globe's dot spacing is shown to converge towards the limb where the flat map's doesn't.
LeadcodeDev
added a commit
that referenced
this pull request
Sep 28, 2026
…406) * feat(3d): curved layout-surface containers and an orthographic dot_map globe Both halves of #387 — a dome of flat content and a real globe — need the same thing a per-node transform/perspective can't give: one shared vanishing point (or one shared hemisphere test) for a whole set of children, not a private one per node. style.layout-surface (cylinder|sphere) projects a container's direct children onto a curved surface at paint time only — taffy still lays out the grid flat, so nothing about layout itself changes. Each child gets a translate3d computed from its normalized offset from the container's center, run through the same css_perspective_m44/pre_concat machinery apply_plane_camera already uses for the camera's own 3D tilt (#400), just applied per child instead of per plane. Depth ordering is left at declaration/z-index order — a wide enough arc or turntable rotation can make a far cell paint over a near one, which #93 owns generally and isn't fixed here. dot_map gains projection: "orthographic", the closed-form lat/lng-to-screen projection plus the far-hemisphere visibility test a clip-path circle can't express (it just crops a flat map, it never curves). rotate.lng/lat aim the globe, limb_shading darkens towards the edge, and arcs are great-circle paths slerped in 3D and lifted off the surface, culled the same way as the dots so they don't get drawn straight through the globe. The historical flat mapping is untouched code, gated behind the projection's default, and pinned byte-identical by test. Both features are proven with pixel renders, not just the underlying geometry: a projected grid's columns are shown to differ in width where a flat grid's don't, and an orthographic globe's dot spacing is shown to converge towards the limb where the flat map's doesn't. * docs(skills): link the layout-surface and orthographic dot_map rules
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #362 — part 1 of two. Part 2 is split out, see below.
The gap
style.depthdrove parallax: translation and zoom. To tilt a whole shot you copied the samerotate_x/rotate_ykeyframes onto every group — and each group then got its own vanishing point, which reads as cards turning independently, not as a camera. Camera keyframes rejected it outright:What it does
The projection is applied once, about the camera's origin — that is the whole difference from the per-group workaround. The rotation is scaled by each direct child's
style.depth, the same rule parallax andcamera.focusalready follow, so a plane at depth 3 swings three times as far as one at depth 1.Rendered at 0.0 / 1.0 / 1.9 s with
rotate_yanimated −16° → +16°: tilted one way, flat, tilted the other — and the deep pink plane visibly swings further than the shallow blue one.Built with
css_perspective_m44andpre_concat, the same machinery per-noderotate_xalready uses, rather than a second hand-rolled projection.Verification
Four tests. Two fail without the tilt. Two assert equality and pass both ways on purpose, which is the point:
a_camera_tilt_of_zero_renders_exactly_as_no_camera— a camera declaring perspective but no tilt must not move a pixel, because every scenario written before this declares neithera_deeper_plane_swings_further_than_a_near_one— the depth-scaling rule, measured as ink displacementperspective_zero_is_an_orthographic_tiltKNOWN_CAMERA_PROPERTIEShad to learn the three names; the validator rejects an unknown keyframe property by design, and caught them before any render did.cargo fmt --all --check,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace(1659) all clean.Part 2 is split, not skipped
Camera motion blur means accumulating several sub-frame renders along the camera's motion within a shutter window. That lives in the encode loop, not the paint pass — a different subsystem, a different cost model (k× render time per frame), and a different set of tests. Bundling it here would make one PR that is really two.
I am leaving #362 open for it rather than closing on part 1.
Written comment-free, per the codebase-wide rule from #345.