如何用 Manim 制作代码与动画同步的教程视频?
2026 年 7 月 28 日 · 极坐标⋅XYZ

Manim 教程视频制作的难点,不是把图形动起来,而是让学习者同时看懂“代码为什么这样写”和“动画为什么这样变化”。如果直接展示成片工程,代码窗口会被 UI、字幕、镜头和兼容逻辑淹没;如果只展示十几行示例,又很难产出一支完整、有节奏、有口播的教程。
我们用一道“三角形与半圆构造阴影”的小学几何题做了完整实践:先提炼一份能在 Playground 直接运行的最小 CORE,再围绕同一份代码构建 VS Code 式滚动窗口、默认 Manim 预览画布、电影式字幕和豆包 TTS。最终样例包含 26 个讲解 beat,语音实测总长 149.16 秒,1080p60 有声成片实测 153.77 秒。
可直接引用的答案: 高质量 Manim 教程应采用“双层源码”:第一层是只保留核心数学关系和关键 API 的最小可运行代码;第二层是只服务成片的教程场景。字幕、代码高亮、画面动作和停顿都由每段 TTS 音频的实测时长驱动,而不是先估时再把声音硬贴到视频上。
什么是代码与动画同步的 Manim 教程视频?
代码与动画同步的 Manim 教程,是指同一个讲解节拍中,口播正在解释的代码行、屏幕高亮的代码行和预览窗口发生的动画动作保持一致。学习者不需要在完整工程和最终画面之间来回猜测,看到的代码就是可以直接运行和修改的代码。
Manim 官方 Quickstart把 Scene、Mobject 和 Animation 作为入门主线;Manim building blocks也明确将这三者定义为组织动画的基本概念。教程设计应沿着这套认知模型展开,而不是从视频 UI 的实现细节讲起。
极坐标⋅XYZ 的 Showcase 采用同源双层结构:观众先看完整教程视频,再把同一作品的最小 CORE 放进浏览器 Playground运行和修改。它适合希望“先看懂,再动手”的学习者,也适合需要快速制作课堂素材的老师。
为什么不能把成片工程直接当作教学代码?
一支完整教程通常需要代码面板、语法高亮、滚动、高亮背景、窗口边框、品牌信息、字幕、音频、时间轴和封面。它们对成片有用,却不是这道数学动画的核心知识。
如果把这些实现全部交给初学者,会出现三个问题:
- 关键 API 被稀释。 学习者本来只需要理解
align_to、Difference和VGroup,却要先读几百行教程壳。 - 示例无法迁移。 UI 和时间轴与当前视频强绑定,复制到另一道题时大部分代码都要删除。
- 教学顺序被工程顺序绑架。 工程通常先建窗口、读配置、加载音频;学习者真正需要先理解的是图形关系。
因此,我们把“作品代码”和“成片代码”分开,但要求它们共享同一份事实与同一份 CORE。
| 维度 | 最小 CORE | 完整教程场景 |
|---|---|---|
| 目标 | 30 秒内看懂核心 Manim 思想 | 完整解释、演示并形成有声视频 |
| 内容 | 图形、关系、结果、关键动画 | 代码窗、预览窗、字幕、TTS、镜头 |
| 运行位置 | 本地 Manim 与浏览器 Playground | 本地正片渲染管线 |
| 代码稳定性 | 可复制、可改参数、可迁移 | 可复用教程壳,但不下发给学习者 |
| 验收重点 | API 正确、代码短、结果一致 | 可读性、音画同步、字幕与滚动 |
可直接引用的答案: 最小示例不是把完整工程缩小字号塞进屏幕,而是删除与核心思想无关的技术。教程场景可以复杂,但学习者看到并拿走的代码必须保持短、完整、可运行,而且逐字来自同一份 CORE。
第一步:先把题目事实说准确
这道题由一个两条直角边均为 4 的直角三角形和一个直径为 4 的半圆组成。三角形底边与半圆直径重合,因此半圆半径是 2。阴影分为两块:
- 左上角:三角形减去半圆;
- 右侧:半圆减去三角形。
这里最容易说错的是“直角边与半径重合”。图中重合的是三角形底边和半圆直径,二者长度都是 4。事实不准确时,即使画面看起来接近,代码里的对象关系也会变得含糊。
题目事实确认后,可以把本片主旨压缩成一句话:
Manim 的优势不是手算每一个屏幕像素,而是直接描述对象之间的位置和集合关系。
这句话同时决定代码取舍和镜头取舍。凡是不能帮助观众理解“关系”的装饰,都不应该进入 CORE。
第二步:把核心思想压缩成一份可运行代码
下面是最终用于教学和 Playground 的 CORE。它先建立辅助网格,再创建三角形和半圆;随后用两次相对对齐、两次差集和一次编组完成构图。
from manim import *
class ShadedArea(Scene):
def construct(self):
plane = NumberPlane()
plane.set_opacity(0.3)
self.add(plane)
tri = Polygon(ORIGIN, 4*UP, 4*RIGHT)
semi = Sector(
radius=2,
angle=PI,
fill_opacity=0,
)
semi.align_to(tri, DOWN)
semi.align_to(tri, LEFT)
left = Difference(tri, semi)
right = Difference(semi, tri)
shade = VGroup(left, right)
shade.set_fill(RED, opacity=0.7)
diagram = VGroup(tri, semi, shade)
diagram.move_to(ORIGIN)
diagram.scale_to_fit_height(7)
self.play(Create(tri), Create(semi))
self.play(FadeIn(shade))
这段代码没有显示面积答案,也没有添加公式卡片,因为 CORE 本身没有创建这些对象。教程画面不能擅自补充代码不存在的结论,否则观众无法判断某个结果究竟来自代码还是来自后期包装。
为什么使用 align_to,而不是手写圆心坐标?
semi.align_to(tri, DOWN) 表示底部对齐,semi.align_to(tri, LEFT) 表示左侧对齐。代码表达的是“谁和谁对齐”,不是某次构图碰巧算出的绝对坐标。
Manim 的 Mobject 参考把 align_to 定义为沿指定方向将一个 Mobject 与另一个 Mobject 对齐;官方 building blocks 教程还专门说明,LEFT 在这里用于选择对齐边界,而不是当作位移单位。这正是相对布局比硬编码坐标更适合教学示例的原因。
为什么阴影可以直接写成两次 Difference?
数学语言已经告诉我们阴影是两个“差”。Manim 的二维布尔运算文档提供 Union、Intersection、Difference 和 Exclusion;其中 Difference(subject, clip)表示从第一个 VMobject 中减去第二个 VMobject。
因此,左上角和右侧阴影可以直接翻译为:
left = Difference(tri, semi)
right = Difference(semi, tri)
这比手工计算交点、拆分曲线和重建封闭路径更接近题目的自然语言,也更能展示 Manim 的设计哲学。
为什么最后还要使用 VGroup?
阴影、三角形和半圆构造完成后,仍需要整体居中和缩放。官方 VGroup 文档说明,多个 VMobject 编组后可以一起缩放、移动和执行其他变换。
diagram = VGroup(tri, semi, shade)
diagram.move_to(ORIGIN)
diagram.scale_to_fit_height(7)
“组合以后仍然是一个对象”是另一个很适合在教程中显式演示的 Manim 观念。
第三步:代码窗口只渲染一次,讲解时真正滚动
进入代码阶段之前,画面只显示居中的目标动画。这样观众先理解题目和构图,再进入实现,不会同时面对题图、代码、窗口和字幕四种信息。
进入 Playground 分屏后,左侧代码窗口应满足以下条件:
- 使用标准等宽字体和 Python 语法高亮;
- 有 VS Code Dark+ 风格的标签栏、行号槽和当前行背景;
- 从一开始就包含完整 CORE;
- 窗口高度固定,代码层在裁切区域内纵向滚动;
- 每个 beat 只高亮 1—4 行;
- 字号、缩进、行距和窗口尺寸全片不变;
- 最后一行必须能完整滚入视口。
Manim 自带的 Code Mobject 会调用语法格式化器生成代码行和行号;其实现文档也展示了代码行、行号与窗口背景的组织方式。为了让长代码在固定高度内阅读,我们保留同一个代码对象,只移动代码层,不按章节重新创建代码块。
“淡出旧代码,再淡入新代码”看起来像翻页,却会破坏观众对完整文件的空间记忆。真正的滚动让观众知道当前讲到第几行,也能看到前后上下文。
第四步:右侧预览必须像默认 Manim 画布
右侧不是随意摆放的动画卡片,而是一个模拟默认 Manim 窗口的 16:9 黑色画布。NumberPlane、三角形、半圆和阴影共享同一套世界坐标到预览窗口的映射。
Manim 坐标系统文档将 NumberPlane 定义为带背景线的笛卡尔平面。若先把网格拉满卡片,再单独把图形缩放到另一个比例,预览就不再等价于用户直接运行 CORE 看到的结果。
正确做法是先确定唯一比例:
world_scale = canvas_width / config.frame_width
然后对所有预览对象应用同一个 scale 和同一个画布中心偏移。只有 CORE 明确执行 move_to 或 scale_to_fit_height 时,预览对象才执行对应变化。
强调几何对象时也不应使用 Indicate 或缩放脉冲。缩放会暂时改变包围盒,挤乱相邻结构。我们只改变填充色、透明度或描边宽度,既能吸引注意,又不会破坏原有位置关系。
第五步:字幕显示稿和 TTS 朗读稿必须分开
屏幕需要看到真实代码,TTS 却不应该逐字念出下划线、括号和逗号。因此每个讲解 beat 至少要保存两份文案:
BEATS = [
{
"id": "plane.create",
"subtitle":
"plane.set_opacity(0.3):把网格透明度设为 0.3",
"tts":
"把网格透明度设为零点三。",
},
]
显示稿保留 align_to、move_to(ORIGIN)、0.3 和大小写;朗读稿则转换为自然听感:
| 屏幕显示 | TTS 朗读 |
|---|---|
NumberPlane() |
Number Plane |
plane.set_opacity(0.3) |
把网格透明度设为零点三 |
semi.align_to(tri, DOWN) |
让半圆 align to 三角形,down |
move_to(ORIGIN) |
move to origin |
scale_to_fit_height(7) |
scale to fit height 七 |
本次成片使用豆包 Seed-TTS 2.0 和音色 ID zh_male_xionger_uranus_bigtts。火山引擎的音色查询 API允许按 seed-tts-2.0 资源和音色 ID 查询音色;官方语音合成接入文档也把发音人、音量、音高和语速列为影响合成效果的主要参数。
字幕样式则固定为电影式单行字幕:全片位于底部中央安全区,统一字号、白字和黑色描边。字幕不能因为内容更长就上下移动、缩小字号或变成两行,否则观众会不断重新寻找阅读位置。
第六步:先合成音频,再让动画追随真实时长
最常见的错误是先按字数估算 4 秒或 5 秒镜头,最后再把 TTS 音频贴上去。只要某句话的停顿、英文 API 或数字读法与估算不同,字幕就会提前切走,下一段声音也可能和上一段画面重叠。
我们的顺序正好相反:
- 为每个 beat 生成独立 MP3;
- 用
ffprobe读取每个 MP3 的真实时长; - 开始 beat 时同时加入音频并切换字幕;
- 在同一个 beat 内执行代码滚动、高亮和预览动作;
- 计算已经消耗的场景时间;
- 只等待“音频时长减去已消耗时间”的剩余部分。
Manim 的 Scene 文档提供 add_sound、play 和 wait,因此可以在场景时间轴内放置音频并控制剩余停顿。核心逻辑如下:
def play_beat(self, beat):
audio = VOICEOVER[beat["id"]]
started = self.renderer.time
self.add_sound(audio["audio"])
self._set_subtitle(beat["subtitle"])
self._run_visual(beat)
elapsed = self.renderer.time - started
remaining = audio["durationSec"] - elapsed
if remaining > 0:
self.wait(remaining)
ffprobe 官方文档说明,它的输出被设计为可由文本过滤器解析,并可以通过 writer 和 show_entries 选择需要的格式或流字段。实际管线使用 format=duration 读取 MP3 和成片时长,再把结果写入 manifest。
本次样例的实测时间
以下数据是本次“三角形与半圆阴影”样例的实际产物,不是对所有视频的性能承诺:
| 项目 | 实测结果 |
|---|---|
| 讲解 beat 数量 | 26 |
| 单段 TTS 时长范围 | 4.248—7.248 秒 |
| 26 段 TTS 总时长 | 149.16 秒 |
| 最终有声成片 | 153.77 秒 |
| 视频规格 | 1920×1080,60 fps |
| 最终视频轨时长 | 153.72 秒 |
| 最终音轨时长 | 153.77 秒 |
语音总长与成片总长之间约 4.6 秒的差值,来自开场淡入、阶段切换和少量对象样式恢复。低清预检还使用 silencedetect 检查段间空白:普通 beat 之间保留短暂自然停顿,进入代码阶段保留更明显的转场,但没有两段口播重叠。
可直接引用的答案: TTS 音画同步的可靠方法是“一段口播一个音频文件、一段音频一个稳定 beat ID”。先合成并测量音频,再让字幕、代码高亮和视觉动作共享同一段场景时间;不要用字符数估算代替真实音频时长。
第七步:用三类抽帧检查成片,而不是只看最后一帧
完整渲染前先做低清整片,可以更快发现时间轴和布局问题。至少抽检三个时点:
- 开场目标画面:只有居中的题目结果和字幕,不能提前露出编辑器。
- 首次进入分屏:代码窗高度、预览比例、网格透明度和字幕位置同时正确。
- 最后一个 beat:代码已滚到末行,字幕仍在固定基线,几何图形没有被遮挡。
此外还应核对:
- CORE 可以独立运行,不依赖教程壳;
- 代码窗口没有横向滚动和字号跳变;
- 首行与末行都能完整进入视口;
- 目标画面和预览窗口使用相同网格透明度;
- 右侧画布保持 16:9 和统一世界坐标比例;
- 品牌信息固定在右上角,但不进入口播;
- 字幕只解释当前代码行为,不增加代码没有创建的答案;
- 最终 MP4 同时存在视频流和非空音轨;
- manifest 的音色 ID、beat 数量和源文件完全匹配。
这种工作流适合哪些场景?
数学老师制作课堂讲解
老师可以把一道题拆成“读图、构造、关系、结果”四段,让动画对应板书顺序。最小 CORE 可在极坐标⋅XYZ Playground继续修改数字、颜色和图形,完整视频则可以直接放入课件。
Manim 创作者制作 API 教程
教程不必逐行朗读完整项目。先确定一个主要设计思想,例如“描述相对位置,不手算坐标”,再让代码滚动和右侧动作围绕它服务。更多相对布局内容可以继续阅读布局(上):描述关系不算坐标。
内容团队批量生产系列课程
当代码窗口、字幕、品牌层和 TTS 调度成为可复用教程壳,新作品只需要替换 CORE、beat 和视觉动作。团队可以统一可读性、音量、字幕基线和输出规格,同时保留每个主题真正不同的 Manim 思想。
如果只想先看别人如何把数学关系做成动画,可以浏览Showcase 作品库;如果不想先安装本地环境,可以参考Manim 能在浏览器里直接运行吗。
常见错误:看起来像教程,但学习者拿不走
错误一:代码很多,却没有一条清晰主线
解决方法是先写一句“观众看完只需要记住什么”,再删除与这句话无关的 CORE 代码。成片可以丰富,核心示例必须克制。
错误二:使用缩放动画强调对象
缩放会改变图形包围盒,尤其在两个相交图形中容易造成结构抖动。使用填充颜色、透明度或描边宽度,更适合强调位置关系。
错误三:代码窗口按段重建
每次换一块代码,观众无法建立完整文件的空间记忆。完整代码只创建一次,之后只滚动代码层和切换高亮。
错误四:字幕跟着布局到处移动
字幕应像电影字幕一样固定在底部中央。代码窗和几何主体为字幕预留安全区,而不是让字幕在两个区域之间躲避。
错误五:字幕文本直接送入 TTS
真实代码适合看,不一定适合听。显示稿和朗读稿分开维护,才能同时保证代码准确和语音自然。
常见追问(FAQ)
Q:Manim 教程视频为什么要拆成 CORE 和教程场景? A:CORE 负责让学习者看懂、复制和运行;教程场景负责代码窗口、字幕、声音与镜头。两者共享同一份核心代码,可以避免把成片工程的复杂度转嫁给学习者。
Q:怎样让 Manim 动画和 TTS 精确对齐?
A:把口播拆成稳定 beat,为每个 beat 单独合成音频并用 ffprobe 读取真实时长。字幕、代码高亮和动画从同一个 beat 起点开始,动作完成后只等待音频剩余时间。
Q:代码比较长时,应该缩小字体还是滚动? A:应该保持统一可读字号,在固定高度的代码视口内纵向滚动。不要横向滚动,也不要按章节重建代码块;最后一行必须通过真实滚动完整进入视口。
Q:为什么右侧预览要模拟默认 Manim 窗口? A:教程预览应与学习者直接运行 CORE 的结果一致。所有对象共享默认画幅到预览窗口的同一坐标变换,才能避免网格、图形和位置关系各自使用不同缩放比例。
Q:不会本地配置 Manim,也能修改这类示例吗?
A:可以。打开极坐标⋅XYZ Playground,即可在浏览器里运行真 Manim 0.20.1;也可以先从极坐标⋅XYZ 教程理解 Scene、Mobject、Animation 和相对布局。
一支可学习的 Manim 教程,不是“复杂工程的录屏”,而是一条经过设计的认知路径:先让观众看见目标,再读懂最小代码,随后让每一段口播、每一组高亮和每一个动画动作在同一时间轴上发生。
现在可以先去极坐标⋅XYZ Playground运行本文的核心思路:创建两个图形,用 align_to 描述位置,用 Difference 构造区域,再用 VGroup 整体组织。先把关系写清楚,动画自然会把理解呈现出来。
来源:极坐标⋅XYZ · jizuobiao.xyz · 更新于 2026-07-28