Remotion Compose 是做什么的?
你将一个自然语言需求转化为完整的、可预览的 Remotion 合成——真实的 React/TypeScript 帧驱动动画。输出是一个 .tsx 文件,位于 src/compositions/ 中,并注册到 src/Root.tsx,用户可以在 Studio 中立即拖动预览,或者交给 remotion-render 渲染。
当收到任何创建、生成、动画或“为……编写视频代码”的请求时触发,例如产品演示、引导、功能展示或营销动画——包括“制作一个展示 X 的 20 秒视频”。
第一步 - 提取需求
在开始编码前,必须先获取以下信息。尽可能一次性询问,对于明显缺失的部分使用合理的默认值,不要卡住:
- 时长(秒)和 fps(默认 30——产品和营销视频的标准,渲染成本是 60fps 的一半。仅在动画精细光标运动或滚动 UI 且 30fps 明显卡顿时才使用 60fps)。
- 场景——按顺序发生了什么。优先获取节拍列表:“logo,然后三个功能,然后一个 CTA”优于“我的应用程序的演示”。
- 品牌——主色/强调色/背景色十六进制颜色,以及字体(一个
@remotion/google-fonts字体家族名称)。 - 宽高比/平台——16:9 横屏(1920×1080)、9:16 竖屏(1080×1920)用于社交媒体,或 1:1 正方形(1080×1080)。
- 文案——实际的标题/功能名称/CTA 文字。永远不要发明产品声明;询问真正的文字。
将所有内容转换为帧:durationInFrames = 秒 * fps。典型总长度:社交媒体/产品演示 15-60 秒,广告片段 6-15 秒。
第二步 - 构建场景计划
每个视频一个 <Composition>,每个场景一个组件,在时间轴上用 <Sequence> 拼接。在编写代码之前先规划好帧预算:
总计:20 秒 @ 30fps = 600 帧
Intro (logo) 帧 0-90 (Sequence from=0 durationInFrames=90)
功能 1 帧 90-240 (Sequence from=90 durationInFrames=150)
功能 2 帧 240-390 (Sequence from=240 durationInFrames=150)
功能 3 帧 390-540 (Sequence from=390 durationInFrames=150)
CTA outro 帧 540-600 (Sequence from=540 durationInFrames=60)
预算时的节奏阈值:
- 任何包含文本的节拍至少需要 2 秒(30fps 下 60 帧)——入场动画加上停留。一个标题每个词大约需要 150-200ms 的停留时间才能被阅读;一个 6 个词的标题在动画落地后大约需要 1 秒停留。
- 入场动画:10-20 帧。淡入淡出和退出:12-15 帧。更长会显得迟缓,更短则像故障。
- 除非是有意的闪切,否则没有场景短于约 45 帧。
<Sequence from={F}> 将子元素的本地时钟偏移,因此在组件内部 useCurrentFrame() 从 0 开始。将每个场景当作从帧 0 开始进行动画。
第三步 - 核心 API 参考
useCurrentFrame()——当前帧(时钟)。所有动画都是它的函数。useVideoConfig()——{ fps, width, height, durationInFrames }。interpolate(frame, [inFrames], [outValues], opts)——线性映射。在入场/退出时始终传递{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },这样值不会超出其范围。spring({ frame, fps, config })——基于物理的 0→1(轻微过冲)。用于任何需要显得有生命的东西(弹入、滑动、缩放进入)。AbsoluteFill——一个全屏绝对定位的 div;每个场景的基础层。Sequence——时间偏移和时间边界子元素(场景调度器)。Series/Series.Sequence——无需手动计算from偏移量的背靠背场景序列。Img,staticFile——见第六步。
第四步 - spring() 与 interpolate() 对比
使用 spring() 的场景…… | 使用 interpolate() 的场景…… |
|---|---|
| 运动需要有物理感觉——弹跳、缩放进入、滑动进入 | 需要在精确帧之间进行精确的线性控制 |
| 希望自然的稳定/过冲效果 | 淡入淡出(不透明度 0→1)、颜色变化、进度条、任何按帧计时的东西 |
| 卡片、logo、徽章等元素的入场 | 淡入淡出和退出,过冲会导致视觉错误 |
模式——用 spring 驱动变换,用 interpolate 驱动不透明度:
const { fps } = useVideoConfig();
const frame = useCurrentFrame();
const pop = spring({ frame, fps, config: { damping: 200 } }); // 0 → 1,生动
const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });
const scale = interpolate(pop, [0, 1], [0.8, 1]);
damping: 200 是主力配置——快速稳定,几乎看不到过冲。较低的阻尼(约 10-20)会明显反弹;只有在反弹是目的时才使用它。
第五步 - 标准产品演示结构
除非另有说明,否则使用此结构:logo/品牌介绍 → N 个功能展示 → CTA 结尾。使其数据驱动(第六步),这样变体就免费了。
// src/compositions/ProductDemo.tsx
import {
AbsoluteFill, Sequence, useCurrentFrame, useVideoConfig,
interpolate, spring,
} from "remotion";
import { loadFont } from "@remotion/google-fonts/Inter";
const { fontFamily } = loadFont();
export type Feature = { title: string; body: string };
export type ProductDemoProps = {
brand: string;
colors: { bg: string; fg: string; accent: string };
logoText: string;
features: Feature[];
cta: string;
};
const Intro: React.FC<{ text: string; colors: ProductDemoProps["colors"] }> = ({ text, colors }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const s = spring({ frame, fps, config: { damping: 200 } });
const scale = interpolate(s, [0, 1], [0.7, 1]);
const opacity = interpolate(frame, [0, 12], [0, 1], { extrapolateRight: "clamp" });
return (
<AbsoluteFill style={{ backgroundColor: colors.bg, justifyContent: "center", alignItems: "center" }}>
<h1 style={{ fontFamily, fontSize: 120, color: colors.fg, transform: `scale(${scale})`, opacity }}>
{text}
</h1>
</AbsoluteFill>
);
};
const FeatureCallout: React.FC<{ feature: Feature; colors: ProductDemoProps["colors"] }> = ({ feature, colors }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const slide = spring({ frame, fps, config: { damping: 200 } });
const x = interpolate(slide, [0, 1], [-60, 0]);
const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });
return (
<AbsoluteFill style={{ backgroundColor: colors.bg, justifyContent: "center", paddingLeft: 160 }}>
<div style={{ transform: `translateX(${x}px)`, opacity }}>
<div style={{ fontFamily, fontSize: 32, color: colors.accent, fontWeight: 700, letterSpacing: 2 }}>
{feature.title.toUpperCase()}
</div>
<div style={{ fontFamily, fontSize: 64, color: colors.fg, maxWidth: 1100, marginTop: 16 }}>
{feature.body}
</div>
</div>
</AbsoluteFill>
);
};
const Outro: React.FC<{ cta: string; colors: ProductDemoProps["colors"] }> = ({ cta, colors }) => {
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 18], [0, 1], { extrapolateRight: "clamp" });
return (
<AbsoluteFill style={{ backgroundColor: colors.accent, justifyContent: "center", alignItems: "center" }}>
<h2 style={{ fontFamily, fontSize: 84, color: colors.bg, opacity }}>{cta}</h2>
</AbsoluteFill>
);
};
export const ProductDemo: React.FC<ProductDemoProps> = ({ colors, logoText, features, cta }) => {
const intro = 90;
const per = 150;
const outro = 60;
return (
<AbsoluteFill>
<Sequence durationInFrames={intro}>
<Intro text={logoText} colors={colors} />
</Sequence>
{features.map((f, i) => (
<Sequence key={i} from={intro + i * per} durationInFrames={per}>
<FeatureCallout feature={f} colors={colors} />
</Sequence>
))}
<Sequence from={intro + features.length * per} durationInFrames={outro}>
<Outro cta={cta} colors={colors} />
</Sequence>
</AbsoluteFill>
);
};
在 src/Root.tsx 中注册,基于相同的数字计算 durationInFrames 并提供 defaultProps:
<Composition
id="ProductDemo"
component={ProductDemo}
durationInFrames={90 + 3 * 150 + 60}
fps={30}
width={1920}
height={1080}
defaultProps={{
brand: "Acme",
colors: { bg: "#0B0B0F", fg: "#FFFFFF", accent: "#6C5CE7" },
logoText: "Acme",
features: [
{ title: "Fast", body: "Ship in seconds, not sprints." },
{ title: "Simple", body: "One prompt, one video." },
{ title: "Yours", body: "Your brand, your copy." },
],
cta: "Start free →",
}}
/>
对于竖屏/社交,设置 width={1080} height={1920};对于正方形,1080×1080。保持组件相同——只有画布变化。
第六步 - 数据驱动的属性与静态资源
上述合成是完全数据驱动的:每个词和颜色都是属性。这就是关键——变体(新文案、新调色板、本地化文本)无需重新提示;只需传入不同的属性(remotion-render 支持从 JSON 文件批量处理)。
静态资源(logo、截图、音频)位于 public/assets/ 中,通过 staticFile() 引用——绝不使用 import 或原始路径:
import { Img, staticFile } from "remotion";
<Img src={staticFile("assets/logo.png")} />
第七步 - 输出
将完成的文件保存到 src/compositions/<VideoName>.tsx,在 src/Root.tsx 中注册,并告诉用户使用 npx remotion studio(http://localhost:3000)预览。然后交接:*“在 Studio 中拖动预览。告诉我需要修改什么——太快了、颜色不对、文案不同——我会编辑并重新渲染(remotion-render)。”*
禁止事项
- 禁止使用 CSS 过渡/动画或
setTimeout/setInterval进行动画。Remotion 会独立并行渲染每一帧;任何不基于useCurrentFrame()的内容在最终渲染中都是不可见或不一致的,尽管在浏览器预览中可能看起来正常。 - 禁止使用
Math.random()进行散射/粒子效果——每个渲染工作线程会得到不同的值,帧不再匹配。使用 Remotion 的random(seed)实现确定性的伪随机性。 - 禁止在没有
delayRender()/continueRender()的情况下获取数据或等待异步工作——渲染器会在数据到达之前捕获帧。 - 禁止在场景组件内硬编码 fps、width 或 height——从
useVideoConfig()读取它们,这样切换 30↔60fps 或横屏↔竖屏不会破坏每个时间和布局。 - 禁止在入场/退出插值中省略
extrapolateLeft/Right: "clamp"——不透明度会超过 1,元素在其范围结束后会漂移到画布之外。 - 禁止发明标题、功能声明或 CTA——使用用户的真实文案,如果缺失则询问。
质量标准
- 每个动画值都是
useCurrentFrame()(加上属性)的纯函数——在 Studio 中向后拖拽看起来与向前播放相同。 - 计划中的帧预算总和正好等于
Root.tsx中的durationInFrames,并且没有文本节拍停留时间低于其可读性最小值。 - 所有文案和颜色都通过类型化的属性传入,并附带
defaultProps——场景组件中没有硬编码的字面量。 - 资源通过
staticFile()加载;文件正确注册并在 Studio 中预览,没有控制台错误。