mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(skills): teach and ship the remotion translation as sub-compositions (#4141)
* fix(skills): teach and ship the remotion translation as sub-compositions The converter reference fixtures now emit one sub-composition per scene under a mounting root, and the api map, sequencing guide and tier-2 README teach that shape instead of the flat data-start div. SSIM per tier is unchanged (t1 0.974415, t2 0.985993, t3 0.953275 to 0.953281). * docs(skills): make every remotion translation doc teach the sub-composition shape SKILL.md step 3, the sequencing guide (nested, series, crossfade), the api map asset rows, and the parameters guide now match the shipped fixtures. * docs(skills): say where scene cuts versus cross-scene fades live
This commit is contained in:
@@ -70,8 +70,8 @@
|
||||
"files": 30
|
||||
},
|
||||
"remotion-to-hyperframes": {
|
||||
"hash": "13ade041e6416e5b",
|
||||
"files": 70
|
||||
"hash": "65222f2d894e8776",
|
||||
"files": 77
|
||||
},
|
||||
"slideshow": {
|
||||
"hash": "71174bff2d869f18",
|
||||
|
||||
@@ -74,10 +74,9 @@ Don't load all of them — load only what the specific source needs.
|
||||
Emit `index.html` with:
|
||||
|
||||
- Root `<div id="stage">` carrying the composition's `data-composition-id`, `data-start="0"`, `data-duration` (in seconds), `data-fps`, `data-width`, `data-height`, plus one `data-*` per scalar prop.
|
||||
- A flat list of scene divs with `data-start` / `data-duration` / `data-track-index`.
|
||||
- Inline `<style>` for layout; CSS sets the `from` state of every animated property.
|
||||
- A single `<script>` tag at the bottom containing one paused `gsap.timeline({paused: true})`. Every Remotion `useCurrentFrame()` derivation becomes a tween on this timeline at the right offset.
|
||||
- `window.__timelines["<composition-id>"] = tl;` registers the timeline with HF's runtime.
|
||||
- One host `<div>` per scene with `data-composition-src="compositions/<scene>.html"` and `data-start` / `data-duration` / `data-track-index`. The root holds no nested layout.
|
||||
- One `compositions/<scene>.html` per scene (a `<template>` sub-composition): its inline `<style>` for layout (CSS sets the `from` state of every animated property), its markup, and one paused `gsap.timeline({paused: true})` in the scene's local time. Every Remotion `useCurrentFrame()` derivation becomes a tween on that timeline at the offset within the scene.
|
||||
- `window.__timelines["<scene-id>"] = tl;` in each scene file, and `window.__timelines["<composition-id>"]` for the root's own (possibly empty) timeline.
|
||||
|
||||
Custom React subcomponents inline as repeated HTML using the prop interface as the template (see [`parameters.md`](references/parameters.md) for the per-instance `data-*` pattern).
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
"ssim_threshold": 0.95,
|
||||
"validation": {
|
||||
"measured_mean_ssim": 0.974,
|
||||
"measured_min_ssim": 0.972,
|
||||
"measured_p05_ssim": 0.972,
|
||||
"measured_p95_ssim": 0.983,
|
||||
"measured_at": "2026-04-27",
|
||||
"measured_against": "remotion@4.0 (PNG output, BT.709) vs hyperframes@0.4.15-alpha.1"
|
||||
"measured_min_ssim": 0.973,
|
||||
"measured_p05_ssim": 0.973,
|
||||
"measured_p95_ssim": 0.984,
|
||||
"measured_at": "2026-09-18",
|
||||
"measured_against": "remotion@4.0 (PNG output, BT.709) vs hyperframes@0.7.71 (sub-composition fixtures)"
|
||||
},
|
||||
"translation_notes": [
|
||||
"Remotion: AbsoluteFill → HF: position:absolute;inset:0 div",
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<template id="title-scene-template">
|
||||
<div data-composition-id="title-scene" data-width="1280" data-height="720" data-duration="3">
|
||||
<style>
|
||||
.title {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
font-size: 160px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.05em;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
<div class="title">HELLO</div>
|
||||
<script>
|
||||
// Translation of Remotion's
|
||||
// interpolate(frame, [0, 15, 75, 90], [0, 1, 1, 0]) at fps=30
|
||||
// into a paused GSAP timeline keyed in seconds.
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
const target = document.querySelector(".title");
|
||||
tl.to(target, { opacity: 1, duration: 0.5, ease: "none" }, 0);
|
||||
tl.to(target, { opacity: 1, duration: 2.0, ease: "none" }, 0.5);
|
||||
tl.to(target, { opacity: 0, duration: 0.5, ease: "none" }, 2.5);
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["title-scene"] = tl;
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
+10
-25
@@ -15,18 +15,6 @@
|
||||
background: #0a0a0a;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
.title {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
font-size: 160px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.05em;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -39,22 +27,19 @@
|
||||
data-duration="3"
|
||||
data-fps="30"
|
||||
>
|
||||
<div id="title" class="clip" data-start="0" data-duration="3" data-track-index="0">
|
||||
<div class="title">HELLO</div>
|
||||
</div>
|
||||
<div
|
||||
id="title"
|
||||
class="clip"
|
||||
data-composition-id="title-scene"
|
||||
data-composition-src="compositions/title.html"
|
||||
data-start="0"
|
||||
data-duration="3"
|
||||
data-track-index="0"
|
||||
></div>
|
||||
|
||||
<script>
|
||||
// Translation of Remotion's
|
||||
// interpolate(frame, [0, 15, 75, 90], [0, 1, 1, 0]) at fps=30
|
||||
// into a paused GSAP timeline keyed in seconds.
|
||||
// Frame ranges → time ranges: 0/30=0, 15/30=0.5, 75/30=2.5, 90/30=3.0
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
const target = document.querySelector("#title .title");
|
||||
tl.to(target, { opacity: 1, duration: 0.5, ease: "none" }, 0);
|
||||
tl.to(target, { opacity: 1, duration: 2.0, ease: "none" }, 0.5);
|
||||
tl.to(target, { opacity: 0, duration: 0.5, ease: "none" }, 2.5);
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["tier-1-title-card"] = tl;
|
||||
window.__timelines["tier-1-title-card"] = gsap.timeline({ paused: true });
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -18,16 +18,16 @@ If a translation passes T2, the skill correctly handles `<Sequence>` boundaries,
|
||||
|
||||
## Translation walk-through
|
||||
|
||||
| Remotion | HyperFrames |
|
||||
| ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| `<Sequence from={0} durationInFrames={60}>` | `<div data-start="0" data-duration="2" data-track-index="0">` |
|
||||
| `spring({frame, fps, config: {damping:12, stiffness:100, mass:1}})` | `gsap.to(target, { scale: 1, duration: 0.7, ease: "back.out(1.4)" })` |
|
||||
| `<Audio src={staticFile("music.wav")} volume={0.5} />` | `<audio src="assets/music.wav" data-start="0" data-duration="6" data-volume="0.5" data-track-index="1">` |
|
||||
| `<Img src={staticFile("square.png")} />` | `<img src="assets/square.png">` (with setup.sh copying into both trees) |
|
||||
| `interpolate(frame, [0, 15], [0, 1])` at 30 fps | `gsap.to(target, { opacity: 1, duration: 0.5, ease: "none" })` |
|
||||
| Remotion | HyperFrames |
|
||||
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| `<Sequence from={0} durationInFrames={60}>` | host `<div data-composition-src="compositions/scene-1.html" data-start="0" data-duration="2" data-track-index="0">` |
|
||||
| `spring({frame, fps, config: {damping:12, stiffness:100, mass:1}})` | `gsap.to(target, { scale: 1, duration: 0.7, ease: "back.out(1.4)" })` |
|
||||
| `<Audio src={staticFile("music.wav")} volume={0.5} />` | `<audio src="assets/music.wav" data-start="0" data-duration="6" data-volume="0.5" data-track-index="1">` |
|
||||
| `<Img src={staticFile("square.png")} />` | `<img src="../assets/square.png">` inside `compositions/scene-2.html` (with setup.sh copying into both trees) |
|
||||
| `interpolate(frame, [0, 15], [0, 1])` at 30 fps | `gsap.to(target, { opacity: 1, duration: 0.5, ease: "none" })` |
|
||||
|
||||
The scene crossfading is a HyperFrames idiom, not a Remotion one: at scene boundaries
|
||||
we `gsap.set(scene, { opacity: 0 })` so the previous scene disappears at the
|
||||
each scene sub-composition sets its own `gsap.set(scene, { opacity: 0 })` at its local end so the scene disappears at the
|
||||
right time. Remotion does this implicitly by virtue of `<Sequence>`'s durationInFrames.
|
||||
|
||||
## How to render and evaluate
|
||||
|
||||
@@ -9,19 +9,19 @@
|
||||
"height": 720,
|
||||
"ssim_threshold": 0.95,
|
||||
"validation": {
|
||||
"measured_mean_ssim": 0.985,
|
||||
"measured_min_ssim": 0.963,
|
||||
"measured_p05_ssim": 0.966,
|
||||
"measured_p95_ssim": 0.999,
|
||||
"measured_at": "2026-04-27",
|
||||
"measured_against": "remotion@4.0 (PNG output, BT.709) vs hyperframes@0.4.15-alpha.1",
|
||||
"notes": "Spring → back.out(1.4) translation came out cleaner than expected; mean 0.985 leaves substantial headroom over 0.95."
|
||||
"measured_mean_ssim": 0.986,
|
||||
"measured_min_ssim": 0.964,
|
||||
"measured_p05_ssim": 0.967,
|
||||
"measured_p95_ssim": 1,
|
||||
"measured_at": "2026-09-18",
|
||||
"measured_against": "remotion@4.0 (PNG output, BT.709) vs hyperframes@0.7.71 (sub-composition fixtures)",
|
||||
"notes": "Spring → back.out(1.4) translation came out cleaner than expected; mean 0.986 leaves substantial headroom over 0.95."
|
||||
},
|
||||
"translation_notes": [
|
||||
"spring({damping:12, stiffness:100, mass:1}) → back.out(1.4) over 0.7s. Spring overshoot+settle and back.out's overshoot+settle have similar shape; budget ~0.02 SSIM for the late-tail curvature mismatch (validated lower than predicted in spec; original notes overestimated drift).",
|
||||
"<Sequence from durationInFrames> → wrapping div with data-start/data-duration in seconds and explicit gsap.set(opacity, 0/1) at scene boundaries to crossfade in/out cleanly",
|
||||
"<Sequence from durationInFrames> → a sub-composition host with data-start/data-duration in seconds; the scene's own file uses local time and explicit gsap.set(opacity, 0/1) at its boundaries to crossfade in/out cleanly",
|
||||
"<Audio src volume> → <audio data-start data-duration data-volume>",
|
||||
"<Img src={staticFile('x')}> → <img src='assets/x'>; setup.sh copies the asset into both fixture trees",
|
||||
"<Img src={staticFile('x')}> → <img src='assets/x'> (`../assets/x` inside the scene sub-composition file); setup.sh copies the asset into both fixture trees",
|
||||
"interpolate with default linear easing → ease:'none' in GSAP",
|
||||
"Fonts again rely on system Helvetica/Arial; ~0.015 SSIM cost from AA differences"
|
||||
],
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<template id="scene-1-template">
|
||||
<div data-composition-id="scene-1" data-width="1280" data-height="720" data-duration="2">
|
||||
<style>
|
||||
.scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
.title {
|
||||
font-size: 140px;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
letter-spacing: 0.05em;
|
||||
transform: scale(0);
|
||||
}
|
||||
</style>
|
||||
<div class="scene">
|
||||
<div class="title">Welcome</div>
|
||||
</div>
|
||||
<script>
|
||||
// Scene 1 (0-2s): spring scale on "Welcome". Remotion:
|
||||
// spring({frame, fps, config: {damping:12, stiffness:100, mass:1}}) -> back.out(1.4) over ~0.7s.
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
const scene = document.querySelector(".scene");
|
||||
tl.set(scene, { opacity: 1 }, 0);
|
||||
tl.to(scene.querySelector(".title"), { scale: 1, duration: 0.7, ease: "back.out(1.4)" }, 0);
|
||||
tl.set(scene, { opacity: 0 }, 2);
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["scene-1"] = tl;
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<template id="scene-2-template">
|
||||
<div data-composition-id="scene-2" data-width="1280" data-height="720" data-duration="2">
|
||||
<style>
|
||||
.scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
.square {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
</style>
|
||||
<div class="scene">
|
||||
<img class="square" src="../assets/square.png" alt="" />
|
||||
</div>
|
||||
<script>
|
||||
// Scene 2 (2-4s, local 0-2s): opacity 0->1 over 0.5s, scale 0.8->1.0 over 2s, both linear.
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
const scene = document.querySelector(".scene");
|
||||
const square = scene.querySelector(".square");
|
||||
tl.set(scene, { opacity: 1 }, 0);
|
||||
tl.to(square, { opacity: 1, duration: 0.5, ease: "none" }, 0);
|
||||
tl.to(square, { scale: 1.0, duration: 2.0, ease: "none" }, 0);
|
||||
tl.set(scene, { opacity: 0 }, 2);
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["scene-2"] = tl;
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<template id="scene-3-template">
|
||||
<div data-composition-id="scene-3" data-width="1280" data-height="720" data-duration="2">
|
||||
<style>
|
||||
.scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
.outro {
|
||||
font-size: 100px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
<div class="scene">
|
||||
<div class="outro">Goodbye</div>
|
||||
</div>
|
||||
<script>
|
||||
// Scene 3 (4-6s, local 0-2s): opacity 0->1 over 1s, linear.
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
const scene = document.querySelector(".scene");
|
||||
tl.set(scene, { opacity: 1 }, 0);
|
||||
tl.fromTo(
|
||||
scene.querySelector(".outro"),
|
||||
{ opacity: 0 },
|
||||
{ opacity: 1, duration: 1.0, ease: "none" },
|
||||
0,
|
||||
);
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["scene-3"] = tl;
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
+28
-71
@@ -15,32 +15,6 @@
|
||||
background: #0a0a0a;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
.scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
.title {
|
||||
font-size: 140px;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
letter-spacing: 0.05em;
|
||||
transform: scale(0);
|
||||
}
|
||||
.square {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
.outro {
|
||||
font-size: 100px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -53,17 +27,35 @@
|
||||
data-duration="6"
|
||||
data-fps="30"
|
||||
>
|
||||
<div id="scene-1" class="scene clip" data-start="0" data-duration="2" data-track-index="0">
|
||||
<div class="title">Welcome</div>
|
||||
</div>
|
||||
<div
|
||||
id="scene-1"
|
||||
class="clip"
|
||||
data-composition-id="scene-1"
|
||||
data-composition-src="compositions/scene-1.html"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-track-index="0"
|
||||
></div>
|
||||
|
||||
<div id="scene-2" class="scene clip" data-start="2" data-duration="2" data-track-index="0">
|
||||
<img class="square" src="assets/square.png" alt="" />
|
||||
</div>
|
||||
<div
|
||||
id="scene-2"
|
||||
class="clip"
|
||||
data-composition-id="scene-2"
|
||||
data-composition-src="compositions/scene-2.html"
|
||||
data-start="2"
|
||||
data-duration="2"
|
||||
data-track-index="0"
|
||||
></div>
|
||||
|
||||
<div id="scene-3" class="scene clip" data-start="4" data-duration="2" data-track-index="0">
|
||||
<div class="outro">Goodbye</div>
|
||||
</div>
|
||||
<div
|
||||
id="scene-3"
|
||||
class="clip"
|
||||
data-composition-id="scene-3"
|
||||
data-composition-src="compositions/scene-3.html"
|
||||
data-start="4"
|
||||
data-duration="2"
|
||||
data-track-index="0"
|
||||
></div>
|
||||
|
||||
<audio
|
||||
id="bg-music"
|
||||
@@ -75,43 +67,8 @@
|
||||
></audio>
|
||||
|
||||
<script>
|
||||
// Translation of Remotion's 3-Sequence MultiScene at fps=30, total 6s.
|
||||
//
|
||||
// Scene 1 (0-2s): TitleScene — spring scale on "Welcome".
|
||||
// Remotion: spring({frame, fps, config: {damping:12, stiffness:100, mass:1}})
|
||||
// GSAP equivalent: back.out(1.4) over ~0.7s. Spring → ease translation
|
||||
// is approximate; expect minor differences in the tail of the curve.
|
||||
//
|
||||
// Scene 2 (2-4s): ImageScene — opacity 0→1 over 0-0.5s + scale 0.8→1.0 over 0-2.0s.
|
||||
// Both Remotion interpolates use linear (default), match with ease:"none".
|
||||
//
|
||||
// Scene 3 (4-6s): OutroScene — opacity 0→1 over 4-5s, linear.
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Scene 1 setup: opacity flick on so scene becomes visible at start
|
||||
const scene1 = document.querySelector("#scene-1");
|
||||
const title = scene1.querySelector(".title");
|
||||
tl.set(scene1, { opacity: 1 }, 0);
|
||||
tl.to(title, { scale: 1, duration: 0.7, ease: "back.out(1.4)" }, 0);
|
||||
tl.set(scene1, { opacity: 0 }, 2);
|
||||
|
||||
// Scene 2
|
||||
const scene2 = document.querySelector("#scene-2");
|
||||
const square = scene2.querySelector(".square");
|
||||
tl.set(scene2, { opacity: 1 }, 2);
|
||||
tl.to(square, { opacity: 1, duration: 0.5, ease: "none" }, 2);
|
||||
tl.to(square, { scale: 1.0, duration: 2.0, ease: "none" }, 2);
|
||||
tl.set(scene2, { opacity: 0 }, 4);
|
||||
|
||||
// Scene 3
|
||||
const scene3 = document.querySelector("#scene-3");
|
||||
const outro = scene3.querySelector(".outro");
|
||||
tl.set(scene3, { opacity: 1 }, 4);
|
||||
tl.fromTo(outro, { opacity: 0 }, { opacity: 1, duration: 1.0, ease: "none" }, 4);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["tier-2-multi-scene"] = tl;
|
||||
window.__timelines["tier-2-multi-scene"] = gsap.timeline({ paused: true });
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
"measured_mean_ssim": 0.953,
|
||||
"measured_min_ssim": 0.927,
|
||||
"measured_p05_ssim": 0.938,
|
||||
"measured_p95_ssim": 0.977,
|
||||
"measured_at": "2026-04-27",
|
||||
"measured_against": "remotion@4.0 (PNG output, BT.709) vs hyperframes@0.4.15-alpha.1",
|
||||
"measured_p95_ssim": 0.978,
|
||||
"measured_at": "2026-09-18",
|
||||
"measured_against": "remotion@4.0 (PNG output, BT.709) vs hyperframes@0.7.71 (sub-composition fixtures)",
|
||||
"notes": "Count-up timing in StatsScene shows a few-frame offset between Remotion's manual 1-(1-t)^3 and GSAP's power3.out — both formulas are identical, so the offset comes from sub-frame timing of when the seek + onUpdate fire. Final values converge correctly. Visible only as transient digit mismatches mid-animation; no SSIM impact above the noise floor."
|
||||
},
|
||||
"remotion_apis_exercised": [
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
<template id="scene-1-template">
|
||||
<div data-composition-id="scene-1" data-width="1280" data-height="720" data-duration="3">
|
||||
<style>
|
||||
.scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
.scene-1 {
|
||||
flex-direction: column;
|
||||
}
|
||||
.scene-1 .title {
|
||||
font-size: 160px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.05em;
|
||||
transform: scale(0);
|
||||
}
|
||||
.scene-1 .subtitle {
|
||||
font-size: 36px;
|
||||
font-weight: 400;
|
||||
color: #9ca3af;
|
||||
margin-top: 24px;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
<div class="scene scene-1">
|
||||
<div class="title">STARGAZED</div>
|
||||
<div class="subtitle">by HeyGen</div>
|
||||
</div>
|
||||
<script>
|
||||
// Remotion: <Sequence 0..90> TitleScene. Frame -> time at 30 fps.
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
const scene = document.querySelector(".scene-1");
|
||||
tl.set(scene, { opacity: 1 }, 0);
|
||||
// spring({damping:12, stiffness:100, mass:1}) -> back.out(1.4) ~0.7s
|
||||
tl.to(scene.querySelector(".title"), { scale: 1, duration: 0.7, ease: "back.out(1.4)" }, 0);
|
||||
// interpolate(frame, [20,40], [0,1]) -> 0.667s..1.333s linear
|
||||
tl.fromTo(
|
||||
scene.querySelector(".subtitle"),
|
||||
{ opacity: 0 },
|
||||
{ opacity: 1, duration: 0.667, ease: "none" },
|
||||
0.667,
|
||||
);
|
||||
tl.set(scene, { opacity: 0 }, 3);
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["scene-1"] = tl;
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
<template id="scene-2-template">
|
||||
<div data-composition-id="scene-2" data-width="1280" data-height="720" data-duration="4">
|
||||
<style>
|
||||
.scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
.scene-2 {
|
||||
gap: 48px;
|
||||
}
|
||||
.stat-card {
|
||||
width: 280px;
|
||||
height: 220px;
|
||||
background: #1a1a1a;
|
||||
border-radius: 16px;
|
||||
border: 2px solid var(--card-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
.stat-card .number {
|
||||
font-size: 72px;
|
||||
font-weight: 800;
|
||||
color: var(--card-color);
|
||||
line-height: 1;
|
||||
}
|
||||
.stat-card .label {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
color: #9ca3af;
|
||||
margin-top: 16px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
</style>
|
||||
<div class="scene scene-2">
|
||||
<div
|
||||
class="stat-card"
|
||||
data-stat-index="0"
|
||||
data-stat-value="1247"
|
||||
style="--card-color: #fbbf24"
|
||||
>
|
||||
<div class="number">0</div>
|
||||
<div class="label">Stars</div>
|
||||
</div>
|
||||
<div
|
||||
class="stat-card"
|
||||
data-stat-index="1"
|
||||
data-stat-value="312"
|
||||
style="--card-color: #60a5fa"
|
||||
>
|
||||
<div class="number">0</div>
|
||||
<div class="label">Forks</div>
|
||||
</div>
|
||||
<div class="stat-card" data-stat-index="2" data-stat-value="48" style="--card-color: #f87171">
|
||||
<div class="number">0</div>
|
||||
<div class="label">Issues</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Remotion: <Sequence 90..210> StatsScene; StatCard stagger i*12 frames = i*0.4s.
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
const scene = document.querySelector(".scene-2");
|
||||
tl.set(scene, { opacity: 1 }, 0);
|
||||
scene.querySelectorAll(".stat-card").forEach((card, i) => {
|
||||
const start = i * 0.4;
|
||||
const value = Number(card.dataset.statValue);
|
||||
const numberEl = card.querySelector(".number");
|
||||
// spring({damping:14, stiffness:90, mass:1}) -> back.out(1.2) ~0.7s; opacity 0..0.4s linear
|
||||
tl.to(card, { scale: 1, duration: 0.7, ease: "back.out(1.2)" }, start);
|
||||
tl.to(card, { opacity: 1, duration: 0.4, ease: "none" }, start);
|
||||
// AnimatedNumber: easeOutCubic over 45 frames (1.5s) is GSAP power3.out on a counter.
|
||||
const counter = { v: 0 };
|
||||
tl.to(
|
||||
counter,
|
||||
{
|
||||
v: value,
|
||||
duration: 1.5,
|
||||
ease: "power3.out",
|
||||
onUpdate: () => {
|
||||
numberEl.textContent = Math.round(counter.v).toLocaleString();
|
||||
},
|
||||
},
|
||||
start,
|
||||
);
|
||||
});
|
||||
tl.set(scene, { opacity: 0 }, 4);
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["scene-2"] = tl;
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<template id="scene-3-template">
|
||||
<div data-composition-id="scene-3" data-width="1280" data-height="720" data-duration="3">
|
||||
<style>
|
||||
.scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
.scene-3 .outro-wrap {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
}
|
||||
.scene-3 .outro-text {
|
||||
font-size: 80px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.scene-3 .outro-underline {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -8px;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: #fbbf24;
|
||||
border-radius: 3px;
|
||||
transform: scaleX(0);
|
||||
transform-origin: left center;
|
||||
}
|
||||
</style>
|
||||
<div class="scene scene-3">
|
||||
<div class="outro-wrap">
|
||||
<div class="outro-text">thanks for watching</div>
|
||||
<div class="outro-underline"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Remotion: <Sequence 210..300> OutroScene.
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
const scene = document.querySelector(".scene-3");
|
||||
tl.set(scene, { opacity: 1 }, 0);
|
||||
tl.to(scene.querySelector(".outro-wrap"), { opacity: 1, duration: 0.4, ease: "none" }, 0);
|
||||
// interpolate(frame, [10,40], [0,1]) for the underline -> starts 0.333s, 1s long
|
||||
tl.to(
|
||||
scene.querySelector(".outro-underline"),
|
||||
{ scaleX: 1, duration: 1.0, ease: "none" },
|
||||
0.333,
|
||||
);
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["scene-3"] = tl;
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
+13
-212
@@ -16,86 +16,6 @@
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
color: #ffffff;
|
||||
}
|
||||
.scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Scene 1 — Title */
|
||||
.scene-1 {
|
||||
flex-direction: column;
|
||||
}
|
||||
.scene-1 .title {
|
||||
font-size: 160px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.05em;
|
||||
transform: scale(0);
|
||||
}
|
||||
.scene-1 .subtitle {
|
||||
font-size: 36px;
|
||||
font-weight: 400;
|
||||
color: #9ca3af;
|
||||
margin-top: 24px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Scene 2 — Stats */
|
||||
.scene-2 {
|
||||
gap: 48px;
|
||||
}
|
||||
.stat-card {
|
||||
width: 280px;
|
||||
height: 220px;
|
||||
background: #1a1a1a;
|
||||
border-radius: 16px;
|
||||
border: 2px solid var(--card-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
.stat-card .number {
|
||||
font-size: 72px;
|
||||
font-weight: 800;
|
||||
color: var(--card-color);
|
||||
line-height: 1;
|
||||
}
|
||||
.stat-card .label {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
color: #9ca3af;
|
||||
margin-top: 16px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
/* Scene 3 — Outro */
|
||||
.scene-3 .outro-wrap {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
}
|
||||
.scene-3 .outro-text {
|
||||
font-size: 80px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.scene-3 .outro-underline {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -8px;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: #fbbf24;
|
||||
border-radius: 3px;
|
||||
transform: scaleX(0);
|
||||
transform-origin: left center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -111,158 +31,39 @@
|
||||
data-subtitle="by HeyGen"
|
||||
data-outro="thanks for watching"
|
||||
>
|
||||
<!-- Scene 1: Title -->
|
||||
<div
|
||||
id="scene-1"
|
||||
class="scene scene-1 clip"
|
||||
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 class="title">STARGAZED</div>
|
||||
<div class="subtitle">by HeyGen</div>
|
||||
</div>
|
||||
></div>
|
||||
|
||||
<!-- Scene 2: Stats — three StatCards as repeated markup with per-instance data attrs -->
|
||||
<div
|
||||
id="scene-2"
|
||||
class="scene scene-2 clip"
|
||||
class="clip"
|
||||
data-composition-id="scene-2"
|
||||
data-composition-src="compositions/scene-2.html"
|
||||
data-start="3"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
>
|
||||
<div
|
||||
class="stat-card"
|
||||
data-stat-index="0"
|
||||
data-stat-value="1247"
|
||||
style="--card-color: #fbbf24"
|
||||
>
|
||||
<div class="number">0</div>
|
||||
<div class="label">Stars</div>
|
||||
</div>
|
||||
<div
|
||||
class="stat-card"
|
||||
data-stat-index="1"
|
||||
data-stat-value="312"
|
||||
style="--card-color: #60a5fa"
|
||||
>
|
||||
<div class="number">0</div>
|
||||
<div class="label">Forks</div>
|
||||
</div>
|
||||
<div
|
||||
class="stat-card"
|
||||
data-stat-index="2"
|
||||
data-stat-value="48"
|
||||
style="--card-color: #f87171"
|
||||
>
|
||||
<div class="number">0</div>
|
||||
<div class="label">Issues</div>
|
||||
</div>
|
||||
</div>
|
||||
></div>
|
||||
|
||||
<!-- Scene 3: Outro -->
|
||||
<div
|
||||
id="scene-3"
|
||||
class="scene scene-3 clip"
|
||||
class="clip"
|
||||
data-composition-id="scene-3"
|
||||
data-composition-src="compositions/scene-3.html"
|
||||
data-start="7"
|
||||
data-duration="3"
|
||||
data-track-index="0"
|
||||
>
|
||||
<div class="outro-wrap">
|
||||
<div class="outro-text">thanks for watching</div>
|
||||
<div class="outro-underline"></div>
|
||||
</div>
|
||||
</div>
|
||||
></div>
|
||||
|
||||
<script>
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// Translation of Stargazed.tsx
|
||||
//
|
||||
// Remotion structure:
|
||||
// <Composition schema={zod} defaultProps={...} fps=30 dur=300>
|
||||
// <Sequence 0..90> <TitleScene title subtitle />
|
||||
// <Sequence 90..210> <StatsScene stats />
|
||||
// <Sequence 210..300><OutroScene text />
|
||||
//
|
||||
// HF structure:
|
||||
// - root #stage carries data-* with the same props (title, subtitle, outro)
|
||||
// - 3 scene divs with data-start/data-duration in seconds (frames/fps)
|
||||
// - Custom React subcomponents inline as repeated HTML; their per-instance
|
||||
// props become data-* attributes on the markup
|
||||
// - Animation lives in a single paused GSAP timeline keyed by composition seconds
|
||||
//
|
||||
// Frame → time:
|
||||
// Sequence(0,90) → 0s..3s (title)
|
||||
// Sequence(90,210) → 3s..7s (stats; 4s window)
|
||||
// Sequence(210,300) → 7s..10s (outro)
|
||||
// Stagger(i*12) → i*0.4s (12 frames at 30fps)
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// ─── Scene 1: Title ─────────────────────────────────────────────────
|
||||
const scene1 = document.querySelector("#scene-1");
|
||||
const title = scene1.querySelector(".title");
|
||||
const subtitle = scene1.querySelector(".subtitle");
|
||||
|
||||
tl.set(scene1, { opacity: 1 }, 0);
|
||||
// spring({damping:12, stiffness:100, mass:1}) → back.out(1.4) ~0.7s
|
||||
tl.to(title, { scale: 1, duration: 0.7, ease: "back.out(1.4)" }, 0);
|
||||
// interpolate(frame, [20,40], [0,1]) at fps=30 → 0.667s..1.333s linear
|
||||
tl.fromTo(subtitle, { opacity: 0 }, { opacity: 1, duration: 0.667, ease: "none" }, 0.667);
|
||||
tl.set(scene1, { opacity: 0 }, 3);
|
||||
|
||||
// ─── Scene 2: Stats ─────────────────────────────────────────────────
|
||||
const scene2 = document.querySelector("#scene-2");
|
||||
const cards = scene2.querySelectorAll(".stat-card");
|
||||
|
||||
tl.set(scene2, { opacity: 1 }, 3);
|
||||
|
||||
cards.forEach((card, i) => {
|
||||
const stagger = i * 0.4; // i * 12 frames at 30 fps
|
||||
const start = 3 + stagger;
|
||||
const value = Number(card.dataset.statValue);
|
||||
const numberEl = card.querySelector(".number");
|
||||
|
||||
// StatCard entrance:
|
||||
// spring({damping:14, stiffness:90, mass:1}) → back.out(1.2) ~0.7s
|
||||
// interpolate(local, [0,12], [0,1]) → 0s..0.4s linear opacity
|
||||
tl.to(card, { scale: 1, duration: 0.7, ease: "back.out(1.2)" }, start);
|
||||
tl.to(card, { opacity: 1, duration: 0.4, ease: "none" }, start);
|
||||
|
||||
// AnimatedNumber: count from 0 → value with easeOutCubic over 45 frames (1.5s).
|
||||
// GSAP equivalent: a tween on a counter object with onUpdate rewriting textContent.
|
||||
// power3.out is GSAP's name for cubic easeOut.
|
||||
const counter = { v: 0 };
|
||||
tl.to(
|
||||
counter,
|
||||
{
|
||||
v: value,
|
||||
duration: 1.5,
|
||||
ease: "power3.out",
|
||||
onUpdate: () => {
|
||||
numberEl.textContent = Math.round(counter.v).toLocaleString();
|
||||
},
|
||||
},
|
||||
start,
|
||||
);
|
||||
});
|
||||
|
||||
tl.set(scene2, { opacity: 0 }, 7);
|
||||
|
||||
// ─── Scene 3: Outro ─────────────────────────────────────────────────
|
||||
const scene3 = document.querySelector("#scene-3");
|
||||
const outroWrap = scene3.querySelector(".outro-wrap");
|
||||
const underline = scene3.querySelector(".outro-underline");
|
||||
|
||||
tl.set(scene3, { opacity: 1 }, 7);
|
||||
// interpolate(frame, [0,12], [0,1]) → 0s..0.4s linear opacity
|
||||
tl.to(outroWrap, { opacity: 1, duration: 0.4, ease: "none" }, 7);
|
||||
// interpolate(frame, [10,40], [0,1]) for underline → starts at 7+0.333s, dur 1s
|
||||
tl.to(underline, { scaleX: 1, duration: 1.0, ease: "none" }, 7.333);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["tier-3-data-driven"] = tl;
|
||||
window.__timelines["tier-3-data-driven"] = gsap.timeline({ paused: true });
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -27,12 +27,12 @@ details (timing, transitions, etc.).
|
||||
|
||||
See [sequencing.md](sequencing.md) for nesting and stagger details.
|
||||
|
||||
| Remotion | HyperFrames |
|
||||
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
|
||||
| `<Sequence from={F} durationInFrames={D}>` | `<div data-start="<F/fps>" data-duration="<D/fps>" data-track-index="N">` |
|
||||
| `<Series>` + `<Series.Sequence>` | siblings with sequential `data-start` values |
|
||||
| `<Loop durationInFrames={D}>` | not a primitive — emit a bounded GSAP repeat from the available duration |
|
||||
| `<Freeze frame={F}>` | drop the wrapper; HF doesn't have running animation outside the seek-driven timeline so freeze is a no-op |
|
||||
| Remotion | HyperFrames |
|
||||
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `<Sequence from={F} durationInFrames={D}>` | a host `<div data-composition-src="compositions/<name>.html" data-start="<F/fps>" data-duration="<D/fps>" data-track-index="N">` in the root, its content in that sub-composition file (see [sequencing.md](sequencing.md)) |
|
||||
| `<Series>` + `<Series.Sequence>` | sibling scene hosts with sequential `data-start` values |
|
||||
| `<Loop durationInFrames={D}>` | not a primitive — emit a bounded GSAP repeat from the available duration |
|
||||
| `<Freeze frame={F}>` | drop the wrapper; HF doesn't have running animation outside the seek-driven timeline so freeze is a no-op |
|
||||
|
||||
## Timing
|
||||
|
||||
@@ -53,16 +53,16 @@ See [timing.md](timing.md) — this is the highest-leverage section.
|
||||
|
||||
See [media.md](media.md) for trim, volume ramps, and decoder notes.
|
||||
|
||||
| Remotion | HyperFrames |
|
||||
| -------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| `<Audio src volume>` | `<audio data-start data-duration data-track-index data-volume src>` |
|
||||
| `<Audio playbackRate startFrom endAt>` | `data-playback-rate`, `data-trim-start`, `data-trim-end` |
|
||||
| `<Video src>` | `<video muted playsinline data-start data-duration data-track-index src>` |
|
||||
| `<OffthreadVideo>` | `<video>` — HF doesn't need the off-thread variant (uses headless Chrome) |
|
||||
| `<Img src>` | `<img>` |
|
||||
| `<IFrame src>` | `<iframe>` — HF auto-falls back to screenshot mode for nested iframes |
|
||||
| `staticFile("x.png")` | `"assets/x.png"` — copy the file into `hf-src/assets/` next to `index.html` |
|
||||
| `delayRender()` / `continueRender()` | drop — HF waits on asset readiness via the Frame Adapter pattern |
|
||||
| Remotion | HyperFrames |
|
||||
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `<Audio src volume>` | `<audio data-start data-duration data-track-index data-volume src>` |
|
||||
| `<Audio playbackRate startFrom endAt>` | `data-playback-rate`, `data-trim-start`, `data-trim-end` |
|
||||
| `<Video src>` | `<video muted playsinline data-start data-duration data-track-index src>` |
|
||||
| `<OffthreadVideo>` | `<video>` — HF doesn't need the off-thread variant (uses headless Chrome) |
|
||||
| `<Img src>` | `<img>` |
|
||||
| `<IFrame src>` | `<iframe>` — HF auto-falls back to screenshot mode for nested iframes |
|
||||
| `staticFile("x.png")` | `"assets/x.png"` in the root, `"../assets/x.png"` inside a `compositions/` scene file — copy the file into `hf-src/assets/` next to `index.html` |
|
||||
| `delayRender()` / `continueRender()` | drop — HF waits on asset readiness via the Frame Adapter pattern |
|
||||
|
||||
## Transitions
|
||||
|
||||
|
||||
@@ -37,6 +37,11 @@ The `data-title` attribute carries the value through. Code that originally
|
||||
read `props.title` reads `document.getElementById("stage").dataset.title`
|
||||
in HF.
|
||||
|
||||
Inside a scene sub-composition file that lookup does not reach the root
|
||||
`#stage`. Pass a prop to a scene with `data-variable-values` on its host and
|
||||
read it there with `window.__hyperframes.getVariables()` (declare defaults with
|
||||
`data-composition-variables` on the scene's composition element).
|
||||
|
||||
## Async calculateMetadata (NOT translatable)
|
||||
|
||||
```tsx
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Sequencing translation: Sequence, Series, Composition root
|
||||
|
||||
How Remotion's nested `Sequence` tree maps to HF's flat `data-start` /
|
||||
`data-duration` markup with a single paused GSAP timeline.
|
||||
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.
|
||||
|
||||
## The core idea
|
||||
|
||||
@@ -11,8 +11,9 @@ 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 siblings on the same
|
||||
parent, each with their own time window.
|
||||
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.
|
||||
|
||||
## `<Composition>` → root `#stage`
|
||||
|
||||
@@ -57,7 +58,7 @@ playback; missing it triggers a lint warning).
|
||||
`AbsoluteFill` is just a styled div in Remotion. Translate to a div with
|
||||
`position:absolute; inset:0` and copy through any other style props.
|
||||
|
||||
## `<Sequence>` → time-windowed div
|
||||
## `<Sequence>` → sub-composition host
|
||||
|
||||
```tsx
|
||||
<Sequence from={0} durationInFrames={90}>
|
||||
@@ -65,15 +66,42 @@ playback; missing it triggers a lint warning).
|
||||
</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.
|
||||
|
||||
```html
|
||||
<div data-start="0" data-duration="3" data-track-index="0">
|
||||
<!-- TitleCard children inlined -->
|
||||
</div>
|
||||
<!-- 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>
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- 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`).
|
||||
|
||||
## Nested `<Sequence>` flattens
|
||||
|
||||
@@ -89,13 +117,17 @@ Remotion adds offsets when sequences nest:
|
||||
|
||||
The inner sequence's effective window is `[60+30, 60+30+60] = [90, 150]`.
|
||||
|
||||
Translate by computing the sum and emitting one HF div with the resolved
|
||||
window:
|
||||
Translate by computing the sum and emitting one host with the resolved
|
||||
window; the scene's children go in its sub-composition file:
|
||||
|
||||
```html
|
||||
<div data-start="3" data-duration="2" data-track-index="0">
|
||||
<!-- ImageScene children -->
|
||||
</div>
|
||||
<div
|
||||
data-composition-id="image-scene"
|
||||
data-composition-src="compositions/image-scene.html"
|
||||
data-start="3"
|
||||
data-duration="2"
|
||||
data-track-index="0"
|
||||
></div>
|
||||
```
|
||||
|
||||
## `<Series>` → siblings with sequential offsets
|
||||
@@ -115,19 +147,40 @@ window:
|
||||
```
|
||||
|
||||
Each `Sequence.Sequence` lives in the next time slot. Emit siblings
|
||||
with `data-start` accumulating:
|
||||
with `data-start` accumulating (each host mounts its scene file):
|
||||
|
||||
```html
|
||||
<div data-start="0" data-duration="2" data-track-index="0">A</div>
|
||||
<div data-start="2" data-duration="4" data-track-index="0">B</div>
|
||||
<div data-start="6" data-duration="3" data-track-index="0">C</div>
|
||||
<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>
|
||||
```
|
||||
|
||||
## 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:
|
||||
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:
|
||||
|
||||
```js
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
Reference in New Issue
Block a user