Skip to content

Sub-composition slot goes black after GSAP timeline ends, regardless of host data-duration #911

Closed

Description

@CypherPoet

Describe the bug

When a sub-composition's internal GSAP timeline runs shorter than its host's data-duration, the slot renders correctly during the timeline window, then goes black for the rest of the slot. The runtime's own skill rule says data-duration takes precedence over GSAP timeline length — so the sub-comp should hold its final timeline state through the full slot.

Root compositions hold correctly. Only sub-compositions loaded via data-composition-src exhibit this.

Link to reproduction

https://github.com/CypherPoet/hyperframes-subcomp-blanks-after-timeline-end

Steps to reproduce

  1. Clone the repo above.
  2. npx hyperframes render --fps 60 --quality high --output repro.mp4
  3. Sample frames across the 3s output:
    for t in 0.5 1.0 1.5 2.0 2.5 2.9; do
      ffmpeg -y -loglevel error -ss $t -i repro.mp4 -frames:v 1 frame-${t}.png
    done
  4. Open each PNG.

The repo contains exactly these two files:

index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <style>
      html, body { margin: 0; padding: 0; background: #111; }
      #root { position: relative; width: 1920px; height: 1080px; background: #111; }
      .scene { position: absolute; inset: 0; }
    </style>
  </head>
  <body>
    <div
      id="root"
      data-composition-id="root"
      data-width="1920"
      data-height="1080"
      data-start="0"
      data-duration="3"
    >
      <!-- Host slot is 3s. Sub-comp's internal GSAP timeline runs only 0–1s. -->
      <div
        id="sub-host"
        class="clip scene"
        data-composition-id="sub"
        data-composition-src="compositions/sub.html"
        data-start="0"
        data-duration="3"
        data-track-index="1"
      ></div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
    <script>
      const tl = gsap.timeline({ paused: true });
      tl.set("#sub-host", { opacity: 1 }, 0);
      window.__timelines = window.__timelines || {};
      window.__timelines["root"] = tl;
    </script>
  </body>
</html>

compositions/sub.html

<!doctype html>
<html lang="en">
  <head><meta charset="UTF-8" /></head>
  <body>
    <template id="sub-template">
      <div data-composition-id="sub" data-width="1920" data-height="1080">
        <style>
          .sub-root {
            position: absolute; inset: 0;
            display: flex; align-items: center; justify-content: center;
            background: #1d2540;
            color: #f8f4e8;
            font: 600 200px/1 system-ui, sans-serif;
          }
        </style>
        <div class="sub-root"><div class="sub-text">HOLD ME</div></div>
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
        <script>
          (function () {
            const tl = gsap.timeline({ paused: true });
            // Entrance lands at t=1.0s. No further tweens.
            tl.fromTo(
              ".sub-text",
              { y: 80, opacity: 0 },
              { y: 0, opacity: 1, duration: 1.0, ease: "power3.out" },
              0,
            );
            window.__timelines = window.__timelines || {};
            window.__timelines["sub"] = tl;
          })();
        </script>
      </div>
    </template>
  </body>
</html>

Expected behavior

The sub-composition holds its final timeline state through the full 3s host slot. At every sampled timestamp (0.5s through 2.9s), the rendered frame shows the navy background and "HOLD ME" text — mid-entrance at 0.5s, fully composed from 1.0s onward.

This matches the runtime's documented rule (see "Additional context" below).

Actual behavior

The sub-composition is visible during 0–1s, then the slot goes black for the rest:

t Result
0.5s ✅ Text mid-entrance
1.0s ❌ Black
1.5s ❌ Black
2.0s ❌ Black
2.5s ❌ Black
2.9s ❌ Black

The slot's #1d2540 background (on .sub-root inside the sub-composition) also disappears — only the root container's #111 shows through. The sub-composition DOM stops rendering, not just the GSAP-animated elements.

Environment

✓ Version          0.6.14 (latest)
  ✓ Node.js          v24.14.1 (darwin arm64)
  ✓ FFmpeg           ffmpeg version 8.1.1
  ✓ Chrome           cache: chrome-headless-shell mac_arm-147.0.7727.57

Additional context

What the docs say

The skill documentation that ships with the package (dist/skills/hyperframes/SKILL.md) lists this as a non-negotiable rule under "Timeline Contract":

Duration comes from data-duration, not from GSAP timeline length

And under "Composition Clips":

data-duration ... Takes precedence over GSAP timeline duration

Root compositions honor the rule. Sub-compositions don't.

Workaround

Extending the sub-composition's GSAP timeline past the host's data-duration keeps the slot visible. Any tween whose end falls at t ≥ duration works — even a no-op tween between identical values:

// Inside the sub-composition's timeline, after the real entrance:
tl.to(".sub-text", { opacity: 1, duration: 2.0 }, 1.0);

With this added to the repro, all six sample timestamps render correctly. The entrance state recovers fine — the runtime simply stops seeking into it after the timeline ends.

Possible fixes

  1. Runtime: if the GSAP adapter clamps seek(localTime) to timeline.duration(), clamp to the host's data-duration instead — or call tl.progress(1) once localTime >= timeline.duration(). Either holds the end state.
  2. Docs: if internal timeline length is intentionally authoritative for sub-comps, the "data-duration takes precedence" rule needs the exception called out, and the workaround needs a name.

Activity

  1. miguel-heygen commented on May 17, 2026

    @miguel-heygen
    Collaborator

    Should be fixed by #917

  2. added a commit that references this issue on May 17, 2026
    2b46565
  3. miguel-heygen commented on May 17, 2026

    @miguel-heygen
    Collaborator

    Follow-up fix landed in 2b46565c. PR #917 only fixed the preview path — the render path was still broken because the producer's htmlCompiler.ts strips data-composition-src during inlining without setting data-composition-file (the core bundler sets both). The runtime now checks both attributes, and the producer mirrors the core bundler's behavior.

    Verified with the exact repro from this issue — all 6 timestamps now render correctly in both preview and render modes.

  4. added 3 commits that reference this issue on Aug 6, 2026
    189d5b4
    8e7f7c1
    2eb7bcb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions