Source: GSAP Animation — HeyGen HyperFrames docs

HyperFrames animates compositions with GSAP, but with one inversion from normal GSAP usage: you build a paused timeline and the framework owns playback, seeking it frame-by-frame at render time. This is the deep-dive on that contract — how to register a timeline on window.__timelines, the methods and properties supported, the rule that timeline duration is composition duration, and the anti-patterns the runtime forbids. For how GSAP plugs into the deterministic render pipeline, see HyperFrames Core Concepts; for the framework overview see the hub, HeyGen Hyperframes.

Key Takeaways

  • Paused timeline, framework-driven. Always create with gsap.timeline({ paused: true }). The runtime controls playback via deterministic seeking — you never call .play().
  • Register on window.__timelines, keyed by data-composition-id. The registry key must exactly match the data-composition-id on the composition’s root element.
  • Composition duration = GSAP timeline duration. The two are directly linked: if your last tween ends at 3s, the composition is 3s — even if a video clip in it is longer.
  • Extend a short timeline to match a long video with a zero-duration tween: tl.set({}, {}, 283). A timeline shorter than its video is the single most common HyperFrames mistake — the video gets cut short.
  • Use the position parameter (3rd argument) for absolute timing: tl.to(el, vars, 1.5).
  • Only animate visual properties. Never drive media playback (.play(), .currentTime) from scripts — the framework owns media playback, clip lifecycle, and sub-timeline nesting.
  • Sub-compositions auto-nest. Each nested composition registers its own timeline; the framework nests it into the parent by data-start. Don’t manually masterTL.add(...).
  • Never animate width/height/top/left on a <video> directly — it can make the browser stop rendering frames. Wrap the video in a <div> and animate the wrapper.

Setup — the registration pattern

Include GSAP and create a paused timeline, then register it on the global registry under the root’s data-composition-id:

<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
<script>
  // 1. Create a paused timeline — the framework controls playback
  const tl = gsap.timeline({ paused: true });
 
  // 2. Add animations using the position parameter (3rd arg) for absolute timing
  tl.to("#title", { opacity: 1, duration: 0.5 }, 0);
 
  // 3. Initialize the global timelines registry (if not already present)
  window.__timelines = window.__timelines || {};
 
  // 4. Register the timeline using the data-composition-id as the key
  window.__timelines["root"] = tl;
</script>

The key in window.__timelines must match the data-composition-id attribute on the composition’s root element (see HyperFrames HTML Schema & Compositions for how the root is structured).

Key rules

  1. Always create timelines with { paused: true } — the framework controls playback via deterministic seeking.
  2. Register timelines on window.__timelines with the data-composition-id as the key.
  3. Use the position parameter (3rd argument) for absolute timing: tl.to(el, vars, 1.5).
  4. Only animate visual properties — never control media playback in scripts.

Supported methods

MethodDescription
tl.to(target, vars, position)Animate to values
tl.from(target, vars, position)Animate from values
tl.fromTo(target, fromVars, toVars, position)Animate from/to values
tl.set(target, vars, position)Set values instantly

Supported properties

opacity, x, y, scale, scaleX, scaleY, rotation, width, height, visibility, color, backgroundColor, and any CSS-animatable property.

Timeline duration is composition duration

A composition’s duration equals its GSAP timeline duration — the two are inseparable:

// Your last animation ends at 3 seconds...
tl.from("#title", { opacity: 0, y: -50, duration: 1 }, 0);
tl.to("#title", { opacity: 0, duration: 1 }, 2);
// ...so this composition is exactly 3 seconds long.

If a composition contains a 283-second video clip but the last GSAP animation ends at 8 seconds, the composition is only 8 seconds long and the video is cut short. Extend the timeline with a zero-duration tween that touches no elements:

// All your visual animations
tl.to("#lower-third", { left: -640, duration: 0.6 }, 7.2);
 
// Extend the timeline to 283s to match the video length —
// a zero-duration tween at 283s, affecting no elements.
tl.set({}, {}, 283);

This is one of the most common HyperFrames mistakes: if the video cuts off early, the timeline is too short (see HyperFrames Common Mistakes & Troubleshooting).

What not to do

These patterns break the composition or cause sync issues:

// WRONG: Playing media in scripts — the framework owns media playback
document.getElementById("el-video").play();
document.getElementById("el-audio").currentTime = 5;
 
// WRONG: Creating a non-paused timeline
const tl = gsap.timeline(); // missing { paused: true }!
 
// WRONG: Animating dimensions directly on a <video> element
tl.to("#el-video", { width: 500, height: 280 }, 5);
 
// WRONG: Manually nesting sub-timelines
const masterTL = window.__timelines["root"];
masterTL.add(window.__timelines["intro-anim"], 0);

The framework automatically manages media playback, clip lifecycle, and sub-composition nesting — scripts that duplicate this behavior conflict with it.

Sub-composition timelines

Each nested composition registers its own timeline, and the framework auto-nests it into the parent based on the child’s data-start:

// In compositions/intro-anim.html
const tl = gsap.timeline({ paused: true });
tl.from(".title", { opacity: 0, y: -50, duration: 1 });
window.__timelines["intro-anim"] = tl;
 
// DO NOT manually add sub-timelines to the master:
// masterTL.add(window.__timelines["intro-anim"], 0); // UNNECESSARY

Try It

  • Register a paused timeline keyed to your root’s data-composition-id, animate #title in with tl.from("#title", { opacity: 0, y: -50, duration: 1 }, 0), and confirm playback in npx hyperframes preview.
  • If a video cuts off early, check the timeline length and extend it with tl.set({}, {}, <videoSeconds>).
  • Run npx hyperframes lint before rendering to catch a non-paused or unregistered timeline (and unmuted video) — the cheap structural gate.
  • Translate prompt vocabulary to eases by hand: smooth → power2.out, snappy → power4.out, bouncy → back.out — see Prompting HyperFrames for the full table.