- name
- manim-teaching-figures
- description
- 用 Manim 给大模型 / AI 基础设施课程画「会动的」教学图 —— 矩阵与线性变换、梯度下降与优化器轨迹、动量与梯度场、loss 地形与鞍点、注意力与 KV cache 的时间演化、高维不可视时的降维表示。产出是挂 loop 的无声 mp4,配静态图与页面图注使用。当用户说「画个动图」「这段用 manim 画」「梯度下降/动量/矩阵变换要动起来」「三蓝一棕那种图」「给课件加动画」,或者一格内容的增量只能来自「时间」或「第三维」时使用。也用于检查已有动画的循环接缝对不对得上。
# 用 Manim 画教学动画
> ⭐ **一句话认领它长什么样**:manim 原生深色(`#000000` 底、`BLUE #58C4DD` /
> `RED #FC6255` / `GREY #888888`、线宽 4),**每一支都无缝循环、页面上不给播放器**,
> 画面里的每一句话都在 `aria-label` 里复述一遍。
> —— 这三条是**身份**,不是偏好。
## 这个 skill 解决什么
一段教学动画真正难的**不是画出来,是画对且能循环播**。三件事反复出错:
1. **该不该做成动画** —— 不占「时间」也不占「第三维」的内容,静态 SVG 更好
2. **首尾接不接得上** —— 挂 `loop` 的片子首末帧对不上就每轮跳一下,
而**你播一遍是看不出来的**,得等它循环到第二遍
3. **有没有用对类** —— manim 自带 `Matrix` / `LinearTransformationScene` /
`VectorField`,拿 `Rectangle` 手摆格子是在重造轮子
## 环境
```
~/.venvs/manim/bin/manim Manim CE 0.21.0;LaTeX 齐全,Matrix / MathTex 可用
```
⛔ **manim 不能输出 SVG。** 静态图走项目自己的 SVG 基元,manim 只管会动的那部分。
## 房规(2026-09-18 松绑过一次,读完再动手)
⭐⭐⭐ 最初那版是「一个字都不放 + 15 秒 + 首尾同帧 + 数据当场算」。
做完六段之后被问:**「为啥没有 3b1b 那么 fancy?」**
答案不是能力,是**前两条在压表现力**:
| 原规矩 | 它砍掉了什么 |
|---|---|
| 一个字都不放 | **公式与图形同步变形** —— 3b1b 最强的手法,直接没了 |
| 15 秒 + 首尾同帧 | 一个概念只能铺 15 秒,还得留一段复位 |
⭐ 所以现在是这样:
### ⛔ 每次动手前先判这一句
**它缺的是「解释」还是「铺陈」?**
| 缺的是 | 那就 |
|---|---|
| **解释** —— 读者看得懂画面,但不知道这在说什么概念 | 加公式/字幕,**时长和 `loop` 都不动** |
| **铺陈** —— 一个动作太快,或者有起承转合讲不完 | 才放长到 20 秒以上,改 `controls` 不给 `loop` |
⭐⭐ **「不够 fancy」的解药是信息密度,不是时长。**
一段 15 秒循环片配一条公式,可能比拉成 30 秒更好看也更好懂。
**① 文字为讲解服务,不为装饰。**
可以放:`MathTex` 公式、`Text` 中文短句(走 Pango,**不需要 CJK LaTeX 宏包**)。
⛔ 但多出一条**硬义务**:视频里的字**选不中、读屏读不到、搜索搜不到** ——
所以 `aria-label` 必须把画面里出现过的**每一句话都复述一遍**。
⛔ 还有:别把静态图已经说清的话再抄一遍进画面。
**② 静态图排在动画上面** —— 视频加载不出来也要能读懂。(没松)
**②b ⭐⭐⭐ 配色:manim 自带的原生那套,<u>这就是这个 skill 的样子</u>。**
> 2026-09-19 拍板,2026-09-20 复核后确认为**默认,不再讨论**:
> 「现在你这种**黑色背景的图画得非常好**,以后经常用 ——
> 就要这种**跟三蓝一棕的原始用法一模一样**的画图方式。」
⛔ **新开一支动画时,这一节不用再判断、直接照抄。**
要偏离(比如某个场合非要亮底)得有具体理由,并且**做并排对照再定**。
```python
# ⛔ 不要写 self.camera.background_color —— 默认 #000000 就是作者的用法
from manim import BLUE, RED, GREEN, YELLOW, WHITE, GREY, GREY_B
# BLUE #58C4DD RED #FC6255 GREEN #83C167 YELLOW #F7D96F
# GREY #888888 GREY_B #BBBBBB 线宽默认 4,别往下调
```
实证(扒 3b1b 神经网络系列那三个源文件):`background_color` **出现 0 次**,
用色频率 **YELLOW 103 / WHITE 101 / BLUE 78 / RED 70 / GREEN 44**。
⛔ 三条配套的坑:
- **黄不能当正负号用。** 他讲分量符号那一幕用的也是蓝/红;黄是**强调色**不是语义色。
- **黑底的辅助线要比白底亮。** `GREY_D #444444` 在黑底上整幕消失,得用 `GREY #888888`。
对比度是**相对背景**的,不是绝对的。
- ⛔⛔ **`rate_func` 要分两种 play,不能一刀切**(2026-09-19 实测打脸,原来这里写的是
「不要显式传 linear,匀速是机器感的来源」—— **那条一刀切是错的**):
| play 在动什么 | `rate_func` |
|---|---|
| **一个物体**(`mob.animate.*`、`FadeIn`、`Write`、`Indicate`、镜头推拉…) | `smooth`(默认,别显式写 linear) |
| **一个时钟**(`ValueTracker.animate.set_value`,后面挂 `always_redraw` / `clock()`) | ⭐ **必须显式 `rate_func=linear`** |
⭐⭐ 为什么:`smooth` 把 play **两端的速度压到零**。动物体时那是自然的收势;
**驱动时钟时那是让时间停摆。** 而本 skill 的主力写法(`ValueTracker` + `always_redraw`)
几乎每个 play 都在驱动时钟 —— 一刀切换成 smooth,等于让画面在每个段界卡住。
实测(`anim-descend`,段界在 4.6 秒,量 50 ms 内整帧平均变化):
```
t=3.6 … 4.6 变化 0.00 ← 段界前整整一秒画面完全静止
t=5.2 … 6.0 变化 0.0x ← 下一段开头又是慢启动
```
⛔ 复位段尤其不能碰:`clock()` 倒放、三角波对称,**都建立在时钟匀速上**。
⭐ 判据(这一轮学到的):**「作者怎么用」是证据,不是结论。**
先看约束对不对得上 —— 他的默认是为**全屏播放的独立视频**调的。
本项目的视频嵌在亮底网页里,一度因此选了白底;对照之后仍然选原生深色,
但那是**看过并排对照才定的**,不是照搬。
**③ ⭐⭐⭐ 全部都循环,一律不给播放器。**(2026-09-20 现场拍板,**这一条收紧了**)
> 原话:「**这种能放一个动图搞定的东西,就不要放一个视频啦。**」
```html
<video src="…" autoplay loop muted playsinline> <!-- 唯一的写法 -->
```
⛔ **`controls` 这个出口没有了。** 之前的规矩是「>20 秒的叙事片给 controls 不给 loop,
因为它首尾本来就不一样」—— **那是把「我没做复位」说成了「它不该循环」。**
⭐⭐ **24.6 秒的叙事片照样能循环**,做法就一句话:
> **结尾把画面恢复成第 0 帧的样子。**
```python
# ⭐ 通用收尾模板:清干净,再摆一个跟第 0 帧一模一样的静态件
_keep = [m for m in self.mobjects if m is not curve] # curve 是第 0 帧就在的
self.play(*[FadeOut(m) for m in _keep], run_time=0.9)
self.remove(*_keep) # ⛔ FadeOut 之后补一刀,别留残影
self.add(Dot(P(TRAJ[0][0]), radius=0.13, color=GY2_)) # = 第 0 帧那颗球
self.wait(0.6)
```
⛔ **两个真栽过的坑**(2026-09-20,`anim-momentum`,一次改对花了三版):
| 做法 | 结果 | 不一致度 |
|---|---|---|
| 只留那条曲线 | ⛔ **首帧那颗灰球没了** | 22.9% —— 跟正常循环同档,**完全看不出来** |
| `tracker.set_value(0)` + `self.add(球)` | ⛔ 那颗球**中途已经被 `self.remove` 换成静态点**,唤不回来 | 27.7% —— 还是看不出来 |
| 清干净 + 摆一个静态件 | ✅ | **15.7%** |
⭐ 判据一:**复位不要去唤醒一个已经被移走的 `always_redraw`,直接清干净再摆静态件。**
确定性比聪明重要 —— 你很难记清一个 mobject 在二十几个 play 之后还在不在场上。
⭐⭐ 判据二:**首尾一致<u>别只看那个百分比</u>,一定要看拼接图。**
上面两次都是**图**抓到的,数字一次都没报警 ——
**一颗小球的有无,在整幅画的平均差异里几乎不占面积。**
⛔ baseline 里那个 `"loop": false` 现在应该是**空的**。真要用它,
在 note 里写清「为什么这一支做不到复位」—— 而不是「它是叙事片所以不循环」。
**④ 数据全部当场算** —— 等高线真二分、轨迹真迭代、精度边界真从位模式读。
⛔ **这条永远不松**,而且**参数也算数据**:与其手调到「看着对」,
不如把「要满足的条件」写成筛子去搜一遍(见 `references/traps.md` §3.4)。
### ⛔ 多步过程类动画(环、流水线、一步一步的算法)
2026-09-25 现场纠正,原话:「你怎么 A 往右,然后剩下都往左。它应该是环形的,大家都往右发,到头了转一圈回来……
第一步慢点,还没看明白呢,停一下再跳第二步。」
- **运动方向跟算法方向一致**:绕回的那一块也往右走(出画面右边 → 底下车道 → 从左边进来),不许横穿画面往回飞。
- **不拿「直接飞到终点」的逻辑视图冒充过程**:有真实步骤的,就按步骤走。
- **每一步落地后停住**(飞约 1.8 秒、停约 2 秒),字幕写「第 s 步完成:……」。
- **停顿点写成数据**:每次停顿记 `self.renderer.time`,渲染完落一份 `steps/<Scene>.json`;
页面把它挂成 `<video data-pauses="…">`,播到就暂停、出「下一步」按钮。这是 `loop` 之外唯一被允许的播放控制。
- **手工模式第一步也等人点**:一上来停在第 0 帧、按钮写「开始」;播完绕回开头也停住;配一个「⟲ 重来」。
- 落点被占就换布局(例:每张卡分「寄出」「收到」两列),别让块叠在一起。
## 流程
### 第 0 步 · 先问它该不该动
⭐⭐ **判据:增量只能来自「时间」或「第三维」。** 两个都不占就别做视频。
| 该动 | 不该动 |
|---|---|
| 「你得重复 N 遍」—— 重复本身就是时间 | 并列对比两种方案 |
| 「一步步降下去」—— 过程就是内容 | 结构图 / 数据通路 |
| 「这个矩阵把空间掰成什么样」 | 一组数字、一张口径表 |
| 曲面要转着看才知道是鞍还是碗 | 流程先后(箭头就够) |
### 第 1 步 · 写场景
主力写法是 **`ValueTracker` + `always_redraw`**:把「第 t 秒画面长什么样」
写成纯函数,让一个 tracker 从 0 走到 T。
⭐ 为什么不用一串 `self.play`:**每一帧都能由 t 算出来**,于是
「首尾同一帧」这类不变量可以被直接验证 —— 拼出来的时间线做不到。
复位段**只在一个地方定义**(把时钟拨回 0,或让它倒着走回去),
不要在每个绘制函数里各写一份 `if` —— 见 `references/traps.md` §1.3。
选类去查 **[`references/toolbox.md`](references/toolbox.md)**:
按「要讲什么」列了对应的类、标了哪些实测过,
并给了 `LinearTransformationScene` 改成亮底的完整写法。
### 第 2 步 · 草稿迭代
```bash
scripts/render.sh <脚本.py> <SceneName> /tmp/draft.mp4 --draft
```
**4–8 秒**出一版 480p。构图、时序、接缝在草稿上全看得出来 ——
一次正式渲染要 2–5 分钟,够你在草稿上试二十轮。
### 第 3 步 · 正式渲染 + 查接缝
```bash
scripts/render.sh <脚本.py> <SceneName> path/to/out.mp4
```
渲染 → 压到 960 宽 crf 30 → 查首尾接缝,一条命令走完。
十几秒的动画通常落在 50–300 KB,可以直接进仓库。
⛔ 放后台跑,**并且配一个 watch-task** —— turn 结束你就不在了,
「渲完我再报」说出口即落空。
### 第 4 步 · ⭐ 看那张拼接图
```
/tmp/loopdiff-<名字>.png 上=首帧,下=末帧
```
**必须真的看。** 工具报的那个百分比**分不开**「末帧是另一幕」和
「末帧偏了一像素」—— 实测同一个 51.2%,一次是差着整整一幕,
一次是转多了半度。
这就是它做成**基线回归**而不是阈值判定的原因:只问「有没有比记录的更糟」,
判定权在人,判完把结论写进 baseline 的 `note`。
```bash
scripts/check-loop.py --media <放 mp4 的目录> # 查
scripts/check-loop.py --media <目录> --update # 人眼判过了,登记
```
新片子没有基线会报红,逼你看一眼再登记。
⭐ 建议把这一句接进项目的 build(只读 mp4 不重渲,四支片子 1.4 秒)——
否则「构建全绿」永远只覆盖单帧的几何与文字,**盖不到产物的时间维度**。
### 第 5 步 · 嵌页面
```html
<figure class="fbox fwide" id="anim-xxx">
<video src="media/xxx.mp4" autoplay loop muted playsinline
aria-label="……这段在演什么,给读屏用……"></video>
<figcaption>……分幕讲清楚论点……
<span class="sub">(15 秒无声循环,Manim 渲染。)</span>
</figcaption></figure>
```
⛔ 图注里那句「N 秒」**会过期** —— 改了时长它不会自己变。
`check-loop.py` 带了这一项体检,拿实测时长去对(容差 0.6 秒)。
## ⛔ 版权:借思路,不抄代码
**manim 本身是 MIT**,随便用。但 **`3b1b/videos` 那个仓库里的内容是
CC BY-NC-SA 4.0**(署名 / 非商业 / 相同方式共享)—— 跟 manim 的 license
**完全不是一回事**,抄代码或画面会把它传染给你的仓库。
⭐ 可以借的是**思想**:叙事顺序、编码手法(颜色表符号、长度表大小)、
「先一维讲通 → 升到二维 → 承认高维想不出来 → 于是不再画空间」这类教学结构。
借了就在图注里写明出处。
## 出错了先看这里
**[`references/traps.md`](references/traps.md)** —— 全部来自真实事故:
循环接缝判据的三次误判、`FadeOut` 会把东西移出 scene、`Scene.add` 追加到最上层、
`wait()` 不一定驱动 updater、偶函数导致两个谷必然等深、
「断言挡住时先看它挡的是什么」、渲染成本实测。
## 文件
| | |
|---|---|
| `scripts/render.sh` | 渲染 → 压缩 → 查接缝,一条命令 |
| `scripts/check-loop.py` | 首尾接缝基线回归 + 图注秒数体检,可单独接进 build |
| `references/toolbox.md` | 按教学主题选类;哪些实测过、哪些没有 |
| `references/traps.md` | 踩过的坑 |
> 📖 课程仓库 `Courses/构建手册/10-Manim动画.md` 是这份房规的**公开版**(外部读者看不到本 skill)。
> **改了这里的房规,同步改那一篇**,否则就是「一条规矩复制 N 份,N 份会过期」。
>
> 课程仓库 `Courses/tools/manim/` 下另有一份**钉在那个项目上的
> `check-loop.py` 副本**(已接进它的 `build-all.sh`)。两份是故意的:
> 那边是部署好的守卫,这边是拿去装进新项目的模板。
> 改了这边,想一下要不要同步过去。
View on GitHub