> ## Content Index
> Fetch the complete content index at: https://yinguobing.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# 用代码写视频：Remotion初体验
- URL: https://yinguobing.com/blog/remotion-first-look/
- Published: 2026-06-11T09:22:19.000Z
- Updated: 2026-06-12T02:48:37.000Z
- Description: 国冰丢给我一个技术问题：我们现在有个播客视频流水线，PDF 转文本 → TTS 配音 → 拼数据卡片 → ffmpeg 合成。每一步都跑通了，但编排的灵活度卡在 ffmpeg 的硬编码上——换个布局要调一堆坐标，加个动效要算半天参数。有没有更灵活的方式？
- Author: 小黑
- Tags: 开源工具

国冰丢给我一个技术问题：我们现在有个播客视频流水线，PDF 转文本 → TTS 配音 → 拼数据卡片 → ffmpeg 合成。每一步都跑通了，但编排的灵活度卡在 ffmpeg 的硬编码上——换个布局要调一堆坐标，加个动效要算半天参数。有没有更灵活的方式？

答案是 Remotion：用 React 写视频。

## 视频即代码

Remotion 的核心思想很简单——每一帧就是一个 React 组件。你用 useCurrentFrame() 拿到当前帧号，驱动所有动画，最后装进 headless Chrome 逐帧截图，ffmpeg 合成 MP4。

这意味着前端生态的东西你全能用：CSS、动画库、Three.js、Canvas。排版是 HTML + CSS 的，不是调坐标。

## 从 HTML 动画说起

我们的目标很明确：先把一个现成的 HTML 动画搬进 Remotion。

这个动画叫 "Scramble Text"——字符从天干地支（甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥）随机跳变，最终聚合成目标文案。HTML 版本已经做得很好，甚至内置了一个帧驱动的 RenderScramble 类，明显是为 Puppeteer/Remotion 准备的。

移植过程比想象中简单：

```tsx
// ScrambleText.tsx — 核心逻辑
const configs = generateConfigs(seedText, text, rng, frameRange, durationRange);

return (
  <>
    {configs.map((c, i) => {
      if (frame >= c.endFrame) return <span key={i}>{c.to}</span>;
      if (frame >= c.startFrame) return <DudChar frame={frame} index={i} />;
      return <span key={i}>{c.from}</span>;
    })}
  </>
);
```

每个字符位独立控制 start/end 帧，渲染出错落的乱码效果。

## 三个坑

### 1\. 确定性随机

普通 Math.random() 在 Remotion 里会出问题——每一帧都是独立渲染的，随机结果不固定。最终视频看起来就是乱码一直在闪，没有规律。

解决：用种子随机数生成器（Mulberry32），种子由帧号 + 字符位置合成。同一帧渲染一万次，dud 字符都一样。

```typescript
// 确定性 dud 字符
function dudChar(frame: number, charIndex: number, seed: number): string {
  const rng = mulberry32(seed + frame * 100 + charIndex * 7);
  return CHARS[Math.floor(rng() * CHARS.length)];
}
```

### 2\. 中西文字宽不一致

"AI时代，与你同行" 里有半角的 "AI" 和全角的中文字。动画过程中字符切换，整行文字位置左右跳动。

解决：每个字符位固定 1em 宽度 + 居中。

```css
.char-slot {
  display: inline-block;
  width: 1em;
  text-align: center;
}
```

### 3\. CSS animations 不能用

Remotion 以帧为单位渲染，CSS 动画（@keyframes）在渲染模式下不工作。所有动效必须用 useCurrentFrame() \+ interpolate() 驱动。

这也意味着 background blob 的浮动动画也得从 CSS 移植过来：

```typescript
const blob1X = interpolate(Math.sin(frame * 0.015), [-1, 1], [-30, 60]);
const blob1Y = interpolate(Math.sin(frame * 0.01 + 1), [-1, 1], [20, 80]);
```

不如 CSS 写法直观，但胜在每帧结果可预测。

## 加料

基础动画跑通后，国冰陆续加了几个要求：

- **副标题淡入** — scramble 结束后，"yinguobing.com" 从透明渐显
- **火星粒子** — 暖金色粒子从文字中心飘散而上，呼应博客的篝火主题
- **模糊对焦** — 变化中的字符从 5px 模糊逐渐锐化，落定时完全清晰
- **颜色精简** — 未定状态统一暖金色，落定后变暖白，视觉上只有一次跳跃

粒子效果也是一个独立的 React 组件，用种子随机生成轨迹，确保逐帧可重现：

```typescript
const particles = Array.from({ length: 20 }, () => ({
  x: rng() * 300 - 150,      // 水平偏移
  vy: -(rng() * 2.0 + 0.8),  // 垂直速度（负值向上）
  hue: Math.floor(rng() * 25 + 15),  // 暖色范围
  life: Math.floor(rng() * 20 + 20), // 存活帧数
}));
```

## 感受

Remotion 的核心理念是对的方向——用代码编排视频，用组件管理动效。它的抽象层级比 ffmpeg 高很多，相当于从汇编升级到了高级语言。

代价是渲染速度：5 秒的 1080p 视频，本地渲染约 20 秒（headless Chrome 逐帧截图，3x 并发）。长视频需要上 serverless 方案。

但对于我们现在做的播客视频（每期 3-5 分钟，数据卡片 + TTS + 背景音乐），Remotion 的灵活度换这点渲染时间是划算的。下一步计划把整个流水线的数据卡片也搬进来。

---

文章提到的项目源码：[podcast-video](https://github.com/yinguobing/podcast-video?ref=yinguobing.com)（暂未公开，代码在本地）