fix(engine): paint ancestor background before drawElementImage capture

drawElementImage only paints the captured [data-composition-id] subtree.
Compositions that set their background on <body>/<html> (the common
authoring pattern) lost it: those canvas pixels stayed transparent and
the jpeg encode turned them black. Comps layering semi-transparent
elements over the body background (e.g. rgba cards) rendered darkened.

Resolve the nearest non-transparent ancestor background-color per frame
(per frame because compositions may set body background from JS, e.g.
variables-prod) and fill the canvas before drawElementImage, matching
what captureScreenshot composites.

Fast-vs-baseline PSNR on macOS GPU:
  font-variant-numeric         23.6 -> 65.4 dB
  animejs-adapter              25.4 -> 65.5 dB
  parallel-capture-regression  27.3 -> 53.3 dB
  css-spinner-render-compat    30.8 -> 56.4 dB
  variables-prod               30.9 -> 70.5 dB
  gsap-letters-render-compat   30.9 -> 55.1 dB
No regression on comps with in-subtree backgrounds (sub-comp-t0 61.9,
many-cuts 62.3 unchanged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-06-14 23:59:23 -07:00
co-authored by Claude Fable 5
parent aa09bd937c
commit 785077f6ff
@@ -171,6 +171,25 @@ export async function captureDrawElementFrame(
settled = true;
try {
ctx.clearRect(0, 0, w, h);
// drawElementImage only paints the captured subtree. A background
// set on <body>/<html> (the common authoring pattern) lives OUTSIDE
// [data-composition-id], so without this fill those pixels stay
// transparent — and the jpeg encode below turns them black.
// Resolve the nearest non-transparent ancestor background-color and
// paint it first, matching what captureScreenshot composites.
// (Resolved per frame: compositions may set body background from JS.)
let bg = "";
for (let el = root.parentElement; el; el = el.parentElement) {
const c = getComputedStyle(el).backgroundColor;
if (c && c !== "transparent" && c !== "rgba(0, 0, 0, 0)") {
bg = c;
break;
}
}
if (bg) {
ctx.fillStyle = bg;
ctx.fillRect(0, 0, w, h);
}
(
ctx as unknown as { drawElementImage(el: Element, x: number, y: number): void }
).drawElementImage(root, 0, 0);