HyperFrames 时序移植指南:Remotion interpolate、spring 与 easing 到 GSAP 的完整映射
【免费下载链接】hyperframesWrite HTML. Render video. Built for agents.项目地址: https://gitcode.com/GitHub_Trending/hy/hyperframes
导读:本文是 remotion-to-hyperframes 技能中最高杠杆的一份参考——在把 Remotion(React)合成移植为 HyperFrames(HTML + GSAP)时,缓动与时间轴是观众最先感知的差异,时序翻错比其他任何翻译选择损失的 SSIM 都更多。本文以 timing.md 为主体骨架,逐条讲解帧与秒的换算、
interpolate线性/多段/自定义缓动、spring()弹簧、颜色与数字动画、stagger 错峰在 GSAP 时间线上的落地写法,并给出 T1–T3 分层语料实测的 SSIM 数据与源码级佐证。读完你将能够把任意 Remotion 的帧驱动动画准确翻译成可渲染、可验证的 HyperFrames 时间线。
为什么时序是移植中的最高优先级
HyperFrames 的时间线以秒为单位,而 Remotion 以帧为单位。两者之间的换算和缓动映射贯穿整个移植过程,直接影响渲染结果的像素级一致性。在 SKILL.md 定义的评估流程里,翻译结果会与 Remotion 基线渲染逐帧做 SSIM 对比:时序错误造成的画面差异,往往比其他任何翻译选择(布局、字体、媒体)都更显眼。因此本参考被标注为"single highest-leverage reference",并且已经过 T1–T3 分层语料的经验验证:
| 层级 | 合成形态 | 实测平均 SSIM | 阈值 |
|---|---|---|---|
| T1 | 单元素淡入淡出 | 0.974 | 0.95 |
| T2 | 多场景 + spring + 音频 + 图片 | 0.985 | 0.95 |
| T3 | 数据驱动、自定义子组件、数字滚动 | 0.953 | 0.90 |
完整验证基线见 SKILL.md 的 validated baseline 表;SSIM 评估的完整流程见 eval.md。
换算基础:frames → seconds
HF 的时间线单位是秒,Remotion 基于帧。永远执行:
time_seconds = frame / fps以 fps=30 为例:
- frame 15 → 0.5 s
- frame 30 → 1.0 s
- frame 90 → 3.0 s
关键实践:在翻译时一次性完成换算,而不是在运行时换算。HyperFrames 的合成是"seek 驱动"的确定性模型,动画应被烘焙成一条暂停的 GSAP 时间线,帧计数在渲染时不应出现。
这条规则在 tier-1-title-card 的 HF 产物中直接可见——翻译注释明确写着"Frame ranges → time ranges: 0/30=0, 15/30=0.5, 75/30=2.5, 90/30=3.0",所有时间偏移都在生成 HTML 时计算完毕。
interpolate 线性插值 →ease: "none"
Remotion 最常见的单段线性插值:
const opacity = interpolate(frame, [0, 30], [0, 1], { extrapolateRight: "clamp" });翻译为 GSAP:
gsap.to(target, { opacity: 1, duration: 1.0, ease: "none" }, 0); // 如果属性从 0 开始且 CSS 尚未设置初始值,使用 fromTo: gsap.fromTo(target, { opacity: 0 }, { opacity: 1, duration: 1.0, ease: "none" }, 0);要点:
ease: "none"与 Remotion 默认的线性插值完全一致;- 如果初始状态已经写在 CSS 里,用
gsap.to即可;否则必须用fromTo显式给出起始值。
关于extrapolate的边界行为:Remotion 中extrapolateLeft/extrapolateRight默认是"extend"(继续外推),但实际翻译中遇到最多的写法是"clamp"。GSAP 本身不会外推——tween 的起止值在动画前后保持恒定,天然等价于clamp。因此:
- 源码用
clamp→ GSAP 直接匹配,无需额外处理; - 源码用
extend→ 需要在发射 HTML 前手动扩展输入范围(把 tween 的起始/结束时间按外推斜率继续延伸到需要的帧),GSAP 无法隐式表达。
interpolate 多段插值 → 时间线分段 tween
Remotion 的多段插值,如淡入→保持→淡出:
const opacity = interpolate(frame, [0, 15, 75, 90], [0, 1, 1, 0]);在 GSAP 中翻译为三条时间线 tween,偏移量依次为[0]/fps、[1]/fps、[2]/fps(fps=30 时即 0 / 0.5 / 2.5 秒):
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);该模式在 T1 中已验证,平均 SSIM 0.974。实际语料 TitleCard.tsx 的源码正是interpolate(frame, [0, 15, 75, 90], [0, 1, 1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" })(淡入 0–15 帧、保持 15–75 帧、淡出 75–90 帧),其 HF 产物 index.html 中三条tl.to与上述代码逐条对应,duration恰好是相邻关键帧的帧距除以 30。
spring() 弹簧 → GSAP back.out
spring()是损失最严重的翻译。Remotion 的弹簧基于物理模型(damping/stiffness/mass),而 GSAP 的back.out/elastic.out是参数化缓动,两者并非精确等价。但经验证,近似映射足以让真实合成保持 ≥ 0.92 SSIM。
Remotionspring配置 | GSAP 等价写法 | 验证情况 |
|---|---|---|
{damping: 12, stiffness: 100, mass: 1}(干脆利落) | back.out(1.4),时长约 0.7 s | T2、T3(TitleScene) |
{damping: 14, stiffness: 90, mass: 1}(更温和) | back.out(1.2),时长约 0.7 s | T3(StatCard) |
{damping: 8, stiffness: 200}(非常有弹性) | back.out(2.0)或elastic.out(1, 0.5),时长约 0.6 s | 未验证;预算约 0.05 SSIM |
{overshootClamping: true} | power3.out,时长约 0.6 s(无过冲) | 未验证 |
经验法则:back.out(N)的过冲比例 ≈(stiffness / damping^2) * 1.4。例如damping:12, stiffness:100时1.4 * 100/144 ≈ 0.97,接近已验证的 1.4——公式只是粗略估计,最终以目视微调为准。典型配置的默认时长约 0.7 s。
当 spring 的delay/from/to参数非默认值时,按比例缩放时长(把延迟与位移折算进 tween 的偏移与目标值)。
源码佐证:T3 的 TitleScene.tsx 使用spring({ config: { damping: 12, stiffness: 100, mass: 1 } })驱动标题缩放,其 HF 产物 index.html 中对应tl.to(title, { scale: 1, duration: 0.7, ease: "back.out(1.4)" }, 0);StatCard 的{damping: 14, stiffness: 90, mass: 1}则对应back.out(1.2),两处注释都标注了映射来源。
interpolate + 自定义缓动 → GSAP 缓动等价表
Remotion 通过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")(需 CustomEase 插件) |
Easing.elastic(bounciness) | ease: "elastic.out(${bounciness}, 0.3)" |
Easing.bounce | ease: "bounce.out" |
Easing.back(overshoot) | ease: "back.out(${overshoot * 1.7})"(Remotion 的过冲比例刻度不同,需放大 1.7 倍) |
注意Easing.back的过冲刻度与 GSAP 不一致,这正是上面spring → back.out经验法则里"1.4 倍系数"的另一种体现形式。
interpolateColors 非数值属性 → GSAP 原生颜色动画
Remotion 用interpolateColors做颜色插值:
const color = interpolateColors(frame, [0, 30], ["#ff0000", "#0000ff"]);GSAP 原生支持颜色 tween:
gsap.to(target, { color: "#0000ff", duration: 1.0, ease: "none" }, 0);backgroundColor、borderColor同理。from值由 CSS 或内联样式读取,无需在 JS 中重复声明。在 SKILL.md 的 lint 分级中,interpolateColors属于 Info 级(可直接翻译并附注),不会阻断移植。
自定义数字滚动 / count-up → 计数器对象 + onUpdate
当 Remotion 使用帧驱动的数字斜坡(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 等价写法——tween 一个计数器对象,在onUpdate中写回textContent:
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与1 - (1-t)^3精确相等,无需近似;- 在 T3 中验证,平均 SSIM 0.953;
- 子帧时序偏移可能导致个别帧的数字显示略有出入,但终值必然收敛,对 SSIM 的影响低于噪声底,可忽略。
源码佐证:T3 的 AnimatedNumber.tsx 正是"45 帧内1 - (1-t)^3缓出 +toLocaleString"的实现,HF 产物 index.html 中三张 StatCard 各自对应一个counter对象 +tl.to(..., { duration: 1.5, ease: "power3.out", onUpdate }),起始偏移为场景起始时间加各自的 stagger。
Stagger 错峰 → 按实例 prop 换算时间线偏移
当自定义子组件接收delayInFramesprop 时:
<StatCard delayInFrames={i * 12} value={...} />翻译为 GSAP 时间线偏移:
cards.forEach((card, i) => { const start = base + i * (12 / fps); // i * 0.4s at fps=30 tl.to(card, { ... }, start); });在 T3 中验证:三张 StatCard 以 0.0 / 0.4 / 0.8 s 错峰入场(12 帧 × i / 30fps)。具体落地可对照 StatCard.tsx 的local = frame - delayInFrames局部帧逻辑——HF 端将这一"局部时间"折叠进全局时间线的偏移计算,i * 12帧直接换算为i * 0.4秒。
组合实践:从源码到 HF 时间线的完整链路
综合以上映射,一个典型的移植遵循 SKILL.md 的工作流:
- Lint 源:对 Remotion 源码跑 lint_source.py,拦截
useState/useEffect等阻塞项(详见 escape-hatch.md),识别interpolate/spring/Easing/interpolateColors等需要本参考的构造; - 规划映射:根据 api-map.md 确定需要加载的专题参考——出现
useCurrentFrame、interpolate、spring、Easing、interpolateColors时加载本文对应的 timing.md; - 生成 HF 合成:输出
index.html,包含根#stage的data-composition-id/data-start/data-duration/data-fps等属性、扁平场景列表,以及一条暂停的 GSAP 时间线,所有useCurrentFrame()派生都变成时间线上正确偏移的 tween(参数化的 props 处理见 parameters.md); - 验证:渲染两侧基线并做 SSIM 对比,阈值约为该复杂度层级 p05 之下 0.02(详见 eval.md)。
验证命令(参考 SKILL.md 的快速路径):
# 渲染 Remotion 基线 cd remotion-src && npx remotion render <CompositionId> out/baseline.mp4 # 渲染 HF 翻译 cd ../hf-src && npx hyperframes render --skill=remotion-to-hyperframes --output ../hf.mp4 # SSIM 对比 ../../scripts/render_diff.sh ./remotion-src/out/baseline.mp4 ./hf.mp4 ./diff关键前提:两侧渲染必须使用匹配的像素格式,在 Remotion 源码的remotion.config.ts中设置Config.setVideoImageFormat("png")与Config.setColorSpace("bt709"),否则 SSIM 差异度量的是编码器差异(约 0.05 SSIM 损失)而非翻译保真度。若对比失败,可用 frame_strip.sh 定位具体分歧帧,再回到本文对应的映射表复查。
小结:一份可执行的时序翻译清单
把 Remotion 的帧驱动动画翻译成 HyperFrames 时间线时,遵循以下核对项即可覆盖绝大多数场景:
- 单位统一:所有帧号除以
fps换成秒,且只在翻译时换算一次; - 线性插值:默认
ease: "none",注意extrapolateRight: "clamp"与 GSAP 天然一致,"extend"需手动外推输入范围; - 多段插值:拆成多条时间线 tween,偏移取各关键帧对应秒数;
- 弹簧:按 spring → back.out 映射表 选取
back.out参数与时长,未验证组合预留约 0.05 SSIM 的偏差预算,最终目视微调; - 自定义缓动:对照 缓动等价表,
back过冲乘 1.7,bezier需 CustomEase 插件; - 颜色/数字:颜色用 GSAP 原生 tween;数字滚动用计数器对象 +
onUpdate写textContent,power3.out精确对应 cubic ease-out; - stagger:
delayInFrames除以fps变为时间线偏移,叠加在场景基础时间之上。
所有映射均已通过 T1–T3 测试语料 的 SSIM 阈值验证,可直接作为移植 Remotion 合成到 HyperFrames 的实战参考。
【免费下载链接】hyperframesWrite HTML. Render video. Built for agents.项目地址: https://gitcode.com/GitHub_Trending/hy/hyperframes
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考