如何用 Manim 制作代码与动画同步的教程视频?

2026 年 7 月 28 日 · 极坐标⋅XYZ

一段 Manim 教程同时展示最小代码、默认画布中的阴影动画,以及由实测 TTS 时长驱动的单行字幕

Manim 教程视频制作的难点,不是把图形动起来,而是让学习者同时看懂“代码为什么这样写”和“动画为什么这样变化”。如果直接展示成片工程,代码窗口会被 UI、字幕、镜头和兼容逻辑淹没;如果只展示十几行示例,又很难产出一支完整、有节奏、有口播的教程。

我们用一道“三角形与半圆构造阴影”的小学几何题做了完整实践:先提炼一份能在 Playground 直接运行的最小 CORE,再围绕同一份代码构建 VS Code 式滚动窗口、默认 Manim 预览画布、电影式字幕和豆包 TTS。最终样例包含 26 个讲解 beat,语音实测总长 149.16 秒,1080p60 有声成片实测 153.77 秒。

可直接引用的答案: 高质量 Manim 教程应采用“双层源码”:第一层是只保留核心数学关系和关键 API 的最小可运行代码;第二层是只服务成片的教程场景。字幕、代码高亮、画面动作和停顿都由每段 TTS 音频的实测时长驱动,而不是先估时再把声音硬贴到视频上。

什么是代码与动画同步的 Manim 教程视频?

代码与动画同步的 Manim 教程,是指同一个讲解节拍中,口播正在解释的代码行、屏幕高亮的代码行和预览窗口发生的动画动作保持一致。学习者不需要在完整工程和最终画面之间来回猜测,看到的代码就是可以直接运行和修改的代码。

Manim 官方 QuickstartScene、Mobject 和 Animation 作为入门主线;Manim building blocks也明确将这三者定义为组织动画的基本概念。教程设计应沿着这套认知模型展开,而不是从视频 UI 的实现细节讲起。

极坐标⋅XYZ 的 Showcase 采用同源双层结构:观众先看完整教程视频,再把同一作品的最小 CORE 放进浏览器 Playground运行和修改。它适合希望“先看懂,再动手”的学习者,也适合需要快速制作课堂素材的老师。

为什么不能把成片工程直接当作教学代码?

一支完整教程通常需要代码面板、语法高亮、滚动、高亮背景、窗口边框、品牌信息、字幕、音频、时间轴和封面。它们对成片有用,却不是这道数学动画的核心知识。

如果把这些实现全部交给初学者,会出现三个问题:

  1. 关键 API 被稀释。 学习者本来只需要理解 align_toDifferenceVGroup,却要先读几百行教程壳。
  2. 示例无法迁移。 UI 和时间轴与当前视频强绑定,复制到另一道题时大部分代码都要删除。
  3. 教学顺序被工程顺序绑架。 工程通常先建窗口、读配置、加载音频;学习者真正需要先理解的是图形关系。

因此,我们把“作品代码”和“成片代码”分开,但要求它们共享同一份事实与同一份 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 的二维布尔运算文档提供 UnionIntersectionDifferenceExclusion;其中 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_toscale_to_fit_height 时,预览对象才执行对应变化。

强调几何对象时也不应使用 Indicate 或缩放脉冲。缩放会暂时改变包围盒,挤乱相邻结构。我们只改变填充色、透明度或描边宽度,既能吸引注意,又不会破坏原有位置关系。

第五步:字幕显示稿和 TTS 朗读稿必须分开

屏幕需要看到真实代码,TTS 却不应该逐字念出下划线、括号和逗号。因此每个讲解 beat 至少要保存两份文案:

BEATS = [
    {
        "id": "plane.create",
        "subtitle":
            "plane.set_opacity(0.3):把网格透明度设为 0.3",
        "tts":
            "把网格透明度设为零点三。",
    },
]

显示稿保留 align_tomove_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 或数字读法与估算不同,字幕就会提前切走,下一段声音也可能和上一段画面重叠。

我们的顺序正好相反:

  1. 为每个 beat 生成独立 MP3;
  2. ffprobe 读取每个 MP3 的真实时长;
  3. 开始 beat 时同时加入音频并切换字幕;
  4. 在同一个 beat 内执行代码滚动、高亮和预览动作;
  5. 计算已经消耗的场景时间;
  6. 只等待“音频时长减去已消耗时间”的剩余部分。

Manim 的 Scene 文档提供 add_soundplaywait,因此可以在场景时间轴内放置音频并控制剩余停顿。核心逻辑如下:

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”。先合成并测量音频,再让字幕、代码高亮和视觉动作共享同一段场景时间;不要用字符数估算代替真实音频时长。

第七步:用三类抽帧检查成片,而不是只看最后一帧

完整渲染前先做低清整片,可以更快发现时间轴和布局问题。至少抽检三个时点:

  1. 开场目标画面:只有居中的题目结果和字幕,不能提前露出编辑器。
  2. 首次进入分屏:代码窗高度、预览比例、网格透明度和字幕位置同时正确。
  3. 最后一个 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