Skip to content

fix(adapter-hyperframes): record real animation, not static end-state#60

Open
liyizhouAI wants to merge 1 commit into
nexu-io:mainfrom
liyizhouAI:fix/adapter-static-end-state
Open

fix(adapter-hyperframes): record real animation, not static end-state#60
liyizhouAI wants to merge 1 commit into
nexu-io:mainfrom
liyizhouAI:fix/adapter-static-end-state

Conversation

@liyizhouAI

Copy link
Copy Markdown

Problem

recordVideo produced clips frozen on the animation's static end-state instead of the animation playing. Two distinct root causes:

  1. CSS-only freeze used animation-play-state: paused. This drifts across the font-wait: a font-display swap triggers relayout mid-wait, the paused timeline drifts, and the animation actually plays during the font wait. The later lead-in trim (-ss seekSec) then slices it off -> static end-state clip.

  2. Single-file GSAP frames (e.g. agent-generated templates) bypass __hvPlayAll. Their gsap.timeline() auto-plays on DOMContentLoaded and is never registered paused, so the CSS freeze doesn't touch GSAP's rAF. The timeline plays out during the ~2-3s font wait and the recording captures only the end-state.

Fix

4 changes in packages/adapter-hyperframes/src/render.ts (+82 / -7):

# Change Why
1 freeze: animation-play-state: paused -> animation: none none fully removes the animation so elements rest at base (frame-0) style; no drift under font swap
2 after goto: pause gsap.globalTimeline holds single-file GSAP frames at frame 0 through the font wait (no-op without GSAP)
3 __hvUnfreeze: add gsap.globalTimeline.play(0) fresh first-play aligned with recording start; play(0) NOT seek(0) -- a completed tween ignores seek(0) and never re-renders
4 lead-in seek: probe webm pixels for first content change (signalstats YMIN) instead of wall-clock leadInMs recordVideo compresses the webm timeline (~0.7x), so a wall-clock-derived seekSec lands at the wrong spot; reading pixels finds the real animation start

Verification

Quantified with ffmpeg signalstats YMIN (frame-darkest pixel; white bg ~210, ink ~20) on a 4-frame single-file-GSAP vertical (1080x1920) sequence:

  • Before: every frame's YMIN flat at ~22 for the full duration = static end-state
  • After: each frame YMIN ~210 (blank) -> drops to ~20 as ink enters -> settles at end-state. 4/4 frames animate; 717 frames total, no corrupt frames.

Notes / scope

  • Only render.ts touched; no public API change.
  • Multi-composition frames (data-composition-src -> __hvPlayAll) were not re-tested in this pass -- they were already on a working path; the freeze change is strictly safer for them too.
  • detectMotionStart returns null on any ffmpeg failure -> falls back to the existing wall-clock estimate, so behavior never regresses if signalstats is unavailable.

Test plan

  • 4-frame single-file-GSAP vertical renders with real animation (YMIN curve as above)
  • CI green

Under recordVideo's continuous capture the CSS freeze doesn't hold through
the ~2-3s font wait: paused drifts on a font-display swap relayout, and
single-file GSAP timelines (e.g. agent-generated frames) auto-play entirely
outside the CSS freeze and finish during the wait. The -ss lead-in trim
then slices the motion off, yielding a static end-state clip.

- freeze: animation-play-state:paused -> animation:none (paused drifts
  on font-display swap relayout)
- after goto: pause gsap.globalTimeline so single-file GSAP rests at
  frame 0 through the font wait (no-op without GSAP)
- __hvUnfreeze: play(0) to release GSAP in sync with recording start
  (NOT seek(0); a completed tween ignores seek and never re-renders)
- ffmpeg seek: probe the webm pixels (signalstats YMIN) for the real
  motion start -- recordVideo compresses the timeline (~0.7x), so a
  wall-clock leadInMs maps to the wrong spot

Verified end-to-end: a 4-frame agent-generated vignelli video went from
all-static (YMIN flat ~20) to each frame opening at frame 0 (YMIN ~210)
and animating in.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant