Files
James 890f305cd1 feat(skills): remotion-to-hyperframes references (6/7)
Adds 11 progressively-disclosed reference files that the skill loads on
demand during translation. Total ~1500 LOC, every file under 200 lines
(skill-creator's progressive-disclosure budget).

  api-map.md         the comprehensive Remotion -> HF translation table
                     (the index; loaded at start of translation)
  timing.md          interpolate, spring (validated configs), easing,
                     count-up, stagger
  sequencing.md      Sequence, Series, Loop, Freeze, AbsoluteFill,
                     Composition root
  media.md           Audio, Video, Img, IFrame, OffthreadVideo,
                     staticFile, asset paths
  transitions.md     @remotion/transitions presentations -> manual GSAP
                     crossfades or HF shader-transitions
  lottie.md          @remotion/lottie -> HF lottie adapter (incl. AE
                     feature limitations note)
  fonts.md           Google Fonts loading, local @font-face, system
                     fallback noise floor
  parameters.md      Zod schemas, defaultProps, sync vs async
                     calculateMetadata
  escape-hatch.md    when to bow out + the runtime interop pattern
                     from PR #214
  limitations.md     known caveat patterns (volume ramps, Loop with
                     state, custom presentations, code-split components)
  eval.md            how to run the validation harness, threshold rule
                     of thumb, what the noise floor looks like

The references are evidence-driven rather than speculative: every spring
config, easing curve, and SSIM threshold is documented from the
validated T1/T2/T3 calibration runs (mean 0.974 / 0.985 / 0.953). The
escape-hatch boundaries match the lint blockers in PR 2 and the T4
fixtures in PR 5.

Replaces the placeholder .gitkeep from PR 1.
2026-04-27 23:55:51 +00:00

6.1 KiB
Raw Permalink Blame History

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 T1T3.

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

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.

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.

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

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

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.

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: "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:

<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.