🎯

remotion-video

🎯Skill

from goooice/rust-skills

VibeIndex|
What it does

Programmatically creates MP4 videos using React components with Remotion, enabling dynamic video generation and animations.

📦

Part of

goooice/rust-skills(35 items)

remotion-video

Installation

npm runRun npm script
npm run dev # 启动 Remotion Studio 预览
pip installInstall Python package
pip install edge-tts
PythonRun Python server
python scripts/generate_audio_edge.py
git cloneClone repository
git clone https://github.com/thudm/vllm-omni
PythonRun Python server
python -m vllm.entrypoints.openai.api_server \

+ 9 more commands

📖 Extracted from docs: goooice/rust-skills
1Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

|

Overview

# Remotion Video

用 React 以编程方式创建 MP4 视频的框架。

核心概念

  1. Composition - 视频的定义(尺寸、帧率、时长)
  2. useCurrentFrame() - 获取当前帧号,驱动动画
  3. interpolate() - 将帧号映射到任意值(位置、透明度等)
  4. spring() - 物理动画效果
  5. - 时间轴上排列组件

快速开始

创建新项目

```bash

npx create-video@latest

```

选择模板后:

```bash

cd

npm run dev # 启动 Remotion Studio 预览

```

项目结构

```

my-video/

├── src/

│ ├── Root.tsx # 注册所有 Composition

│ ├── HelloWorld.tsx # 视频组件

│ └── index.ts # 入口

├── public/ # 静态资源(音频、图片)

├── remotion.config.ts # 配置文件

└── package.json

```

基础组件示例

最小视频组件

```tsx

import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion";

export const MyVideo = () => {

const frame = useCurrentFrame();

const { fps, durationInFrames } = useVideoConfig();

return (

Frame {frame}

);

};

```

注册 Composition

```tsx

// Root.tsx

import { Composition } from "remotion";

import { MyVideo } from "./MyVideo";

export const RemotionRoot = () => {

return (

id="MyVideo"

component={MyVideo}

durationInFrames={150} // 5秒 @ 30fps

fps={30}

width={1920}

height={1080}

/>

);

};

```

动画技巧

interpolate - 值映射

```tsx

import { interpolate, useCurrentFrame } from "remotion";

const frame = useCurrentFrame();

// 0-30帧:透明度 0→1

const opacity = interpolate(frame, [0, 30], [0, 1], {

extrapolateRight: "clamp", // 超出范围时钳制

});

// 位移动画

const translateY = interpolate(frame, [0, 30], [50, 0]);

```

spring - 物理动画

```tsx

import { spring, useCurrentFrame, useVideoConfig } from "remotion";

const frame = useCurrentFrame();

const { fps } = useVideoConfig();

const scale = spring({

frame,

fps,

config: { damping: 10, stiffness: 100 },

});

```

Sequence - 时间编排

```tsx

import { Sequence } from "remotion";

<>

```

AI 语音解说集成

为视频添加 AI 语音解说,实现音视频同步。支持三种方案:

| 方案 | 优点 | 缺点 | 硬件要求 | 推荐度 |

|------|------|------|----------|--------|

| MiniMax TTS | 云端克隆、速度极快(<3秒)、音质优秀 | 按字符计费 | 无 | ⭐⭐⭐ 首选 |

| Edge TTS | 零配置、免费 | 固定音色、无法自定义 | 无 | ⭐⭐ |

| vLLM-Omni Qwen3-TTS | 本地部署、支持克隆音色、完全免费 | 需要 GPU、部署复杂 | GPU 8GB+ | ⭐⭐⭐ 自托管首选 |

方案选择流程

```

  1. 首选 MiniMax TTS

- 检测 API Key 是否配置

- 测试调用是否正常(余额充足)

- 如果成功 → 使用 MiniMax

  1. MiniMax 不可用时

→ 退回 Edge TTS(使用预设音色 zh-CN-YunyangNeural)

  1. 需要本地部署/完全免费

→ 使用 vLLM-Omni Qwen3-TTS(需要 GPU)

```

---

方案一:MiniMax TTS(推荐)

云端 API 方案,无需本地 GPU,生成速度极快,音色克隆效果优秀。

配置

  1. 注册 https://www.minimax.io (国际版)或 https://platform.minimaxi.com (国内版)
  2. 获取 API Key
  3. 在 MiniMax Audio 上传音频克隆音色,获取 voice_id

API 差异

| 版本 | API 域名 | 说明 |

|------|----------|------|

| 国际版 | api.minimax.io | 推荐,稳定 |

| 国内版 | api.minimaxi.com | 需国内账号 |

⚠️ 常见错误api.minimax.chat错误的域名,会返回 "invalid api key"。请确认使用上表中的正确域名。

生成脚本

使用 scripts/generate_audio_minimax.py 生成音频,支持:

  • 断点续作:已存在的音频文件自动跳过
  • 实时进度:显示生成进度,避免茫然等待
  • 自动更新配置:生成完成后自动更新 Remotion 的场景配置

```bash

# 设置环境变量

export MINIMAX_API_KEY="your_api_key"

export MINIMAX_VOICE_ID="your_voice_id"

# 运行脚本

python scripts/generate_audio_minimax.py

```

价格参考(2025年)

| 模型 | 价格 |

|------|------|

| speech-02-hd | ¥0.1/千字符 |

| speech-02-turbo | ¥0.05/千字符 |

⚠️ MiniMax TTS 踩坑经验

| 问题 | 原因 | 解决方案 |

|------|------|----------|

| invalid api key | 使用了错误的 API 域名 | 国际版用 api.minimax.io,国内版用 api.minimaxi.com |

| config.ts 语法错误 Syntax error "n" | Python 脚本在 f-string 中用 ",\\n".join() 产生了字面量 \n 而非真正换行 | 见下方「Python 生成 TypeScript 注意事项」 |

| 长时间无进度显示 | 后台执行命令看不到输出 | 前台执行脚本,或用 tail -f 实时查看日志 |

Python 生成 TypeScript 注意事项

❌ 错误写法:在 f-string 中使用 \n 会产生字面量字符

```python

# 这会在生成的文件中写入字面的 \n 字符串,而非换行!

content = f'export const SCENES = [{",\\n".join(items)}];'

```

✅ 正确写法:分开处理字符串拼接

```python

# 先用真正的换行符拼接

scenes_content = ",\n".join(items) # 在 f-string 外部拼接

# 再放入模板

content = f'''export const SCENES = [

{scenes_content}

];'''

```

---

方案二:Edge TTS

无需特殊硬件,完全免费,适合不需要克隆音色的场景。

安装

```bash

pip install edge-tts

```

推荐语音

| 语音 ID | 名称 | 风格 |

|---------|------|------|

| zh-CN-YunyangNeural | 云扬 | 专业播音腔(推荐) |

| zh-CN-XiaoxiaoNeural | 晓晓 | 温暖自然 |

| zh-CN-YunxiNeural | 云希 | 阳光少年 |

生成脚本

使用 scripts/generate_audio_edge.py 生成音频:

```bash

python scripts/generate_audio_edge.py

```

Remotion 音频同步

```tsx

import { Audio, Sequence, staticFile } from "remotion";

// 音频配置(根据生成的时长)

const audioConfig = [

{ id: "01-intro", file: "01-intro.mp3", frames: 450 },

{ id: "02-main", file: "02-main.mp3", frames: 600 },

];

// 计算起始帧

const sceneStarts = audioConfig.reduce((acc, _, i) => {

if (i === 0) return [0];

return [...acc, acc[i - 1] + audioConfig[i - 1].frames];

}, [] as number[]);

// 场景渲染

{audioConfig.map((scene, i) => (

))}

```

---

方案三:vLLM-Omni Qwen3-TTS(本地部署)

本地部署方案,使用 vLLM-Omni 部署 Qwen3-TTS 模型,完全免费,支持自定义音色。

硬件要求

| 组件 | 最低配置 | 推荐配置 |

|------|----------|----------|

| GPU | 8GB VRAM | 16GB+ VRAM |

| 内存 | 16GB | 32GB |

| 存储 | 20GB | 50GB SSD |

部署步骤

#### 1. 安装 vLLM-Omni

```bash

# 克隆 vLLM-Ommi 仓库

git clone https://github.com/thudm/vllm-omni

cd vllm-omni

# 安装依赖

pip install -r requirements.txt

```

#### 2. 启动服务器

```bash

# 启动 vLLM-Omni 服务器(使用 Qwen3-TTS 模型)

python -m vllm.entrypoints.openai.api_server \

--model Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice \

--port 8000

```

#### 3. 测试服务器

```bash

curl http://localhost:8000/v1/models

```

配置

设置环境变量:

```bash

# 服务器地址(默认)

export VLLM_BASE_URL="http://localhost:8000/v1"

# 模型名称(默认)

export VLLM_MODEL_NAME="Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice"

# 预设语音(可选)

export VLLM_VOICE="Vivian"

```

可用语音

Qwen3-TTS 支持多种预设语音:

| 语音 ID | 名称 | 风格 |

|---------|------|------|

| Vivian | 薇薇安 | 温柔女声 |

| Adam | 亚当 | 成熟男声 |

| echo | 艾科 | 活泼女声 |

| ... | ... | ... |

完整语音列表请参考 [Qwen3-TTS 文档](https://github.com/Qwen/Qwen3-TTS)。

生成脚本

使用 generate_audio_qwen.py 生成音频:

```bash

# 安装依赖

pip install openai>=1.0.0

# 运行脚本

python generate_audio_qwen.py

```

脚本特性:

  • 断点续作:已存在的音频文件自动跳过
  • 实时进度:显示生成进度
  • 自动更新配置:生成完成后自动更新 audioConfig.ts
  • 格式转换:自动处理 WAV → MP3 转换

⚠️ vLLM-Ommi 踩坑经验

| 问题 | 原因 | 解决方案 |

|------|------|----------|

| 连接拒绝 | 服务器未启动 | 确认 vLLM-Omni 服务正在运行 |

| CUDA OOM | GPU 显存不足 | 降低 batch_size 或使用更小的模型 |

| 音频格式错误 | 返回 WAV 而非 MP3 | 脚本会自动用 ffmpeg 转换 |

性能参考

| 模型 | 生成速度 | 音质 |

|------|----------|------|

| Qwen3-TTS-12Hz-0.6B-CustomVoice | ~5-10s/句 | 优秀 |

---

教程类视频架构(场景驱动)

教程、讲解类视频的核心架构:音频驱动场景切换

架构概览

```

音频脚本 → TTS 生成 → audioConfig.ts → 场景组件 → 视频渲染

```

关键思想:

  1. 音频决定时长:每个场景的持续时间由音频长度决定
  2. 场景即章节:一个概念 = 一个场景 = 一段音频
  3. 配置即真理audioConfig.ts 是音画同步的单一数据源

audioConfig.ts 模板

参见 templates/audioConfig.ts,包含:

  • SceneConfig 接口定义
  • SCENES 数组
  • getSceneStart() 计算函数
  • TOTAL_FRAMES 和 FPS 常量

场景切换 Hook

```tsx

import { useCurrentFrame } from "remotion";

import { SCENES } from "./audioConfig";

// 根据当前帧号返回场景索引

const useCurrentSceneIndex = () => {

const frame = useCurrentFrame();

let accumulated = 0;

for (let i = 0; i < SCENES.length; i++) {

accumulated += SCENES[i].durationInFrames;

if (frame < accumulated) return i;

}

return SCENES.length - 1;

};

// 使用

const sceneIndex = useCurrentSceneIndex();

const currentScene = SCENES[sceneIndex];

```

主场景组件模式

```tsx

import { AbsoluteFill, Audio, Sequence, staticFile, useVideoConfig } from "remotion";

import { ThreeCanvas } from "@remotion/three";

import { SCENES, getSceneStart, TOTAL_FRAMES } from "./audioConfig";

export const TutorialVideo: React.FC = () => {

const { width, height } = useVideoConfig();

const sceneIndex = useCurrentSceneIndex();

const currentScene = SCENES[sceneIndex];

return (

{/ 3D 内容 /}

{/ 根据 sceneIndex 渲染不同场景 /}

{sceneIndex === 0 && }

{sceneIndex === 1 && }

{sceneIndex === 2 && }

{/ 音频同步 - 每个场景一个 Sequence /}

{SCENES.map((scene, idx) => (

))}

{/ UI 层:标题 + 进度 /}

教程标题

{currentScene?.title}

{/ 进度条 /}

${((sceneIndex + 1) / SCENES.length) * 100}%, height: "100%", backgroundColor: "#3498DB" }} />

);

};

```

Root.tsx 使用动态帧数

```tsx

import { Composition } from "remotion";

import { TutorialVideo } from "./TutorialVideo";

import { TOTAL_FRAMES } from "./audioConfig";

export const RemotionRoot: React.FC = () => {

return (

id="Tutorial"

component={TutorialVideo}

fps={30}

durationInFrames={TOTAL_FRAMES} // 从 audioConfig 动态获取

width={1920}

height={1080}

/>

);

};

```

⚠️ 教程视频踩坑经验

| 问题 | 原因 | 解决方案 |

|------|------|----------|

| 场景切换生硬 | 直接切换无过渡 | 用 spring/interpolate 添加入场动画 |

| 3D 内容与音频不同步 | 硬编码帧数 | 所有时长从 audioConfig 读取 |

| 渲染时 WebGL 崩溃 | 多个 ThreeCanvas 同时存在 | 用 sceneIndex 条件渲染,同时只有一个 3D 场景 |

| 视频太简略 | 只有一个大场景 | 一个概念 = 一个场景组件,分层讲解 |

场景组件设计原则

  1. 单一职责:每个场景组件只负责一个概念
  2. 独立动画:每个场景有自己的 useCurrentFrame(),动画从 0 开始
  3. 延迟出现:用 delay 参数控制元素依次出现
  4. 相机适配:不同场景可能需要不同相机位置

```tsx

// 场景组件示例

const Scene02Input: React.FC = () => {

const frame = useCurrentFrame();

const { fps } = useVideoConfig();

// 入场动画

const gridScale = spring({ frame, fps, config: { damping: 15 } });

return (

);

};

```

相机控制器模式

```tsx

import { useThree } from "@react-three/fiber";

// ✅ 推荐写法:直接设置相机位置,避免插值导致的持续抖动

const CameraController: React.FC<{ sceneIndex: number }> = ({ sceneIndex }) => {

const { camera } = useThree();

const cameraSettings: Record = {

0: [0, 0, 4], // 开场:正面

1: [0, 0, 3], // 输入层:靠近

2: [-0.5, 0, 3.5], // 卷积:偏左

3: [0, 0, 5], // 总结:拉远全景

};

const target = cameraSettings[sceneIndex] || [0, 0, 4];

// 直接设置位置,不用插值

camera.position.set(target[0], target[1], target[2]);

camera.lookAt(0, 0, 0);

return null;

};

```

⚠️ *不要用 position += (target - position) factor 这种写法**,永远无法精确收敛,会导致画面持续抖动。详见「🚨 3D 场景常见陷阱 - 陷阱1」。

---

常用功能

添加视频/音频

```tsx

import { Video, Audio, staticFile } from "remotion";

// 使用 public/ 目录下的文件

// 外部 URL

```

添加图片

```tsx

import { Img, staticFile } from "remotion";

```

参数化视频(动态数据)

```tsx

// 定义 props schema

const myCompSchema = z.object({

title: z.string(),

bgColor: z.string(),

});

export const MyVideo: React.FC> = ({ title, bgColor }) => {

return (

{title}

);

};

// 注册时传入默认值

id="MyVideo"

component={MyVideo}

schema={myCompSchema}

defaultProps={{ title: "Hello", bgColor: "#ffffff" }}

...

/>

```

渲染输出

CLI 渲染

```bash

# 渲染为 MP4

npx remotion render MyVideo out/video.mp4

# 指定编码器

npx remotion render --codec=h264 MyVideo out/video.mp4

# WebM 格式

npx remotion render --codec=vp8 MyVideo out/video.webm

# GIF

npx remotion render --codec=gif MyVideo out/video.gif

# 仅音频

npx remotion render --codec=mp3 MyVideo out/audio.mp3

# 图片序列

npx remotion render --sequence MyVideo out/frames

# 单帧静态图

npx remotion still MyVideo --frame=30 out/thumbnail.png

```

常用渲染参数

| 参数 | 说明 |

|------|------|

| --codec | h264, h265, vp8, vp9, gif, mp3, wav 等 |

| --crf | 质量 (0-51,越小越好,默认18) |

| --props | JSON 格式传入 props |

| --scale | 缩放因子 |

| --concurrency | 并行渲染数 |

高级功能

字幕 (@remotion/captions)

```bash

npm i @remotion/captions @remotion/install-whisper-cpp

npx remotion-install-whisper-cpp # 安装 Whisper

```

```ts

import { transcribe } from "@remotion/install-whisper-cpp";

const { transcription } = await transcribe({

inputPath: "audio.mp3",

whisperPath: whisperCppPath,

model: "medium",

});

```

播放器嵌入 Web 应用

```bash

npm i @remotion/player

```

```tsx

import { Player } from "@remotion/player";

import { MyVideo } from "./MyVideo";

component={MyVideo}

durationInFrames={150}

fps={30}

compositionWidth={1920}

compositionHeight={1080}

style={{ width: "100%" }}

controls

inputProps={{ title: "Dynamic Title" }}

/>

```

AWS Lambda 渲染

```bash

npm i @remotion/lambda

npx remotion lambda policies role # 设置 IAM

npx remotion lambda sites create # 部署站点

npx remotion lambda render MyVideo # 渲染

```

3D 视频制作(@remotion/three)

使用 React Three Fiber 在 Remotion 中创建 3D 动画视频。

适用场景

| 场景 | 说明 | 示例 |

|------|------|------|

| 产品展示 | 3D 模型旋转、拆解动画 | 手机产品宣传片 |

| 角色动画 | 卡通角色讲解、故事叙述 | 育儿科普视频 |

| 数据可视化 | 3D 图表、空间数据 | 地理信息、建筑展示 |

| Logo 动画 | 品牌 3D Logo 入场 | 片头片尾 |

安装

```bash

npm i three @react-three/fiber @remotion/three @types/three

```

官方模板(推荐新手):

```bash

npx create-video@latest --template three

```

基础示例

```tsx

import { ThreeCanvas } from "@remotion/three";

import { useCurrentFrame, useVideoConfig, interpolate, spring } from "remotion";

import { useEffect } from "react";

import { useThree } from "@react-three/fiber";

// 3D 场景组件

const My3DScene = () => {

const frame = useCurrentFrame();

const { fps, durationInFrames } = useVideoConfig();

const camera = useThree((state) => state.camera);

// 设置相机

useEffect(() => {

camera.position.set(0, 0, 5);

camera.lookAt(0, 0, 0);

}, [camera]);

// 旋转动画

const rotation = interpolate(frame, [0, durationInFrames], [0, Math.PI * 2]);

// 弹性入场

const scale = spring({ frame, fps, config: { damping: 10, stiffness: 100 } });

return (

);

};

// 视频组件

export const My3DVideo = () => {

const { width, height } = useVideoConfig();

return (

);

};

```

加载 GLTF 模型

```tsx

import { useGLTF } from "@react-three/drei";

import { useCurrentFrame, interpolate } from "remotion";

const Model = () => {

const frame = useCurrentFrame();

const { scene } = useGLTF("/models/character.glb");

const rotation = interpolate(frame, [0, 150], [0, Math.PI * 2]);

return ;

};

```

安装 drei(React Three Fiber 工具库):

```bash

npm i @react-three/drei

```

视频作为 3D 纹理

```tsx

import { ThreeCanvas, useVideoTexture } from "@remotion/three";

import { staticFile, useVideoConfig } from "remotion";

const VideoOnMesh = () => {

const { width, height } = useVideoConfig();

const videoTexture = useVideoTexture(staticFile("/video.mp4"));

return (

{videoTexture && }

);

};

```

渲染时使用 useOffthreadVideoTexture() 确保帧精确:

```tsx

import { useOffthreadVideoTexture } from "@remotion/three";

const texture = useOffthreadVideoTexture({ src: staticFile("/video.mp4") });

```

3D 角色组合技巧

用基础几何体组合角色(无需专业建模):

```tsx

// 简单卡通角色:头 + 身体 + 四肢

const CartoonCharacter = ({ emotion = "happy" }) => {

const frame = useCurrentFrame();

// 表情控制

const eyeScale = emotion === "happy" ? 1 : 0.5;

const mouthRotation = emotion === "happy" ? 0 : Math.PI;

// 走路动画:腿部摆动

const legSwing = Math.sin(frame 0.2) 0.3;

return (

{/ 头部 - 球体 /}

{/ 身体 - 胶囊体 /}

{/ 左腿 /}

{/ 右腿 /}

);

};

```

⚠️ 踩坑经验

#### WebGL 上下文溢出

问题:多个 3D 场景同时渲染时报错 Error creating WebGL context

原因:浏览器限制 WebGL 上下文数量(通常 8-16 个)

解决方案

  1. 渲染配置:使用 angle OpenGL 引擎

```ts

// remotion.config.ts

export default {

chromiumOptions: {

gl: "angle", // 或 "angle-egl"

},

};

```

CLI 渲染时:

```bash

npx remotion render --gl=angle MyVideo out.mp4

```

  1. 懒加载场景:只渲染当前帧附近的 3D 内容

```tsx

import { useCurrentFrame } from "remotion";

const LazyScene = ({ sceneStart, sceneDuration, children }) => {

const frame = useCurrentFrame();

const buffer = 30; // 缓冲 30 帧

// 只在场景时间范围 ± buffer 内渲染

const shouldRender =

frame >= sceneStart - buffer &&

frame <= sceneStart + sceneDuration + buffer;

if (!shouldRender) {

return null; // 不渲染,释放 WebGL 上下文

}

return <>{children};

};

// 使用

```

#### 服务端渲染配置

服务端渲染(SSR)必须配置 gl 选项:

```ts

// renderMedia() / renderFrames() / getCompositions()

await renderMedia({

composition,

serveUrl,

outputLocation: "out.mp4",

chromiumOptions: {

gl: "angle",

},

});

```

#### Sequence 内的 useCurrentFrame

内部的 useCurrentFrame() 返回的是相对于 Sequence 开始的帧号,不是全局帧号。

```tsx

{/ 这里 useCurrentFrame() 从 0 开始,不是 60 /}

```

进阶资源

| 资源 | 用途 | 链接 |

|------|------|------|

| Mixamo | 免费骨骼动画库 | https://www.mixamo.com |

| Sketchfab | 免费/付费 3D 模型 | https://sketchfab.com |

| Ready Player Me | 虚拟人物生成 | https://readyplayer.me |

| Spline | 在线 3D 设计工具 | https://spline.design |

| gltfjsx | GLTF 转 React 组件 | npx gltfjsx model.glb |

进阶方向

  1. Blender → GLTF:用 Blender 建模,导出 GLTF 格式,用 useGLTF 加载
  2. Mixamo 动画:下载 FBX 动画,转换为 GLTF,用 useAnimations 播放
  3. Spline 设计:在 Spline 设计 3D 场景,用 @splinetool/r3f-spline 导入

---

3Blue1Brown 风格指南(教程类视频)

针对教程、讲解类视频,借鉴 3Blue1Brown 的可视化设计原则。

核心理念

```

3B1B 内核:让观众「自己发现」,而不是「被告知答案」

```

| 原则 | 说明 | 示例 |

|------|------|------|

| Why → What | 先提问为什么,再展示是什么 | "如何识别手写数字?" → 展示神经网络 |

| 逐步构建 | 元素一个个出现,不要整体淡入 | 神经元依次点亮,而非同时出现 |

| 颜色有语义 | 颜色传达信息,不是装饰 | 蓝=正、红=负、黄=高亮 |

| 数值具象化 | 显示具体数字让抽象概念落地 | 像素值 0.7、激活值 0.92 |

| 2D 优先 | 清晰优先于炫酷,必要时才用 3D | 网络结构用 2D,空间数据用 3D |

配色方案

```tsx

// 3B1B 风格配色(语义化)

const COLORS_3B1B = {

background: "#000000", // 纯黑背景

positive: "#58C4DD", // 蓝色 - 正权重/正向

negative: "#FF6B6B", // 红色 - 负权重/负向

highlight: "#FFFF00", // 黄色 - 当前焦点/高亮

result: "#83C167", // 绿色 - 结果/正确

text: "#FFFFFF", // 白色 - 文字

neutral: "#888888", // 灰色 - 中性/未激活

accent: "#FF8C00", // 橙色 - 强调

};

// 使用示例

color={weight > 0 ? COLORS_3B1B.positive : COLORS_3B1B.negative}

emissive={isHighlighted ? COLORS_3B1B.highlight : "#000"}

emissiveIntensity={isHighlighted ? 0.3 : 0}

/>

```

2D/3D 混合策略

| 内容类型 | 推荐维度 | 原因 |

|----------|----------|------|

| 网络结构图 | 2D | 层次清晰,易于标注 |

| 数据流向 | 2D + 动画箭头 | 强调顺序和因果 |

| 卷积操作 | 2D 俯视图 | 网格对齐,数值可见 |

| 特征图堆叠 | 2.5D(透视) | 展示深度/通道数 |

| 3D 物体识别 | 3D | 内容本身是 3D |

2D 模式实现:使用正交相机 + 扁平几何体

```tsx

import { OrthographicCamera } from "@react-three/drei";

// 正交相机 = 无透视变形 = 2D 感觉

// 扁平几何体

{/ 2D 平面 /}

```

逐步构建动画

核心:用 delay 参数控制元素依次出现

```tsx

// 批量元素逐个出现

const StaggeredGroup: React.FC<{

children: React.ReactNode[];

delayPerItem?: number

}> = ({ children, delayPerItem = 8 }) => {

const frame = useCurrentFrame();

const { fps } = useVideoConfig();

return (

<>

{React.Children.map(children, (child, i) => {

const delay = i * delayPerItem;

const progress = spring({

frame: frame - delay,

fps,

config: { damping: 12, stiffness: 100 },

});

if (frame < delay) return null;

return (

{child}

);

})}

);

};

// 使用

```

数值标签组件

```tsx

import { Text } from "@react-three/drei";

const ValueLabel: React.FC<{

value: number;

position: [number, number, number];

fontSize?: number;

}> = ({ value, position, fontSize = 0.15 }) => {

// 根据值选择颜色

const color = value > 0.5 ? COLORS_3B1B.positive :

value < -0.5 ? COLORS_3B1B.negative :

COLORS_3B1B.neutral;

return (

position={position}

fontSize={fontSize}

color={color}

anchorX="center"

anchorY="middle"

font="/fonts/JetBrainsMono-Regular.ttf" // 等宽字体

>

{value.toFixed(2)}

);

};

```

高亮焦点组件

```tsx

// 脉冲高亮框 - 引导注意力

const FocusBox: React.FC<{

position: [number, number, number];

size: [number, number];

label?: string;

}> = ({ position, size, label }) => {

const frame = useCurrentFrame();

const pulse = 1 + Math.sin(frame 0.15) 0.08;

return (

{/ 高亮框 /}

color={COLORS_3B1B.highlight}

transparent

opacity={0.2}

/>

{/ 边框 /}

{/ 标签 /}

{label && (

{label}

)}

);

};

```

脚本撰写指南(教程类)

❌ 宣布式(避免)

```

"首先是输入层。图像是一个数字矩阵。"

"接下来是卷积层。卷积核在图像上滑动。"

```

✅ 探索式(推荐)

```

"你能轻松认出这是数字 7,但你能描述你是怎么做到的吗?

(停顿 1 秒)

这正是神经网络要解决的问题。

让我们先看看计算机「看到」的是什么——

(数字网格逐个显示)

不是图像,而是 784 个数字。

那么问题来了:如何从这堆数字中识别出 7?"

```

脚本结构模板

```

  1. 🎯 提出问题(10%)

- 用观众能共鸣的问题开场

- "你有没有想过..."

  1. 🤔 直觉猜测(15%)

- 引导观众思考可能的方案

- "也许我们可以..."

  1. 🔍 逐步验证(50%)

- 一步步展示机制

- 每一步都回答「为什么这样设计」

  1. 📐 形式化(15%)

- 展示数学公式(可选)

- 将直觉转化为精确描述

  1. 🎬 回顾总结(10%)

- 完整流程快速回放

- 强调核心洞见

```

⚠️ 常见误区

| 误区 | 问题 | 改进 |

|------|------|------|

| 3D 炫技 | 旋转、透视分散注意力 | 用最简单的视角表达 |

| 颜色随意 | 红绿蓝只是装饰 | 建立颜色-含义映射 |

| 整体出现 | 观众不知道看哪里 | 逐个元素 + 高亮引导 |

| 只说 What | 观众不理解设计动机 | 先问 Why 再展示 What |

| 信息过载 | 一个场景塞太多概念 | 一个场景一个概念 |

---

过程动画模式(Process Animation)

核心理念:不只展示「是什么」,更要展示「怎么算」。让观众亲眼看到数据如何流动、计算如何发生。

适用场景

| 场景 | 说明 | 示例 |

|------|------|------|

| 算法可视化 | 展示每一步操作 | 排序、搜索、图遍历 |

| 数学公式推导 | 逐项展开计算 | 矩阵乘法、卷积运算 |

| 数据处理流程 | 输入→变换→输出 | CNN 前向传播、数据清洗 |

| 决策过程 | 比较、筛选、最终选择 | 池化取最大值、softmax |

动画模式分类

```

静态展示 → 结构动画 → 过程动画

↓ ↓ ↓

截图 元素出现 计算过程

淡入淡出 数据流动

相机移动 结果写入

```

过程动画组件库

#### 1. 计算步骤展示(StepByStep)

```tsx

// 逐步显示计算过程

const StepByStepCalc: React.FC<{

steps: string[]; // ["1×0.5", "+ 0×0.3", "+ 1×(-0.2)", "= 0.3"]

startFrame: number;

framesPerStep?: number;

}> = ({ steps, startFrame, framesPerStep = 20 }) => {

const frame = useCurrentFrame();

return (

{steps.map((step, i) => {

const stepStart = startFrame + i * framesPerStep;

const opacity = interpolate(frame, [stepStart, stepStart + 10], [0, 1], {

extrapolateLeft: "clamp",

extrapolateRight: "clamp",

});

const isResult = i === steps.length - 1;

return (

key={i}

style={{

opacity,

color: isResult ? COLORS.result : COLORS.text,

fontWeight: isResult ? "bold" : "normal",

}}

>

{step}{" "}

);

})}

);

};

```

#### 2. 数值飞入动画(ValueFlyIn)

```tsx

// 计算结果飞入目标位置

const ValueFlyIn: React.FC<{

value: number;

from: [number, number, number];

to: [number, number, number];

startFrame: number;

duration?: number;

}> = ({ value, from, to, startFrame, duration = 30 }) => {

const frame = useCurrentFrame();

const { fps } = useVideoConfig();

const progress = spring({

frame: frame - startFrame,

fps,

config: { damping: 15, stiffness: 80 },

});

if (frame < startFrame) return null;

const position: [number, number, number] = [

from[0] + (to[0] - from[0]) * progress,

from[1] + (to[1] - from[1]) * progress,

from[2] + (to[2] - from[2]) * progress,

];

const scale = 1.5 - 0.5 * progress; // 飞行时放大,落地时缩小

return (

position={position}

fontSize={0.12 * scale}

color={COLORS.result}

anchorX="center"

anchorY="middle"

>

{value.toFixed(1)}

);

};

```

#### 3. 区域高亮比较(CompareHighlight)

```tsx

// 多个值依次比较,胜出者高亮

const CompareHighlight: React.FC<{

values: number[];

positions: [number, number, number][];

startFrame: number;

framesPerCompare?: number;

}> = ({ values, positions, startFrame, framesPerCompare = 15 }) => {

const frame = useCurrentFrame();

// 计算当前比较进度

const compareIndex = Math.floor((frame - startFrame) / framesPerCompare);

const maxIndex = values.indexOf(Math.max(...values));

return (

<>

{values.map((value, i) => {

const isComparing = i <= compareIndex && i <= maxIndex;

const isWinner = compareIndex >= values.length - 1 && i === maxIndex;

return (

color={isWinner ? COLORS.result : isComparing ? COLORS.highlight : COLORS.dim}

emissive={isWinner ? COLORS.result : "#000"}

emissiveIntensity={isWinner ? 0.5 : 0}

/>

{value}

);

})}

);

};

```

#### 4. 滑动窗口(SlidingWindow)

```tsx

// 卷积核/池化窗口滑动

const SlidingWindow: React.FC<{

gridSize: number; // 输入网格大小

windowSize: number; // 窗口大小 (3 for 3x3)

stride: number; // 步幅

currentStep: number; // 当前步骤 (0, 1, 2, ...)

onPositionChange?: (row: number, col: number) => void;

}> = ({ gridSize, windowSize, stride, currentStep }) => {

const outputSize = Math.floor((gridSize - windowSize) / stride) + 1;

const totalSteps = outputSize * outputSize;

const step = Math.min(currentStep, totalSteps - 1);

const row = Math.floor(step / outputSize) * stride;

const col = (step % outputSize) * stride;

// 窗口位置(相对于网格中心)

const pixelSize = 0.12;

const gap = 0.01;

const offset = (gridSize / 2 - 0.5) * (pixelSize + gap);

const windowOffset = (windowSize / 2 - 0.5) * (pixelSize + gap);

const x = col * (pixelSize + gap) - offset + windowOffset;

const y = row * (pixelSize + gap) - offset + windowOffset;

return (

pixelSize + (windowSize - 1) gap,

windowSize pixelSize + (windowSize - 1) gap, 0.02]} />

color={COLORS.negative}

transparent

opacity={0.6}

emissive={COLORS.negative}

emissiveIntensity={0.3}

/>

);

};

```

脚本撰写指南(过程动画版)

关键转变:脚本需要配合动画节奏,给动画「留白时间」。

❌ 传统脚本(信息密集)

```

"卷积核在图像上滑动,每到一个位置就做点乘运算,得到一个数值。"

(一句话带过,观众还没看清发生了什么)

```

✅ 过程动画脚本(留白配合)

```

"让我们看看卷积是怎么计算的。"

(停顿 - 窗口移动到位置)

"卷积核覆盖了这 9 个像素。"

(停顿 - 高亮 3x3 区域)

"我们把每个像素值,和对应的权重相乘..."

(停顿 - 逐步显示乘法)

"然后把所有结果加起来。"

(停顿 - 显示求和过程)

"得到的这个数字,就写入特征图的对应位置。"

(停顿 - 结果飞入)

"第一个位置完成了。接下来,窗口向右滑动一格..."

(加速展示后续步骤)

```

时间分配建议

| 详细程度 | 首次完整展示 | 重复加速 | 适用场景 |

|----------|--------------|----------|----------|

| 极详细 | 3-4 秒/步 | 0.5 秒/步 | 核心概念首次出现 |

| 中等 | 2 秒/步 | 0.3 秒/步 | 辅助概念 |

| 快速 | 1 秒/步 | 闪过 | 已解释过的重复 |

示例:卷积场景时间分配

```

总时长:~25 秒

0-3s: 引入("让我们看看卷积是怎么计算的")

3-12s: 第 1 次卷积(完整详细展示)

- 窗口移动 (1s)

- 高亮区域 (1s)

- 计算过程 (4s)

- 结果飞入 (2s)

- 解说旁白 (1s)

12-18s: 第 2-3 次卷积(中等速度,简化解说)

18-23s: 剩余位置(快速滑动,仅显示结果)

23-25s: 展示完整特征图

```

⚠️ 过程动画踩坑经验

| 问题 | 原因 | 解决方案 |

|------|------|----------|

| 动画太快看不清 | 时间分配不足 | 增加关键步骤的帧数 |

| 解说与动画不同步 | 脚本没有留白 | 重写脚本,加入停顿标记 |

| 信息过载 | 一次展示太多 | 分阶段:先结构,再过程 |

| 重复内容无聊 | 每次都详细展示 | 首次详细 + 后续加速 |

| 数值太小看不见 | 3D 文字渲染问题 | 用 2D HTML overlay |

| 相机持续抖动 | 插值永不收敛 | 见下方「相机控制陷阱」 |

| 图像旋转90度 | 行列坐标映射反了 | 见下方「网格坐标陷阱」 |

| 进度显示好几千% | progress 变量未 clamp | Math.min(1, (frame - start) / duration) |

| 特征图只有色块无数值 | 组件缺少数值显示功能 | 添加 values + showValues 参数 |

#### 进度变量必须 clamp

```tsx

// ❌ 错误:场景持续时间可能远超预期,progress 会变成 5000%

const calcProgress = frame > 30 ? (frame - 30) / 60 : 0;

// ✅ 正确:限制在 [0, 1] 范围

const calcProgress = frame > 30 ? Math.min(1, (frame - 30) / 60) : 0;

```

#### 特征图显示计算结果

```tsx

// FeatureMap 组件应支持显示数值

position={[2, 0, 0]}

size={0.6}

count={1}

color={COLORS.result}

filledCells={filledCount}

gridSize={6}

values={[2, -1, 0, 3, ...]} // 每个格子的计算结果

showValues // 启用数值显示

/>

```

🚨 3D 场景常见陷阱

#### 陷阱 1:相机持续抖动

症状:画面一直微微放大-缩小抖动

错误写法

```tsx

// ❌ 永远无法精确到达目标,导致持续微抖动

const CameraController = ({ targetZ }) => {

const { camera } = useThree();

const frame = useCurrentFrame();

useEffect(() => {

camera.position.z += (targetZ - camera.position.z) * 0.05;

}, [frame]);

return null;

};

```

正确写法

```tsx

// ✅ 方案A:使用 spring 动画(推荐)

const CameraController = ({ targetZ, transitionFrame = 0 }) => {

const { camera } = useThree();

const frame = useCurrentFrame();

const { fps } = useVideoConfig();

const z = spring({

frame: frame - transitionFrame,

fps,

from: camera.position.z,

to: targetZ,

config: { damping: 20, stiffness: 100 },

});

camera.position.z = z;

return null;

};

// ✅ 方案B:直接设置(无过渡)

const CameraController = ({ targetZ }) => {

const { camera } = useThree();

camera.position.set(0, 0, targetZ);

camera.lookAt(0, 0, 0);

return null;

};

// ✅ 方案C:插值但加阈值

useEffect(() => {

const delta = targetZ - camera.position.z;

if (Math.abs(delta) < 0.001) {

camera.position.z = targetZ; // 接近时直接设置

} else {

camera.position.z += delta * 0.1;

}

}, [frame]);

```

#### 陷阱 2:网格图像旋转90度

症状:本应显示为正常方向的图像(如数字7)被旋转了90度

根因:图像处理中 row 对应 y 轴(从上到下),col 对应 x 轴(从左到右),

但代码里把行索引映射到了 x 坐标,列索引映射到了 y 坐标。

错误写法

```tsx

// ❌ row 映射到 x,col 映射到 y,图像会旋转90度

for (let row = 0; row < size; row++) {

for (let col = 0; col < size; col++) {

const x = (row - size/2) * cellSize; // 错!row 应该是 y

const y = (col - size/2) * cellSize; // 错!col 应该是 x

// ...

}

}

```

正确写法

```tsx

// ✅ col 映射到 x,row 映射到 y(且 y 要翻转)

for (let row = 0; row < size; row++) {

for (let col = 0; col < size; col++) {

const x = (col - size/2 + 0.5) * cellSize; // col → x

const y = ((size - 1 - row) - size/2 + 0.5) * cellSize; // row → y(翻转)

// ...

}

}

```

记忆口诀

  • 图像坐标:image[row][col] = image[y][x](行是y,列是x)
  • 3D 坐标:x 向右,y 向上
  • 翻转 row:图像 row=0 在顶部,3D y=max 在顶部

---

工作流最佳实践

推荐的 npm scripts 配置

```json

{

"scripts": {

"dev": "remotion studio",

"audio": "python3 scripts/generate_audio.py",

"render": "remotion render MyVideo out/video.mp4",

"build": "npm run audio && npm run render"

}

}

```

实时进度显示

音频生成和视频渲染都可能耗时较长,务必使用前台执行以便看到进度:

```bash

# ✅ 推荐:前台执行,实时显示进度

npm run audio

npm run render

# ✅ 或者用 shell 脚本封装

bash scripts/render.sh

# ❌ 避免:后台执行看不到进度

npm run render &

```

render.sh 示例

```bash

#!/bin/bash

cd "$(dirname "$0")/.."

echo "🎬 开始渲染视频..."

npx remotion render MyVideo out/video.mp4

if [ $? -eq 0 ]; then

echo "✅ 渲染完成!"

ls -lh out/video.mp4

else

echo "❌ 渲染失败"

exit 1

fi

```

断点续作设计原则

长时间任务(如批量生成音频)应支持断点续作:

  1. 检查已存在文件:跳过已完成的项目
  2. 原子操作:单个文件生成失败不影响已完成的
  3. 进度保存:失败时保留已完成的部分
  4. 幂等执行:重复运行产生相同结果

调试技巧

  1. Studio 热重载npm run dev 实时预览
  2. 检查帧:Studio 中拖动时间轴逐帧检查
  3. 性能:避免在组件内做重计算,用 useMemo
  4. 静态文件:放在 public/ 目录,用 staticFile() 引用

常见问题

Q: 视频渲染很慢?

  • 使用 --concurrency 增加并行数
  • 降低分辨率测试:--scale=0.5
  • 考虑 AWS Lambda 分布式渲染

Q: 字体不显示?

  • 使用 @remotion/google-fonts 或本地加载
  • 确保字体在渲染前已加载

Q: 视频素材不播放?

  • 检查视频编码格式(推荐 H.264)
  • 使用 替代 提升性能

参考资源

  • 官方文档:https://remotion.dev/docs
  • 模板库:https://remotion.dev/templates
  • GitHub:https://github.com/remotion-dev/remotion

More from this repository10

🎯
m10-performance🎯Skill

Optimizes code performance by identifying bottlenecks, measuring impact, and guiding strategic improvements across algorithm, data structure, and memory efficiency.

🎯
m14-mental-model🎯Skill

Applies the M14 mental model framework to enhance decision-making and strategic thinking through structured cognitive analysis.

🎯
m04-zero-cost🎯Skill

Guides developers in choosing zero-cost abstractions by analyzing type system constraints and performance trade-offs in Rust generics and traits.

🎯
meta-cognition-parallel🎯Skill

Performs parallel three-layer meta-cognitive analysis by forking subagents to simultaneously analyze language mechanics, design choices, and domain constraints, then synthesizing results.

🎯
unsafe-checker🎯Skill

Identifies and reviews unsafe Rust code patterns, FFI risks, and potential memory unsafety in Rust projects.

🎯
rust-skill-creator🎯Skill

Dynamically generates Claude skills for Rust crates, standard library modules, and documentation by extracting and processing technical details from specified URLs.

🎯
coding-guidelines🎯Skill

Provides comprehensive Rust coding guidelines covering naming conventions, best practices, error handling, memory management, concurrency, and code style recommendations.

🎯
rust-refactor-helper🎯Skill

Performs safe Rust refactoring by analyzing symbol references, checking conflicts, and applying changes across project files using LSP.

🎯
m03-mutability🎯Skill

Diagnoses and guides resolution of Rust mutability and borrowing conflicts by analyzing ownership, mutation patterns, and thread-safety requirements.

🎯
m05-type-driven🎯Skill

Explores and demonstrates type-driven development techniques in Rust, showcasing advanced type system features and pattern matching strategies.