Skill Tracer
Skills › HyperFrames › remotion-to-hyperframes › sequencing.md

sequencing.md

251 lines · skills/remotion-to-hyperframes/references/sequencing.md at a595755

Open on GitHub
1# Sequencing translation: Sequence, Series, Composition root
2
3How Remotion's nested `Sequence` tree maps to one sub-composition host per
4scene in the root, each with its own paused GSAP timeline in local time.
5

Sequencing translation: Sequence, Series, Composition root

How Remotion's nested Sequence tree maps to one sub-composition host per scene in the root, each with its own paused GSAP timeline in local time.

Raw lines 1 to 5
6## The core idea
7
8Remotion's `<Sequence from={F} durationInFrames={D}>` is a coordinate
9transform: it shifts `useCurrentFrame()` by `F` and clips the child
10component to the window `[F, F+D]`. HF doesn't have a per-element
11"current frame" — there's a single composition seek time and the
12runtime hides/shows elements based on their `data-start` / `data-duration`.
14Result: the nested tree flattens into a list of scene hosts on the same
15parent, each with its own time window; each scene's content lives in its
16own sub-composition file.

The core idea

Remotion's <Sequence from={F} durationInFrames={D}> is a coordinate transform: it shifts useCurrentFrame() by F and clips the child component to the window [F, F+D]. HF doesn't have a per-element "current frame" — there's a single composition seek time and the runtime hides/shows elements based on their data-start / data-duration.

Result: the nested tree flattens into a list of scene hosts on the same parent, each with its own time window; each scene's content lives in its own sub-composition file.

Raw lines 6 to 17
18## `<Composition>` → root `#stage`
20```tsx
21<Composition
22 id="MyVideo"
23 component={MyVideo}
24 durationInFrames={300}
25 fps={30}
26 width={1280}
27 height={720}
28/>
29```
31```html
32<div
33 id="stage"
34 data-composition-id="MyVideo"
35 data-start="0"
36 data-duration="10" <!-- 300/30 -->
37 data-fps="30"
38 data-width="1280"
39 data-height="720"
41 <!-- composition content -->
42</div>
43```
45`data-start="0"` is required on `#stage` (the runtime needs it to anchor
46playback; missing it triggers a lint warning).

<Composition> → root #stage

<Composition
  id="MyVideo"
  component={MyVideo}
  durationInFrames={300}
  fps={30}
  width={1280}
  height={720}
/>
<div
  id="stage"
  data-composition-id="MyVideo"
  data-start="0"
  data-duration="10"      <!-- 300/30 -->
  data-fps="30"
  data-width="1280"
  data-height="720"
>
  <!-- composition content -->
</div>

data-start="0" is required on #stage (the runtime needs it to anchor playback; missing it triggers a lint warning).

Raw lines 18 to 47
48## `<AbsoluteFill>` → positioned div
50```tsx
51<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>...children...</AbsoluteFill>
52```
54```html
55<div style="position:absolute;inset:0;background-color:#0a0a0a;">...children...</div>
56```
58`AbsoluteFill` is just a styled div in Remotion. Translate to a div with
59`position:absolute; inset:0` and copy through any other style props.

<AbsoluteFill> → positioned div

<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>...children...</AbsoluteFill>
<div style="position:absolute;inset:0;background-color:#0a0a0a;">...children...</div>

AbsoluteFill is just a styled div in Remotion. Translate to a div with position:absolute; inset:0 and copy through any other style props.

Raw lines 48 to 60
61## `<Sequence>` → sub-composition host
63```tsx
64<Sequence from={0} durationInFrames={90}>
65 <TitleCard />
66</Sequence>
67```
69The root `#stage` only holds a host per scene; the scene's markup, styles
70and timeline live in their own sub-composition file, so the Studio timeline
71gets one readable row per scene and the lint has no nested structure to flag.
73```html
74<!-- index.html -->
75<div
76 id="scene-1"
77 class="clip"
78 data-composition-id="scene-1"
79 data-composition-src="compositions/scene-1.html"
80 data-start="0"
81 data-duration="3"
82 data-track-index="0"
83></div>
84```
86```html
87<!-- compositions/scene-1.html: TitleCard children inlined, timeline keyed "scene-1", local time starts at 0 -->
88<template id="scene-1-template">
89 <div data-composition-id="scene-1" data-width="1280" data-height="720" data-duration="3">
90 <!-- TitleCard children -->
91 <script>
92 const tl = gsap.timeline({ paused: true });
93 window.__timelines["scene-1"] = tl;
94 </script>
95 </div>
96</template>
97```
99Convert frames to seconds: `from/fps`, `durationInFrames/fps`. Pick a
100`data-track-index` per parallel rendering layer (background = 0,
101overlays = 1, audio = 2, etc.). Sequential scenes can share an index.
102Inside the sub-composition, time is local: a tween that started at `F/fps`
103in the root starts at `0` here. Asset paths are relative to the
104sub-composition file (`../assets/x.png`).

<Sequence> → sub-composition host

<Sequence from={0} durationInFrames={90}>
  <TitleCard />
</Sequence>

The root #stage only holds a host per scene; the scene's markup, styles and timeline live in their own sub-composition file, so the Studio timeline gets one readable row per scene and the lint has no nested structure to flag.

<!-- index.html -->
<div
  id="scene-1"
  class="clip"
  data-composition-id="scene-1"
  data-composition-src="compositions/scene-1.html"
  data-start="0"
  data-duration="3"
  data-track-index="0"
></div>
<!-- compositions/scene-1.html: TitleCard children inlined, timeline keyed "scene-1", local time starts at 0 -->
<template id="scene-1-template">
  <div data-composition-id="scene-1" data-width="1280" data-height="720" data-duration="3">
    <!-- TitleCard children -->
    <script>
      const tl = gsap.timeline({ paused: true });
      window.__timelines["scene-1"] = tl;
    </script>
  </div>
</template>

Convert frames to seconds: from/fps, durationInFrames/fps. Pick a data-track-index per parallel rendering layer (background = 0, overlays = 1, audio = 2, etc.). Sequential scenes can share an index. Inside the sub-composition, time is local: a tween that started at F/fps in the root starts at 0 here. Asset paths are relative to the sub-composition file (../assets/x.png).

Raw lines 61 to 105
106## Nested `<Sequence>` flattens
108Remotion adds offsets when sequences nest:
110```tsx
111<Sequence from={60} durationInFrames={120}>
112 <Sequence from={30} durationInFrames={60}>
113 <ImageScene />
114 </Sequence>
115</Sequence>
116```
118The inner sequence's effective window is `[60+30, 60+30+60] = [90, 150]`.
120Translate by computing the sum and emitting one host with the resolved
121window; the scene's children go in its sub-composition file:
123```html
124<div
125 data-composition-id="image-scene"
126 data-composition-src="compositions/image-scene.html"
127 data-start="3"
128 data-duration="2"
129 data-track-index="0"
130></div>
131```

Nested <Sequence> flattens

Remotion adds offsets when sequences nest:

<Sequence from={60} durationInFrames={120}>
  <Sequence from={30} durationInFrames={60}>
    <ImageScene />
  </Sequence>
</Sequence>

The inner sequence's effective window is [60+30, 60+30+60] = [90, 150].

Translate by computing the sum and emitting one host with the resolved window; the scene's children go in its sub-composition file:

<div
  data-composition-id="image-scene"
  data-composition-src="compositions/image-scene.html"
  data-start="3"
  data-duration="2"
  data-track-index="0"
></div>
Raw lines 106 to 132
133## `<Series>` → siblings with sequential offsets
135```tsx
136<Series>
137 <Series.Sequence durationInFrames={60}>
138 <A />
139 </Series.Sequence>
140 <Series.Sequence durationInFrames={120}>
141 <B />
142 </Series.Sequence>
143 <Series.Sequence durationInFrames={90}>
144 <C />
145 </Series.Sequence>
146</Series>
147```
149Each `Sequence.Sequence` lives in the next time slot. Emit siblings
150with `data-start` accumulating (each host mounts its scene file):
152```html
153<div
154 data-composition-id="a"
155 data-composition-src="compositions/a.html"
156 data-start="0"
157 data-duration="2"
158 data-track-index="0"
159></div>
160<div
161 data-composition-id="b"
162 data-composition-src="compositions/b.html"
163 data-start="2"
164 data-duration="4"
165 data-track-index="0"
166></div>
167<div
168 data-composition-id="c"
169 data-composition-src="compositions/c.html"
170 data-start="6"
171 data-duration="3"
172 data-track-index="0"
173></div>
174```

<Series> → siblings with sequential offsets

<Series>
  <Series.Sequence durationInFrames={60}>
    <A />
  </Series.Sequence>
  <Series.Sequence durationInFrames={120}>
    <B />
  </Series.Sequence>
  <Series.Sequence durationInFrames={90}>
    <C />
  </Series.Sequence>
</Series>

Each Sequence.Sequence lives in the next time slot. Emit siblings with data-start accumulating (each host mounts its scene file):

<div
  data-composition-id="a"
  data-composition-src="compositions/a.html"
  data-start="0"
  data-duration="2"
  data-track-index="0"
></div>
<div
  data-composition-id="b"
  data-composition-src="compositions/b.html"
  data-start="2"
  data-duration="4"
  data-track-index="0"
></div>
<div
  data-composition-id="c"
  data-composition-src="compositions/c.html"
  data-start="6"
  data-duration="3"
  data-track-index="0"
></div>
Raw lines 133 to 175
176## Crossfading scene boundaries
178Remotion `<Sequence>` shows/hides at hard boundaries by default. HF does
179the same — but if your composition needs a smooth fade between scenes,
180you have to drive opacity explicitly with GSAP at the boundary. A scene can
181hard-cut itself with `gsap.set` at its local end (the corpus fixtures do). A
182fade between two scenes goes on the scene hosts from the root timeline, since
183a scene's own timeline only sees its own file:
185```js
186const tl = gsap.timeline({ paused: true });
187tl.set(scene1, { opacity: 1 }, 0);
188tl.set(scene1, { opacity: 0 }, 2); // hard cut at 2s
189tl.set(scene2, { opacity: 1 }, 2);
190```
192For a 0.5 s crossfade:
194```js
195tl.to(scene1, { opacity: 0, duration: 0.5 }, 1.5);
196tl.to(scene2, { opacity: 1, duration: 0.5 }, 1.5);
197```
199For Remotion `<TransitionSeries>` translations see [transitions.md](transitions.md).

Crossfading scene boundaries

Remotion <Sequence> shows/hides at hard boundaries by default. HF does the same — but if your composition needs a smooth fade between scenes, you have to drive opacity explicitly with GSAP at the boundary. A scene can hard-cut itself with gsap.set at its local end (the corpus fixtures do). A fade between two scenes goes on the scene hosts from the root timeline, since a scene's own timeline only sees its own file:

const tl = gsap.timeline({ paused: true });
tl.set(scene1, { opacity: 1 }, 0);
tl.set(scene1, { opacity: 0 }, 2); // hard cut at 2s
tl.set(scene2, { opacity: 1 }, 2);

For a 0.5 s crossfade:

tl.to(scene1, { opacity: 0, duration: 0.5 }, 1.5);
tl.to(scene2, { opacity: 1, duration: 0.5 }, 1.5);

For Remotion <TransitionSeries> translations see transitions.md.

Raw lines 176 to 200
201## `<Loop>`
203```tsx
204<Loop durationInFrames={30}>
205 <Spinner />
206</Loop>
207```
209HF doesn't have a `<Loop>` primitive. Translate it to a bounded GSAP timeline using
210the time available at its insertion point:
212```js
213const cycleDuration = 1;
214const availableDuration = compositionDuration - 3;
215const repeat = Math.max(0, Math.floor(availableDuration / cycleDuration) - 1);
216const spinTl = gsap.timeline({ paused: true, repeat, repeatRefresh: false });
217spinTl.to(spinner, { rotate: 360, duration: 1.0, ease: "none" });
218// Embed in the main composition timeline at the right offset:
219mainTl.add(spinTl, 3);
220```
222This is fragile — Remotion's `<Loop>` resets internal state every iteration,
223which GSAP repeat does too, but if the looped child has its own animation,
224you need to be careful that GSAP's `repeatRefresh` is on or off as needed.
225The finite count is required because HyperFrames seeks a bounded composition frame-by-frame.

<Loop>

<Loop durationInFrames={30}>
  <Spinner />
</Loop>

HF doesn't have a <Loop> primitive. Translate it to a bounded GSAP timeline using the time available at its insertion point:

const cycleDuration = 1;
const availableDuration = compositionDuration - 3;
const repeat = Math.max(0, Math.floor(availableDuration / cycleDuration) - 1);
const spinTl = gsap.timeline({ paused: true, repeat, repeatRefresh: false });
spinTl.to(spinner, { rotate: 360, duration: 1.0, ease: "none" });
// Embed in the main composition timeline at the right offset:
mainTl.add(spinTl, 3);

This is fragile — Remotion's <Loop> resets internal state every iteration, which GSAP repeat does too, but if the looped child has its own animation, you need to be careful that GSAP's repeatRefresh is on or off as needed. The finite count is required because HyperFrames seeks a bounded composition frame-by-frame.

Raw lines 201 to 226
227## `<Freeze>`
229```tsx
230<Freeze frame={30}>
231 <Animated />
232</Freeze>
233```
235Drop the wrapper. `<Freeze>` pins `useCurrentFrame()` at a constant for
236the children — but in HF, the children's animation is already driven by
237explicit GSAP tweens, so freeze translates to "don't tween this element".

<Freeze>

<Freeze frame={30}>
  <Animated />
</Freeze>

Drop the wrapper. <Freeze> pins useCurrentFrame() at a constant for the children — but in HF, the children's animation is already driven by explicit GSAP tweens, so freeze translates to "don't tween this element".

Raw lines 227 to 238
239## Multiple parallel tracks
241When you have a background video + overlay text + audio playing
242simultaneously, use distinct `data-track-index` values:
244```html
245<div data-track-index="0">background video</div>
246<div data-track-index="1">overlay text</div>
247<audio data-track-index="2" ...></audio>
248```
250The runtime picks track ordering from the index. See [media.md](media.md)
251for media-specific track conventions.

Multiple parallel tracks

When you have a background video + overlay text + audio playing simultaneously, use distinct data-track-index values:

<div data-track-index="0">background video</div>
<div data-track-index="1">overlay text</div>
<audio data-track-index="2" ...></audio>

The runtime picks track ordering from the index. See media.md for media-specific track conventions.

Raw lines 239 to 251