Remotion 教程 03:Composition、Sequence、spring 搭一条可控的视频时间轴
前两篇解决了两个基础问题:
Composition是视频入口。- 主动画应该由
frame推导出来。
这一篇继续往工程结构走。一个视频不应该越写越像一个巨大的 React 组件。你需要把“视频入口”“时间段”“局部动画”拆清楚。
可以先记住这条分工:
Composition 决定视频入口
Sequence 决定内容什么时候出现
spring 决定出现过程怎么运动
interpolate 把进度转成具体样式为什么要组织时间轴
刚开始学 Remotion 时,把所有内容写在一个组件里很正常。但视频稍微复杂一点,就会出现几个问题:
- 标题、字幕、素材、图表混在一起
- 每段内容从第几帧开始不清楚
- 动画延迟散落在很多数字里
- 想复用某段动画时,需要复制一大段代码
所以 Remotion 项目通常会形成三个层次:
Composition:视频入口和参数
Scene:一段画面或一个章节
Component:标题、字幕、卡片、图表等局部组件这不是为了抽象而抽象,而是为了让时间轴可读、可改、可复用。
Composition:注册视频入口
Composition 不负责画面细节。它负责告诉 Remotion:这个视频入口叫什么、由哪个组件渲染、尺寸和帧率是多少。
<Composition
id="TimelineDemo"
component={TimelineDemo}
durationInFrames={180}
fps={30}
width={1920}
height={1080}
/>这段配置表示:
- 视频入口叫
TimelineDemo - 画面由
TimelineDemo组件渲染 - 总长度是 180 帧
- 帧率是 30fps
- 输出尺寸是 1920x1080
180 帧除以 30fps,就是 6 秒。
Sequence:安排内容出现时间
视频不是所有内容同时出现。标题可能先出现,正文晚一点出现,素材再接着切入。
Sequence 用来安排一段内容在时间轴上的位置:
<Sequence from={60}>
<Title />
</Sequence>from={60} 表示这段内容从第 60 帧开始。
如果当前视频是 30fps,第 60 帧就是第 2 秒:
60 / 30 = 2 秒注意:from={2} 不是 2 秒,而是第 2 帧。想表达 2 秒,应该写成:
<Sequence from={2 * fps}>Sequence 的局部时间
Sequence 还有一个很重要的特点:它会让子组件更容易复用。
假设你写:
<Sequence from={60}>
<Title />
</Sequence>当全局时间走到第 60 帧时,Title 组件内部看到的 useCurrentFrame() 会从自己的局部时间开始计算。
这意味着 Title 不需要知道自己被放在全局第几秒。它只需要关心“我出现后的第 0 帧、第 10 帧、第 20 帧应该怎么动”。
spring:控制进入过程
Sequence 决定内容从什么时候开始,spring() 决定它出现时怎么运动。
const progress = spring({
frame,
fps,
config: { damping: 90 },
});这里的 frame 可以是子组件内部的局部帧号。spring() 会根据帧号生成动画进度。
然后你可以把进度映射成透明度、位移、缩放等样式:
const y = interpolate(progress, [0, 1], [40, 0]);
const opacity = interpolate(progress, [0, 1], [0, 1]);完整最小示例
下面是一个 6 秒视频:标题先出现,卡片在第 2 秒出现,底部说明在第 4 秒出现。
import React from "react";
import {
AbsoluteFill,
Composition,
Sequence,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
const Title: React.FC<{ text: string }> = ({ text }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({
frame,
fps,
config: { damping: 90 },
});
const y = interpolate(progress, [0, 1], [40, 0]);
const opacity = interpolate(progress, [0, 1], [0, 1]);
return (
<h1
style={{
margin: 0,
fontSize: 76,
opacity,
transform: `translateY(${y}px)`,
}}
>
{text}
</h1>
);
};
const Card: React.FC<{ label: string }> = ({ label }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({ frame, fps, config: { damping: 80 } });
const scale = interpolate(progress, [0, 1], [0.88, 1]);
return (
<div
style={{
marginTop: 96,
width: 520,
padding: 40,
borderRadius: 8,
backgroundColor: "#2563eb",
color: "white",
fontSize: 42,
fontWeight: 800,
transform: `scale(${scale})`,
}}
>
{label}
</div>
);
};
export const TimelineDemo: React.FC = () => {
const { fps } = useVideoConfig();
return (
<AbsoluteFill
style={{
backgroundColor: "#f8fafc",
padding: 96,
fontFamily: "Inter, sans-serif",
}}
>
<Sequence>
<Title text="搭一条可控的视频时间轴" />
</Sequence>
<Sequence from={2 * fps}>
<Card label="第 2 秒出现的内容" />
</Sequence>
<Sequence from={4 * fps}>
<p style={{ marginTop: 80, fontSize: 34 }}>
Sequence 负责位置,spring 负责出现过程。
</p>
</Sequence>
</AbsoluteFill>
);
};
export const RemotionRoot = () => {
return (
<Composition
id="TimelineDemo"
component={TimelineDemo}
durationInFrames={180}
fps={30}
width={1920}
height={1080}
/>
);
};这个例子里有三层结构:
RemotionRoot注册视频入口TimelineDemo编排整条时间轴Title和Card负责局部画面
新手容易踩的坑
把秒和帧混在一起
from={2} 是第 2 帧,不是第 2 秒。30fps 下,第 2 秒应该写:
<Sequence from={2 * fps}>把所有动画都写在一个组件里
短 demo 可以这样写。真实模板里,标题、字幕、素材、图表应该拆成组件,否则很快会难以维护。
忘记 clamp
如果 interpolate() 用在不希望继续外推的地方,建议写清楚:
interpolate(frame, [0, 30], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});示例项目位置
这篇文章对应示例项目里的时间轴基础内容:
Composition: LessonCoreBasics
Composition: LessonAnimationMethod
文件: src/lessons/CoreBasics.tsx
文件: src/lessons/AnimationMethod.tsx运行示例项目后,可以尝试:
- 把动画开始时间从
frame - 30改成frame - 60。 - 把位移距离从 900 改成 500。
- 新增一个
Sequence,让说明文字晚 30 帧出现。
做完这三个练习,你会更清楚:视频时间轴不是自动播放出来的,而是被你一帧一帧安排出来的。
下一篇开始进入真实视频素材:图片、视频、音频、转场和运动模糊。