RSS

第 4 章 / 共 9 章

看懂 Codex 产出的四个核心概念

约 9 分钟 更新于

第3章你让 Codex 写出了能跑的标题卡,但大概率你只是「碰巧能跑」:它写的 Composition、Sequence、useCurrentFrame 对你还是黑话。看不懂没关系——但看不懂,你就没法判断它写得对不对,只能回到「感觉不对」的猜谜循环。这一章给你一套读 Remotion 代码的心智模型,一共四个概念,配一条主线:视频里的时间,是以帧为单位的;画面里的每个变化,都是帧号的函数。读完这章,Codex 写给你的代码会从「天书」变成「可审阅的草稿」。

4.1 时间的单位是帧:30 帧每秒怎么想

先建立一个基础换算。视频不是连续的,它是一张张静止画面快速播放。Remotion 用两个数字描述这种播放:

  • 帧率(fps):每秒播放多少帧。示例视频是 30fps;
  • 总帧数(durationInFrames):整个视频一共有多少帧。

换算关系只有一个:秒数 × 帧率 = 总帧数。示例视频 18 秒 × 30fps = 540 帧。反过来,如果你想让某段动画持续 2 秒,它在代码里的时长就是 60 帧。

还有一个细节必须记住:帧号从 0 开始数。第 1 帧的编号是 0,第 540 帧的编号是 539。这符合程序员的计数习惯,但和人类的「第几秒」直觉不同——所以凡是涉及时间的地方,你都要在心里做一次换算。比如有人说「3 秒处」,你应立刻想到「第 90 帧左右」。

4.2 Composition:告诉渲染器画多大、播多久

「Composition」是 Remotion 里「一个视频条目」的意思。一个项目里可以注册很多个 Composition(比如横版一条、竖版一条),Studio 左侧列表里能切换预览。每个 Composition 用几个数字把视频的「边界条件」说清楚:

// 这段代码通常在 src/Root.tsx 里,由 src/index.ts 注册的根组件返回
import {Composition} from 'remotion';
import {LaunchVideo} from './LaunchVideo';

export const RemotionRoot = () => {
  return (
    <Composition
      id="LaunchVideo"
      component={LaunchVideo}
      durationInFrames={540}
      fps={30}
      width={1920}
      height={1080}
    />
  );
};

逐个字段看:id 是这个视频的名字,渲染时要用它指定「渲染哪一条」;component 是「每一帧要渲染的组件」;width/height 是画幅像素;fps 和 durationInFrames 决定时间。把这六个数字读出来,你就知道这条视频「多大、多长、由哪个组件负责画面」——这是读任何 Remotion 项目的第一件事。

文件入口 src/index.ts 只有一件事:用 registerRoot() 告诉 Remotion「根组件是哪个」。它几乎不需要你改,Codex 也很少动它;你在第3章的任务卡里要求「只读不改 index.ts」,就是因为它是全项目的入口,乱动会牵一发动全身。

4.3 AbsoluteFill:没有排版流的画布

网页有「文档流」:文字从上往下排,元素会互相推挤,页面可以滚动。视频没有这些——一个 1920×1080 的画面就是一块固定画布,不滚动、不推挤,所有元素的位置都由你(或 Codex)显式摆放。

Remotion 为此提供了 <AbsoluteFill> 组件:一个占满整帧的容器,相当于「position: absolute,铺满屏幕」的一个图层。视频代码最常见的外层结构就是层层叠放的 AbsoluteFill:一个放背景,一个放主体,一个放字幕,后写的叠在先写的上面。

import {AbsoluteFill} from 'remotion';

export const TitleCard = () => {
  return (
    <AbsoluteFill style={{backgroundColor: '#0e2231'}}>
      {/* 居中排版放在占满整帧的容器里做 */}
      <AbsoluteFill style={{justifyContent: 'center', alignItems: 'center'}}>
        <h1 style={{color: '#fff', fontSize: 120}}>山雾乌龙</h1>
      </AbsoluteFill>
    </AbsoluteFill>
  );
};

看到这类代码时,你的读法应该是:先找 AbsoluteFill 的嵌套层级,理解「谁叠在谁上面、谁负责居中」,再去看里面的具体内容。视频代码里没有「文档流帮你排好版」这回事,所以一切排版都靠 flex、绝对定位和像素值显式完成——这也是视频代码和普通网页代码最大的阅读差异。顺带一提:正因为画布固定,任何超出 1920×1080 的内容都会被裁掉或不可见,文字「跑出屏幕」是新手最常见的视觉事故,第5章我们会专门处理。

4.4 Sequence:把时间切成一个个场景

一个 18 秒的视频不会只有一个画面。开场 3 秒是标题,中间是卖点,结尾是引导——每个时间段像一个「小视频」叠在时间轴上。Remotion 里切时间段的组件叫 <Sequence>(序列),用两个参数表达:

  • from:这段内容从第几帧开始;
  • durationInFrames:这段内容持续几帧。
import {Sequence} from 'remotion';

export const LaunchVideo = () => {
  return (
    <>
      {/* 0–89 帧:开场标题 */}
      <Sequence from={0} durationInFrames={90}>
        <TitleCard />
      </Sequence>
      {/* 90–449 帧:三条卖点,每条 120 帧 */}
      <Sequence from={90} durationInFrames={360}>
        <Features />
      </Sequence>
      {/* 450–539 帧:结尾引导 */}
      <Sequence from={450} durationInFrames={90}>
        <EndCard />
      </Sequence>
    </>
  );
};

Sequence 最有价值的设计是时间隔离:放进 Sequence 的组件,不需要知道自己在整条视频的第几帧,它只关心自己这一段内部的时间。这一点和 useCurrentFrame 一起构成了 Remotion 最重要的编码习惯,见下一节。

4.5 动画是帧号的函数:useCurrentFrame 与 interpolate

现在把最后一块拼图放上:画面怎么「动」起来?答案是——每个动画值,都是由当前帧号算出来的。Remotion 提供 useCurrentFrame() 钩子,返回「现在渲染到第几帧」:

  • 在普通组件里,它返回整条视频的当前帧(从 0 开始);
  • 在 Sequence(或任何带 from 的定时容器)内部,它返回相对这段起始点的帧。也就是说,一个放进 Sequence 的卡片,永远从自己的第 0 帧开始动画,不管它在整条视频的第几秒出场。这正是上节说的「时间隔离」。

拿到帧号之后,还需要把「帧号」翻译成「某个属性值」。最常用的工具是 interpolate()——它做线性插值:给定输入帧区间和输出值区间,把当前帧映射过去:

import {AbsoluteFill, interpolate, useCurrentFrame} from 'remotion';

export const FadeInTitle = () => {
  const frame = useCurrentFrame();

  // 第 0–30 帧:透明度从 0 到 1
  const opacity = interpolate(frame, [0, 30], [0, 1], {
    extrapolateRight: 'clamp',
  });

  // 第 0–45 帧:向下位移从 80px 归零(像从下方浮上来)
  const translateY = interpolate(frame, [0, 45], [80, 0], {
    extrapolateRight: 'clamp',
  });

  return (
    <AbsoluteFill style={{justifyContent: 'center', alignItems: 'center'}}>
      <h1
        style={{
          color: '#fff',
          fontSize: 120,
          opacity,
          transform: `translateY(${translateY}px)`,
        }}
      >
        山雾乌龙
      </h1>
    </AbsoluteFill>
  );
};

读这类代码的套路是「找那个随时间变化的值」:先找 useCurrentFrame() 赋给了谁,再顺着看这个帧号喂给了哪个 interpolate 或 spring,最后看输出值用在了哪个样式属性上(opacity、translateY、rotate……)。整段代码里,真正「活」的只有这几处;其余都是静态排版。

Codex 写动画时还常用 spring()——它模拟弹簧的物理回弹,让入场动画有「弹一下」的自然感。它需要一个参数 fps(通常用 useVideoConfig() 取,或直接用插值)。你现阶段不需要会写 spring,只需要知道:凡是「入场的弹跳感」,多半是 spring;凡是「匀速的渐变」,多半是 interpolate。细节让 Codex 处理,你负责验收「弹得是不是你要的感觉」。

第4章:Remotion 核心概念地图
图 4.1:读任何 Remotion 代码前,先在脑中展开这张概念地图:时间是帧号(Composition 用 fps 与 durationInFrames 定义),Sequence 把时间切成片段,画面元素放进 AbsoluteFill 画布,useCurrentFrame 取帧号、interpolate/spring 把帧号变成动画值。注意图的中心:四个概念都指向同一个核心——「帧号决定一切」。

4.6 行动:用四问读懂 Codex 写的一段代码

  1. 边界:这段代码属于哪个 Composition?它的画幅、fps、总帧数是多少(去 src/Root.tsx 找)?
  2. 切分:时间被 Sequence 切成了几段?每段从第几帧到第几帧,换算成秒各是多少?
  3. 摆放:画面元素放在哪几层容器里(找 AbsoluteFill)?「叠放顺序」和「居中方式」分别是什么?
  4. 变化:代码里有几个随时间变化的值?它们各用了什么函数,最终作用在哪个样式属性上?

如果你能答出这四问,哪怕不知道每个 API 的每个参数,你也已经能审阅 Codex 的代码了——能判断它改的是不是无关文件、动画是不是真的由帧驱动、时间切分是否符合你的分镜。

概念地基打完了。下一章把这些概念用起来:让 Codex 把第3章的静态标题卡,升级成一条有入场、有停留、有转场、有节奏的三场景小片——你会第一次真正「导演」一段动画。