比特之外

Remotion 教程 06:Motion 的安全使用边界

前面几篇一直强调:Remotion 的主动画应该由 frame 推导出来。那问题来了:很多前端熟悉 Motion,也就是以前常说的 Framer Motion。它能不能放进 Remotion 视频里?

答案不是“完全不能用”,而是要分清边界:

Remotion 控制时间
Motion 负责表现

为什么不能让 Motion 接管时间

Motion 是 Web UI 动画库。它很擅长处理按钮、弹窗、hover、拖拽、布局变化和页面转场。

这些能力在网页里很好用,因为网页动画通常依赖浏览器运行时:

  • 浏览器真实时间经过了多久。
  • 鼠标是否悬停。
  • 用户是否点击或拖拽。
  • DOM 布局测量结果有没有变化。
  • 元素是否进入视口。

但 Remotion 渲染视频时关心的是另一件事:

第 0 帧应该是什么画面?
第 1 帧应该是什么画面?
第 2 帧应该是什么画面?

所以在 Remotion Composition 里,主动画必须能回答“任意一帧是什么状态”。如果动画过程藏在 Motion 的运行时里,视频渲染就很难保证每一帧都可复现。

安全写法:frame 算值,Motion 接收 style

安全写法不是让 Motion 自己跑动画,而是让 Remotion 先算好这一帧的样式值:

const frame = useCurrentFrame();
const { fps } = useVideoConfig();
 
const progress = spring({
  frame: frame - 24,
  fps,
  config: { damping: 100 },
});
 
const opacity = interpolate(frame, [24, 44], [0, 1], {
  extrapolateLeft: "clamp",
  extrapolateRight: "clamp",
});
 
const rotation = interpolate(progress, [0, 1], [-8, 0]);
const x = interpolate(progress, [0, 1], [-80, 0]);

这里有四个关键值:

  • frame:当前正在渲染的帧号。
  • progress:由 Remotion 的 spring() 算出的动画进度。
  • opacity:由 frame 映射出来的透明度。
  • rotationx:由 progress 映射出来的旋转角度和位移。

然后把这些值交给 motion.div

<motion.div
  style={{
    opacity,
    scale: progress,
    rotate: `${rotation}deg`,
    x,
  }}
>
  安全用法
</motion.div>

注意这里没有写 animate,也没有写 transition={{ duration: 1 }}。Motion 只是一个能接收 transform、scale、rotate 等样式的组件。真正决定动画进度的仍然是 Remotion。

不推荐写法:Motion 自己跑动画

在网页里,你可能会这样写:

<motion.div
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
  transition={{ duration: 1 }}
/>

这段代码适合网页交互,但不适合作为 Remotion 视频里的主动画。原因是 duration: 1 表达的是运行时动画过程,而不是明确的帧状态。

对视频来说,更好的写法是:

const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 30], [0, 1], {
  extrapolateLeft: "clamp",
  extrapolateRight: "clamp",
});
 
return <motion.div style={{ opacity }} />;

这样第 0 帧、第 15 帧、第 30 帧分别是什么透明度,都能直接算出来。

完整最小示例

下面是一个安全使用 Motion 的 Remotion 组件:

import React from "react";
import { motion } from "motion/react";
import {
  AbsoluteFill,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from "remotion";
 
export const SafeMotionDemo: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
 
  const progress = spring({
    frame: frame - 24,
    fps,
    config: { damping: 100 },
  });
 
  const opacity = interpolate(frame, [24, 44], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
 
  const rotation = interpolate(progress, [0, 1], [-8, 0]);
  const x = interpolate(progress, [0, 1], [-80, 0]);
 
  return (
    <AbsoluteFill
      style={{
        backgroundColor: "#f8fafc",
        padding: 96,
        fontFamily: "Inter, sans-serif",
      }}
    >
      <h1 style={{ margin: 0, fontSize: 72 }}>
        Remotion 控制时间,Motion 呈现样式
      </h1>
 
      <motion.div
        style={{
          marginTop: 120,
          width: 420,
          padding: 40,
          borderRadius: 8,
          backgroundColor: "#16a34a",
          color: "white",
          opacity,
          scale: progress,
          rotate: `${rotation}deg`,
          x,
        }}
      >
        <h2 style={{ margin: 0, fontSize: 42 }}>安全用法</h2>
        <p style={{ fontSize: 28, lineHeight: 1.4 }}>
          Motion 不计算时间,只接收这一帧的样式值。
        </p>
      </motion.div>
    </AbsoluteFill>
  );
};

这个示例里,Motion 只是表现层。时间仍然来自:

frame -> spring / interpolate -> style

哪些 Motion 能力要避开

放在 Remotion Composition 里时,下面这些能力不适合作为视频主动画:

  • animate
  • transition.duration
  • layout / layoutId
  • whileHover / whileTap
  • drag
  • whileInView
  • scroll 驱动动画

它们的问题不是“库不好”,而是这些能力的设计目标是网页交互,不是逐帧视频渲染。

Motion 适合放在哪里

Motion 很适合放在视频生成产品的网页界面里,例如:

  • 模板选择面板。
  • 参数表单。
  • 弹窗和抽屉。
  • 拖拽排序。
  • 预览页里的交互反馈。

如果放进 Remotion Composition,建议只把它当作表现层。

判断标准

你可以用一个问题判断写法是否安全:

我能不能只给定 frame,就算出这一帧的最终样式?

如果答案是能,通常适合 Remotion。

如果答案是不能,需要等待浏览器播放、等待用户交互、等待布局动画完成,那它就不应该成为视频主动画。

示例项目位置

这篇文章对应示例项目里的:

Composition: LessonMotionBoundary
文件: src/lessons/MotionBoundary.tsx

运行示例项目后,可以暂停在第 20 帧、第 40 帧、第 70 帧,观察 opacityscalerotatex 是否都能由当前帧固定下来。

下一篇进入更广的生态选择:Lottie、Rive、Three、Skia、D3 分别适合什么类型的视频。