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 bydata-composition-id. The registry key must exactly match thedata-composition-idon 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 manuallymasterTL.add(...). - Never animate
width/height/top/lefton 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
- Always create timelines with
{ paused: true }— the framework controls playback via deterministic seeking. - Register timelines on
window.__timelineswith thedata-composition-idas the key. - Use the position parameter (3rd argument) for absolute timing:
tl.to(el, vars, 1.5). - Only animate visual properties — never control media playback in scripts.
Supported methods
| Method | Description |
|---|---|
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); // UNNECESSARYTry It
- Register a paused timeline keyed to your root’s
data-composition-id, animate#titlein withtl.from("#title", { opacity: 0, y: -50, duration: 1 }, 0), and confirm playback innpx hyperframes preview. - If a video cuts off early, check the timeline length and extend it with
tl.set({}, {}, <videoSeconds>). - Run
npx hyperframes lintbefore 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.