From b9321b74893ca7235768cc090217badb1b62f8d0 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Wed, 8 Jul 2026 21:19:22 -0700 Subject: [PATCH] fix(engine): descend into repeating nested timelines for call() detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR #2093 review feedback (Miga, Rames D Jusso): - The walker treated a repeating nested timeline (total > single) as an opaque interval and never descended into it, so a tl.call() living inside one would slip past hasTimelineCall detection entirely — the "any tl.call() disqualifies" claim wasn't quite literal. Now recurses for detection purposes even when the span is already opaque; the parent-level interval still dominates for frame-animated-marking, so this only widens what counts as "has a call()," never narrows the existing interval coverage. - Restored the totalDuration() vs duration() rationale comment that got dropped when the tl.call() detection comment was added above it. --- packages/engine/src/services/frameCapture.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/engine/src/services/frameCapture.ts b/packages/engine/src/services/frameCapture.ts index ac031c114..2b0847bc4 100644 --- a/packages/engine/src/services/frameCapture.ts +++ b/packages/engine/src/services/frameCapture.ts @@ -1983,6 +1983,8 @@ export async function computeStaticFrameSet( }; const intervals: Array<{ start: number; end: number }> = []; let tweenCount = 0; + // totalDuration() (NOT duration()): a repeat/yoyo tween animates past one iteration; + // a repeating timeline is marked opaque over its whole span (conservative). // A GSAP tl.call() is a zero-duration tween whose vars wire the callback as // onComplete (and onReverseComplete, fired on backward crossing — GSAP has // no separate "undo" callback, so both directions invoke the SAME forward @@ -2003,8 +2005,18 @@ export async function computeStaticFrameSet( const single = typeof child.duration === "function" ? child.duration() : 0; const total = typeof child.totalDuration === "function" ? child.totalDuration() : single; if (typeof child.getChildren === "function") { - if (total > single + 1e-6) intervals.push({ start, end: start + total }); - else walk(child, start); + if (total > single + 1e-6) { + intervals.push({ start, end: start + total }); + // Still descend for hasTimelineCall even though the repeating + // span is already opaque (its frames are excluded from dedup + // regardless): a call() inside it is a review-flagged detection + // gap otherwise — the arm-time verifier can still forward-seek + // through this span while checking a LATER static run, firing + // the call() and corrupting the page (review). + walk(child, start); + } else { + walk(child, start); + } } else { tweenCount++; intervals.push({ start, end: start + total });