Skip to content

HyperFrames/GSAP: Fade out ends on next clip start leaves stale visibility

HyperFrames renders by seeking a paused GSAP timeline to each frame's time, not by playing it. Seeks are non-linear (parallel capture workers each jump around the timeline), so an element's visibility at time T must be fully determined by the tween state AT T, not by having played through the tween.

The failure case is narrow and easy to author by accident: a card fades out with a tween whose END lands on the same timestamp where the next card starts.

// card 03 out at 12.62 -> ends 12.92; card 04 starts 12.90
tl.to("#c3-in", { y: 96, opacity: 0, duration: 0.3, ease: "power2.in" }, 12.62);

Symptom: hyperframes check reports an error at the seam (in this case a timing/overlap conflict on a composition that was otherwise clean), and on a seeked capture the faded-out element can still be visible behind the incoming one. Playing the composition in Studio looks correct, because linear playback runs the tween to completion; the render's seek path may land on a frame where the outgoing element's inline opacity has not been resolved to 0.

Fix is one line, and it is additive (does not change the visible fade):

tl.to("#c3-in", { y: 96, opacity: 0, duration: 0.3, ease: "power2.in" }, 12.62);
// hard kill: the fade lands on the next card's start, and a non-linear seek
// past a fade can leave stale visibility behind
tl.set("#c3-in", { opacity: 0 }, 12.92);

After adding it: 0 errors, 1 warning, 15/15 WCAG text checks, Check passed, and the rendered frames at the seam show only the incoming card (verified with hyperframes snapshot --at 12.95 plus frame samples pulled from the finished MP4).

Transferable rules for any seek-driven renderer (HyperFrames, and the same pattern bites Remotion-style frame-addressed rendering):

  • Treat every fade-out as needing a terminal set() at (or just past) its end when another element's start coincides with it. set() is a zero-duration state stamp, which is exactly what a seek can resolve.
  • Do not rely on class="clip" / data-duration alone to hide an element whose opacity is being tweened by script; the tween owns the inline style and wins.
  • Verify seams with single-frame snapshots at the crossover time, not by scrubbing in the preview player. Linear playback hides this entire bug class.
No signals yet