85453da49f
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
166 lines
6.1 KiB
Markdown
166 lines
6.1 KiB
Markdown
# 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.
|
||
|
||
## 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.
|
||
|
||
## interpolate — linear
|
||
|
||
```tsx
|
||
const opacity = interpolate(frame, [0, 30], [0, 1], { extrapolateRight: "clamp" });
|
||
```
|
||
|
||
Translates to:
|
||
|
||
```js
|
||
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.
|
||
|
||
## interpolate — multi-segment
|
||
|
||
```tsx
|
||
const opacity = interpolate(frame, [0, 15, 75, 90], [0, 1, 1, 0]);
|
||
```
|
||
|
||
Three keyframed tweens at offsets `[0]/fps`, `[1]/fps`, `[2]/fps`:
|
||
|
||
```js
|
||
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.
|
||
|
||
## 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` config | GSAP equivalent | Validated in |
|
||
| ------------------------------------------------- | ---------------------------------------------------- | -------------------------------- |
|
||
| `{damping: 12, stiffness: 100, mass: 1}` (snappy) | `back.out(1.4)` over ~0.7 s | T2, T3 (TitleScene) |
|
||
| `{damping: 14, stiffness: 90, mass: 1}` (calmer) | `back.out(1.2)` over ~0.7 s | T3 (StatCard) |
|
||
| `{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 |
|
||
| `{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.
|
||
|
||
## interpolate with custom easing
|
||
|
||
```tsx
|
||
import { Easing } from "remotion";
|
||
interpolate(frame, [0, 30], [0, 1], { easing: Easing.out(Easing.cubic) });
|
||
```
|
||
|
||
| Remotion | GSAP |
|
||
| ---------------------------- | -------------------------------------------------------------------------------------- |
|
||
| `Easing.in(Easing.linear)` | `ease: "none"` |
|
||
| `Easing.out(Easing.cubic)` | `ease: "power3.out"` |
|
||
| `Easing.inOut(Easing.cubic)` | `ease: "power3.inOut"` |
|
||
| `Easing.out(Easing.poly(N))` | `ease: "power<N>.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.bounce` | `ease: "bounce.out"` |
|
||
| `Easing.back(overshoot)` | `ease: "back.out(${overshoot * 1.7})"` (Remotion's overshoot scale differs) |
|
||
|
||
## interpolate driving non-numeric properties
|
||
|
||
```tsx
|
||
const color = interpolateColors(frame, [0, 30], ["#ff0000", "#0000ff"]);
|
||
```
|
||
|
||
GSAP does color tweens natively:
|
||
|
||
```js
|
||
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.
|
||
|
||
## Custom count-up / number tweens
|
||
|
||
When Remotion uses a frame-driven number ramp (`Math.round(value * eased)`):
|
||
|
||
```tsx
|
||
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:
|
||
|
||
```js
|
||
const counter = { v: 0 };
|
||
tl.to(
|
||
counter,
|
||
{
|
||
v: target,
|
||
duration: 1.5,
|
||
ease: "power3.out",
|
||
onUpdate: () => {
|
||
el.textContent = Math.round(counter.v).toLocaleString();
|
||
},
|
||
},
|
||
0,
|
||
);
|
||
```
|
||
|
||
`power3.out` matches `1 - (1-t)^3` exactly. Validated in T3 (mean SSIM 0.953).
|
||
Per-frame digit mismatches occur on sub-frame timing offsets but final values
|
||
converge — no SSIM impact above the noise floor.
|
||
|
||
## Stagger via per-instance prop
|
||
|
||
When custom subcomponents take a `delayInFrames` prop:
|
||
|
||
```tsx
|
||
<StatCard delayInFrames={i * 12} value={...} />
|
||
```
|
||
|
||
Translate to GSAP timeline offsets:
|
||
|
||
```js
|
||
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.
|