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

timing.md

177 lines · skills/remotion-to-hyperframes/references/timing.md at a595755

Open on GitHub
1# Timing translation: interpolate, spring, easing
2
3The single highest-leverage reference. Easings and timings are what readers
4notice; getting them wrong costs more SSIM than any other translation choice.
5Empirically validated against tiers T1–T3.
6

Timing translation: interpolate, spring, easing

The single highest-leverage reference. Easings and timings are what readers notice; getting them wrong costs more SSIM than any other translation choice. Empirically validated against tiers T1–T3.

Raw lines 1 to 6
7## Conversion: frames → seconds
8
9HF's timeline is in seconds. Remotion is frame-based. Always:
11```
12time_seconds = frame / fps
13```
15So at fps=30:
17- frame 15 → 0.5 s
18- frame 30 → 1.0 s
19- frame 90 → 3.0 s
21Do this conversion once when translating, not at runtime.

Conversion: frames → seconds

HF's timeline is in seconds. Remotion is frame-based. Always:

time_seconds = frame / fps

So at fps=30:

  • frame 15 → 0.5 s
  • frame 30 → 1.0 s
  • frame 90 → 3.0 s

Do this conversion once when translating, not at runtime.

Raw lines 7 to 22
23## interpolate — linear
25```tsx
26const opacity = interpolate(frame, [0, 30], [0, 1], { extrapolateRight: "clamp" });
27```
29Translates to:
31```js
32gsap.to(target, { opacity: 1, duration: 1.0, ease: "none" }, 0);
33// fromTo if the property starts at 0 and CSS doesn't already set it
34gsap.fromTo(target, { opacity: 0 }, { opacity: 1, duration: 1.0, ease: "none" }, 0);
35```
37`ease: "none"` matches Remotion's default linear interpolation. CSS sets the
38`from` value if your initial state is in CSS; otherwise use `fromTo`.
40`extrapolateLeft`/`extrapolateRight` defaults to `"extend"` in Remotion but
41`"clamp"` is what the agent will see most often. GSAP doesn't extend — values
42hold at the start and end of the tween. So for `clamp`, GSAP matches; for
43`extend`, you'd need to extend the input range manually before emitting.

interpolate — linear

const opacity = interpolate(frame, [0, 30], [0, 1], { extrapolateRight: "clamp" });

Translates to:

gsap.to(target, { opacity: 1, duration: 1.0, ease: "none" }, 0);
// fromTo if the property starts at 0 and CSS doesn't already set it
gsap.fromTo(target, { opacity: 0 }, { opacity: 1, duration: 1.0, ease: "none" }, 0);

ease: "none" matches Remotion's default linear interpolation. CSS sets the from value if your initial state is in CSS; otherwise use fromTo.

extrapolateLeft/extrapolateRight defaults to "extend" in Remotion but "clamp" is what the agent will see most often. GSAP doesn't extend — values hold at the start and end of the tween. So for clamp, GSAP matches; for extend, you'd need to extend the input range manually before emitting.

Raw lines 23 to 44
45## interpolate — multi-segment
47```tsx
48const opacity = interpolate(frame, [0, 15, 75, 90], [0, 1, 1, 0]);
49```
51Three keyframed tweens at offsets `[0]/fps`, `[1]/fps`, `[2]/fps`:
53```js
54const tl = gsap.timeline({ paused: true });
55tl.to(target, { opacity: 1, duration: 0.5, ease: "none" }, 0);
56tl.to(target, { opacity: 1, duration: 2.0, ease: "none" }, 0.5);
57tl.to(target, { opacity: 0, duration: 0.5, ease: "none" }, 2.5);
58```
60Validated in T1 — mean SSIM 0.974 against Remotion baseline.

interpolate — multi-segment

const opacity = interpolate(frame, [0, 15, 75, 90], [0, 1, 1, 0]);

Three keyframed tweens at offsets [0]/fps, [1]/fps, [2]/fps:

const tl = gsap.timeline({ paused: true });
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);

Validated in T1 — mean SSIM 0.974 against Remotion baseline.

Raw lines 45 to 61
62## spring → GSAP back.out
64Remotion's `spring()` is the most lossy translation. The mapping is approximate
65but close enough that real-world compositions hold ≥ 0.92 SSIM (T2: 0.985, T3: 0.953).
67| Remotion `spring` config | GSAP equivalent | Validated in |
68| ------------------------------------------------- | ---------------------------------------------------- | -------------------------------- |
69| `{damping: 12, stiffness: 100, mass: 1}` (snappy) | `back.out(1.4)` over ~0.7 s | T2, T3 (TitleScene) |
70| `{damping: 14, stiffness: 90, mass: 1}` (calmer) | `back.out(1.2)` over ~0.7 s | T3 (StatCard) |
71| `{damping: 8, stiffness: 200}` (very bouncy) | `back.out(2.0)` or `elastic.out(1, 0.5)` over ~0.6 s | not validated; budget ~0.05 SSIM |
72| `{overshootClamping: true}` | `power3.out` over ~0.6 s (no overshoot) | not validated |
74**Rule of thumb**: `back.out(N)` overshoot ratio ≈ `(stiffness / damping^2) * 1.4`. For
75`damping:12, stiffness:100` that gives `1.4 * 100/144 = 0.97`, which is close to
76the validated 1.4 (the formula is rough; tune by visual). Default duration is
77~0.7 s for the typical config.
79When the spring's `delay`/`from`/`to` are non-default, scale the duration
80proportionally.

spring → GSAP back.out

Remotion's spring() is the most lossy translation. The mapping is approximate but close enough that real-world compositions hold ≥ 0.92 SSIM (T2: 0.985, T3: 0.953).

Remotion spring configGSAP equivalentValidated in
{damping: 12, stiffness: 100, mass: 1} (snappy)back.out(1.4) over ~0.7 sT2, T3 (TitleScene)
{damping: 14, stiffness: 90, mass: 1} (calmer)back.out(1.2) over ~0.7 sT3 (StatCard)
{damping: 8, stiffness: 200} (very bouncy)back.out(2.0) or elastic.out(1, 0.5) over ~0.6 snot validated; budget ~0.05 SSIM
{overshootClamping: true}power3.out over ~0.6 s (no overshoot)not validated

Rule of thumb: back.out(N) overshoot ratio ≈ (stiffness / damping^2) * 1.4. For damping:12, stiffness:100 that gives 1.4 * 100/144 = 0.97, which is close to the validated 1.4 (the formula is rough; tune by visual). Default duration is ~0.7 s for the typical config.

When the spring's delay/from/to are non-default, scale the duration proportionally.

Raw lines 62 to 81
82## interpolate with custom easing
84```tsx
85import { Easing } from "remotion";
86interpolate(frame, [0, 30], [0, 1], { easing: Easing.out(Easing.cubic) });
87```
89| Remotion | GSAP |
90| ---------------------------- | -------------------------------------------------------------------------------------- |
91| `Easing.in(Easing.linear)` | `ease: "none"` |
92| `Easing.out(Easing.cubic)` | `ease: "power2.out"` |
93| `Easing.inOut(Easing.cubic)` | `ease: "power2.inOut"` |
94| `Easing.out(Easing.poly(N))` | `ease: "power<N-1>.out"` (N=2 quad, 3 cubic, 4 quart, 5 quint) |
95| `Easing.bezier(a,b,c,d)` | `CustomEase.create("c", "M0,0 C${a},${b} ${c},${d} 1,1")` (requires CustomEase plugin) |
96| `Easing.elastic(bounciness)` | `ease: "elastic.out(${bounciness}, 0.3)"` |
97| `Easing.bounce` | `ease: "bounce.out"` |
98| `Easing.back(s)` | `ease: "back.in(${s})"` |
99| `Easing.out(Easing.back(s))` | `ease: "back.out(${s})"` |
101GSAP names polynomial eases by degree minus one: `power1` is quadratic,
102`power2` cubic, `power3` quartic, and `power4` quintic. For `Easing.poly(N)`
103with integer N from 1 through 5, use `power<N-1>` (`power0` is linear). For
104other exponents, pass an easing function; an ease-out is `(t) => 1 - (1 - t) ** N`.
106Remotion and GSAP use the same back-ease overshoot parameter. Remotion defaults
107to `s = 1.70158`; pass that value explicitly to GSAP when porting the default.
108Bare `Easing.back` is an ease-in. Its polynomial is `t * t * ((s + 1) * t - s)`;
109wrapping it in `Easing.out` reverses the direction without changing `s`.

interpolate with custom easing

import { Easing } from "remotion";
interpolate(frame, [0, 30], [0, 1], { easing: Easing.out(Easing.cubic) });
RemotionGSAP
Easing.in(Easing.linear)ease: "none"
Easing.out(Easing.cubic)ease: "power2.out"
Easing.inOut(Easing.cubic)ease: "power2.inOut"
Easing.out(Easing.poly(N))ease: "power<N-1>.out" (N=2 quad, 3 cubic, 4 quart, 5 quint)
Easing.bezier(a,b,c,d)CustomEase.create("c", "M0,0 C${a},${b} ${c},${d} 1,1") (requires CustomEase plugin)
Easing.elastic(bounciness)ease: "elastic.out(${bounciness}, 0.3)"
Easing.bounceease: "bounce.out"
Easing.back(s)ease: "back.in(${s})"
Easing.out(Easing.back(s))ease: "back.out(${s})"

GSAP names polynomial eases by degree minus one: power1 is quadratic, power2 cubic, power3 quartic, and power4 quintic. For Easing.poly(N) with integer N from 1 through 5, use power<N-1> (power0 is linear). For other exponents, pass an easing function; an ease-out is (t) => 1 - (1 - t) ** N.

Remotion and GSAP use the same back-ease overshoot parameter. Remotion defaults to s = 1.70158; pass that value explicitly to GSAP when porting the default. Bare Easing.back is an ease-in. Its polynomial is t * t * ((s + 1) * t - s); wrapping it in Easing.out reverses the direction without changing s.

Raw lines 82 to 110
111## interpolate driving non-numeric properties
113```tsx
114const color = interpolateColors(frame, [0, 30], ["#ff0000", "#0000ff"]);
115```
117GSAP does color tweens natively:
119```js
120gsap.to(target, { color: "#0000ff", duration: 1.0, ease: "none" }, 0);
121```
123Same for `backgroundColor`, `borderColor`. The `from` value is read from CSS
124or the inline style.

interpolate driving non-numeric properties

const color = interpolateColors(frame, [0, 30], ["#ff0000", "#0000ff"]);

GSAP does color tweens natively:

gsap.to(target, { color: "#0000ff", duration: 1.0, ease: "none" }, 0);

Same for backgroundColor, borderColor. The from value is read from CSS or the inline style.

Raw lines 111 to 125
126## Custom count-up / number tweens
128When Remotion uses a frame-driven number ramp (`Math.round(value * eased)`):
130```tsx
131const t = interpolate(frame, [0, 45], [0, 1]);
132const eased = 1 - (1 - t) ** 3; // cubic ease-out
133const value = Math.round(target * eased);
134return <div>{value.toLocaleString()}</div>;
135```
137GSAP equivalent — tween a counter object, write `textContent` on update:
139```js
140const counter = { v: 0 };
141tl.to(
142 counter,
143 {
144 v: target,
145 duration: 1.5,
146 ease: "power2.out",
147 onUpdate: () => {
148 el.textContent = Math.round(counter.v).toLocaleString();
149 },
150 },
151 0,
152);
153```
155`power2.out` matches `1 - (1-t)^3` exactly. At the midpoint, both yield 0.875;
156`power3.out` yields 0.9375 because it is quartic. Compare the displayed count
157at matching frame times as well as the final value; SSIM alone can miss a
158plausible-looking counter that advances too early.

Custom count-up / number tweens

When Remotion uses a frame-driven number ramp (Math.round(value * eased)):

const t = interpolate(frame, [0, 45], [0, 1]);
const eased = 1 - (1 - t) ** 3; // cubic ease-out
const value = Math.round(target * eased);
return <div>{value.toLocaleString()}</div>;

GSAP equivalent — tween a counter object, write textContent on update:

const counter = { v: 0 };
tl.to(
  counter,
  {
    v: target,
    duration: 1.5,
    ease: "power2.out",
    onUpdate: () => {
      el.textContent = Math.round(counter.v).toLocaleString();
    },
  },
  0,
);

power2.out matches 1 - (1-t)^3 exactly. At the midpoint, both yield 0.875; power3.out yields 0.9375 because it is quartic. Compare the displayed count at matching frame times as well as the final value; SSIM alone can miss a plausible-looking counter that advances too early.

Raw lines 126 to 159
160## Stagger via per-instance prop
162When custom subcomponents take a `delayInFrames` prop:
164```tsx
165<StatCard delayInFrames={i * 12} value={...} />
166```
168Translate to GSAP timeline offsets:
170```js
171cards.forEach((card, i) => {
172 const start = base + i * (12 / fps); // i * 0.4s at fps=30
173 tl.to(card, { ... }, start);
174});
175```
177Validated in T3 — three StatCards staggered at 0.0/0.4/0.8 s.

Stagger via per-instance prop

When custom subcomponents take a delayInFrames prop:

<StatCard delayInFrames={i * 12} value={...} />

Translate to GSAP timeline offsets:

cards.forEach((card, i) => {
  const start = base + i * (12 / fps); // i * 0.4s at fps=30
  tl.to(card, { ... }, start);
});

Validated in T3 — three StatCards staggered at 0.0/0.4/0.8 s.

Raw lines 160 to 177