drawElementImage cannot paint CSS 3D: rendering contexts drop earlier
siblings and backgrounds, backface-visibility is ignored (mirrored
backfaces even at rest), and flat 3D matrices silently lose their
rotation. Until now every 3D comp was gated to the baseline route.
threeDProjection.ts rewrites 3D content in-page before capture:
- discovery: perspective/preserve-3d contexts, elements at a 3D matrix
at t=0, and the producer stub's record of 3D tween targets
(rotationX/rotationY/transformPerspective) for to()-style tweens that
are still flat at init
- leaf quads rasterized once via SVG foreignObject (fonts and images
inlined as data URLs), shell quads carry only own paint when a child
contains further 3D
- per-frame WebGL projection from live computed matrices: CSS-convention
matrix math, perspective + transform-origin sandwiches, GL backface
culling, accumulated opacity as a fragment uniform
- live elements hidden via clip-path (GSAP autoAlpha fights
visibility/opacity), 3D contexts neutralized and live 3D matrices
stripped after reading (perspective-carrying and rotated matrices
poison the capture even when hidden), authored backface flags captured
before neutralization
- projected canvases composite OVER the DOM paint — the under-pass would
bury them beneath the composition root's own background
Correctness guards fall back to the platform baseline route, same
contract as the video gate: degenerate markup (zero-size/inline-box
quads — gen_os flip-card spans) and quads with GSAP-animated descendants
(static textures would freeze them; golf measured 46->24 dB without
this).
fast-capture-3d test comp (flip card + perspective-free rotationX
entrance): 57.5 dB avg / 51.7 dB min vs baseline at 1.5x speedup.
Real-world comps with animated 3D subtrees fall back cleanly.
Engine suite 692/694 (2 failures pre-exist on clean tree); build, lint,
oxfmt verified manually — hook bypassed for the same stale-lockfile
core-build failure as the previous commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>