feat(producer): auto-fallback screenshot capture for raf and iframes (#331)

* fix(core): drive adapter seeks when composition has no GSAP timeline

renderSeek returned early when deps.getTimeline() was null, skipping the
onDeterministicSeek call that drives all frame adapters (CSS, WAAPI,
Lottie, Three.js). That meant compositions using any non-GSAP animation
primitive froze on their initial frame during capture.

Now we still quantize the seek time and fire onDeterministicSeek even
without a timeline, so each adapter gets a chance to advance.

GSAP compositions are unaffected — timeline-driven seek still takes the
same path it did before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(producer): auto-fallback screenshot capture for raf and iframes

Co-Authored-By: Codex <codex@openai.com>

* test(producer): add render compatibility regression fixtures

Co-Authored-By: Codex <codex@openai.com>

* fix(core): scrub CSS animations via WAAPI currentTime

Co-Authored-By: Codex <codex@openai.com>

* test(producer): cover css keyframe renders

Co-Authored-By: Codex <codex@openai.com>

* fix(producer): propagate virtual time into iframe documents

Co-Authored-By: Codex <codex@openai.com>

* test(producer): refresh iframe docker golden

Co-Authored-By: Codex <codex@openai.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
James Russo
2026-04-18 15:08:52 -07:00
committed by GitHub
co-authored by Claude Opus 4.7 Codex
parent 59aa2c9ec9
commit ad11de698c
31 changed files with 951 additions and 50 deletions
@@ -0,0 +1,13 @@
{
"name": "gsap-letters-render-compat",
"description": "Regression guard for the GSAP-only baseline. This suite must stay visually stable while render-compat fallback logic changes around it.",
"tags": ["regression", "render-compat"],
"minPsnr": 30,
"maxFrameFailures": 0,
"minAudioCorrelation": 0,
"maxAudioLagWindows": 1,
"renderConfig": {
"fps": 30,
"workers": 1
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:52d1b047b61edce8109ed49bf3812d73030c0d9d65b0075a9f788a4e31d11ce2
size 420071
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; background: #0a0a0f; width: 1920px; height: 1080px; font-family: "Inter", system-ui, sans-serif; color: #f5f5f5; }
.stage { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 18px; }
.letter { font-size: 180px; font-weight: 800; letter-spacing: -0.04em; display: inline-block; transform: translateY(-400px); opacity: 0; }
</style>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
</head>
<body>
<div id="root" data-composition-id="gsap-letters"
data-width="1920" data-height="1080"
data-start="0" data-duration="4">
<div class="stage clip" data-start="0" data-duration="4">
<span class="letter">H</span><span class="letter">Y</span><span class="letter">P</span><span class="letter">E</span><span class="letter">R</span><span class="letter">F</span><span class="letter">R</span><span class="letter">A</span><span class="letter">M</span><span class="letter">E</span><span class="letter">S</span>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.to(".letter", { y: 0, opacity: 1, duration: 0.5, stagger: 0.08, ease: "back.out(1.8)" }, 0);
tl.to(".letter", { rotation: 720, y: 400, opacity: 0, duration: 0.7, stagger: 0.05, ease: "power2.in" }, 2.5);
window.__timelines["gsap-letters"] = tl;
</script>
</body>
</html>