Skip to content

feat(camera): tilt a whole shot in 3D with one shared vanishing point - #400

Merged
LeadcodeDev merged 1 commit into
mainfrom
feat/camera-3d-rotation
Sep 27, 2026
Merged

LeadcodeDev merged 1 commit into
mainfrom
feat/camera-3d-rotation

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Refs #362 — part 1 of two. Part 2 is split out, see below.

The gap

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, not as a camera. Camera keyframes rejected it outright:

unknown camera keyframe property 'rotate_x'

What it does

"camera": {
  "perspective": 1400,
  "keyframes": [
    { "property": "rotate_y",
      "values": [{ "time": 0, "value": -16 }, { "time": 2, "value": 16 }],
      "easing": "ease_in_out" }
  ]
}

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 and camera.focus already 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_y animated −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_m44 and pre_concat, the same machinery per-node rotate_x already 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 neither
  • a_deeper_plane_swings_further_than_a_near_one — the depth-scaling rule, measured as ink displacement
  • perspective_zero_is_an_orthographic_tilt

KNOWN_CAMERA_PROPERTIES had 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.

`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 LeadcodeDev added the enhancement New feature or request label Sep 27, 2026
@LeadcodeDev LeadcodeDev self-assigned this Sep 27, 2026
@LeadcodeDev
LeadcodeDev merged commit dfaa7ba into main Sep 27, 2026
4 checks passed
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
@LeadcodeDev
LeadcodeDev deleted the feat/camera-3d-rotation branch September 29, 2026 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant