从零玩转 Manim(2):阶段 1 实战 ------ 11 个例子写出第一个"圆→方→三角"变身动画
目录
- 阶段 0:环境与定位(5 分钟)
- 阶段 1.1:Scene ------ 一切动画的舞台
- 阶段 1.2:Mobjects ------ 万物皆数学对象
- 阶段 1.3:Creation Animations ------ 物体如何登场
- 阶段 1.4:Animations ------ 如何让物体动起来
- 产出:圆 → 方 → 三角变身动画
- 我踩过的 5 个坑
- 下一篇预告
阶段 0:环境与定位(5 分钟)
三件套 + pip install manim(上篇已详述)。这里只补充一个实战必踩的配置:
macOS 项目根目录有 SIP 限制,manim 默认写 media/ 会失败。解法是项目根放一个 manim.cfg:
ini
[CLI]
quality = low_quality
preview = True
[output]
media_dir = /tmp/manim_media
之后所有渲染都进 /tmp/manim_media/videos/,稳。
阶段 1.1:Scene ------ 一切动画的舞台
核心认知 :Scene = 画布 + 时间线。每个动画必须写在一个继承 Scene 的类的 construct(self) 方法里。
4 个核心动作(必背)
| 方法 | 用途 | 时间 |
|---|---|---|
self.play(...) |
推进时间,播放动画 | 默认 1 秒 |
self.add(...) |
瞬时加入,无动画 | 0 秒 |
self.remove(...) |
瞬时移除 | 0 秒 |
self.wait(t) |
空白等待 t 秒 | t 秒 |
01_minimal.py ------ 一切从这里开始:
python
from manim import *
class MyScene(Scene):
def construct(self):
circle = Circle()
self.play(Create(circle))
self.wait(1)
渲染命令:manim 01_minimal.py MyScene
输出:/tmp/manim_media/videos/01_minimal/480p15/MyScene.mp4 ✅
把 4 个动作对照看
02_four_actions.py 把 play/add/remove/wait 都演示了一遍,特别演示了一个反直觉的点:先 add(circle) 再 play(Create(circle)) 不会重画 ------因为对象已经在场景里了,Create 是"渐进画出"动画,对已在的对象无效。
这是 ManimCE 最容易踩的坑之一:Create 只对"第一次出现"的对象画路径。
两个 Scene 子类
| 子类 | 何时用 | 关键能力 |
|---|---|---|
Scene |
普通 2D | 静态相机 |
ThreeDScene |
3D | set_camera_orientation(phi, theta) |
MovingCameraScene |
2D + 镜头运动 | self.camera.frame.animate 缩放/平移 |
最小 3D 例子(03_subclass.py):
python
class Minimal3D(ThreeDScene):
def construct(self):
self.set_camera_orientation(phi=75 * DEGREES, theta=-45 * DEGREES)
axes = ThreeDAxes()
sphere = Sphere(radius=1).set_color(BLUE)
self.add(axes, sphere)
self.wait()
phi 是从 z 轴俯视的角度,theta 是绕 z 轴旋转的角度------两个参数决定 3D 视角。
阶段 1.2:Mobjects ------ 万物皆数学对象
Mobject = Mathematical Object,屏幕上所有可见元素的基类。
继承树(记一个口诀)
scss
Mobject(根)
├── VMobject(向量化的,贝塞尔曲线) ← 90% 时间打交道的就是它
│ ├── Circle / Square / Polygon / Line / Arrow
│ ├── Text / MathTex / Tex
│ ├── Axes / NumberPlane
│ └── VGroup
├── ImageMobject(位图)
└── Group(任意类型混合)
口诀 :能用 set_fill / set_stroke 的都是 VMobject(除了 Text 用 Pango);不能的就是 ImageMobject 或 PMobject。
5 个最常用的 get_* 方法
| 方法 | 用途 |
|---|---|
get_center() |
整体定位 |
get_top()/bottom()/left()/right() |
边对齐 |
get_corner(UL/UR/DL/DR) |
角落布局 |
get_width()/get_height() |
对齐大小 |
get_start()/get_end() |
路径端点(用于 Line/Arrow) |
04_mobjects.py 验证坐标:
python
rect = Rectangle(width=4, height=2)
print(rect.get_corner(UL)) # array([-2., 1., 0.])
输出确认:UL: x=-2.00, y=1.00、UR: x=2.00, y=1.00------4×2 矩形的 4 个角确实在 (±2, ±1)。
避免共享引用:用 copy()
Python 赋值是引用传递。直接 same = rect 然后操作 same,原 rect 也会动 。永远用 copy():
python
rect_copy = rect.copy() # 副本,独立
rect_copy.set_fill(RED, opacity=0.3)
rect_copy.shift(RIGHT * 4)
方法链 ------ 一气呵成但别太长
python
circle = (Circle(radius=0.5, color=YELLOW)
.shift(LEFT * 3 + DOWN * 2)
.set_fill(YELLOW, opacity=0.5)
.rotate(PI / 4))
经验法则:3 步以上拆行更可读。
自定义 VMobject
05_custom_shape.py 演示两种定义方式:
python
# 1. 直线连接:set_points_as_corners
star = VMobject()
star.set_points_as_corners([UP, UP+RIGHT, RIGHT+DOWN*0.2, ...])
# 2. 平滑曲线:set_points_smoothly
heart = VMobject()
heart.set_points_smoothly([np.array([np.cos(t), np.sin(t)-0.3, 0])
for t in np.linspace(0, 2*PI, 50)])
这两招在手,你就能画任意 2D 形状。
阶段 1.3:Creation Animations ------ 物体如何登场
creation-animations.md 列出了 10+ 种"出场方式"。记住这个决策树:
scss
登场一个对象,先问:它是几何形状还是文字?
│
├── 几何形状(Circle/Square/Line/Arrow)
│ └── 默认用 Create() --- 沿路径渐变画出
│
├── 文字 / 公式
│ └── 用 Write() --- 模拟手写
│
├── 想要"低调出现"
│ └── 用 FadeIn() --- 透明度过渡
│
├── 想要"重点强调形状边界"
│ └── 用 DrawBorderThenFill() --- 先描边再填色
│
└── 想要"从某点/边冒出"
└── 用 GrowFromCenter / GrowFromPoint / GrowFromEdge
06_creation_kinds.py 一次性看 5 种
把 5 个相同大小的方块并排放,分别用 Create/Write/FadeIn/DrawBorderThenFill/GrowFromCenter 登场,肉眼对比差异。
特别值得记的是 FadeIn 的两个增强参数:
python
FadeIn(mob, shift=UP) # 边淡入边向上
FadeIn(mob, shift=LEFT) # 边淡入边向左
FadeIn(mob, scale=2) # 边淡入边放大
这是"低调出现但又有点动静"的最常用技巧。
出场/退场对称(黄金规则)
07_symmetry.py 演示了三组对称:
| 出场 | 退场 |
|---|---|
Create |
Uncreate |
FadeIn |
FadeOut |
GrowFromCenter |
ShrinkToCenter |
入场用什么,退场就用什么------这条规则看着废话,但 80% 的"看起来不舒服"的动画都违反它。
验证坑:Create 对已存在对象无效
08_create_already_there.py 故意先 self.add(c) 再 play(Create(c))------结果整个 3 秒视频完全静止。Play 计数 +1,但视觉上无事发生。
这就是为什么 Create 通常配合先 play 后构造的对象使用。
阶段 1.4:Animations ------ 如何让物体动起来
animations.md 的核心是 .animate 语法------这是 ManimCE 最常用的"动画制造机"。
.animate 的本质
.animate 不是一个新方法,它是一个属性。访问它返回的对象把"普通方法调用"变成"动画对象":
python
circle.shift(RIGHT) # 立即修改 circle(瞬时,0 秒)
circle.animate.shift(RIGHT) # 返回"shift 到 RIGHT"的动画(要在 play 里播,默认 1 秒)
记住这条规则,剩下都是细节。
5 个常用动作
| 操作 | 例子 |
|---|---|
| 移动 | mob.animate.shift(LEFT * 2) |
| 缩放 | mob.animate.scale(2) |
| 旋转 | mob.animate.rotate(PI / 4) |
| 变色 | mob.animate.set_color(RED) |
| 链式 | mob.animate.shift(RIGHT).rotate(PI/4).set_color(BLUE)(同时进行) |
run_time 和 rate_func
每个 self.play() 可以加全局参数:
python
self.play(
mob.animate.shift(RIGHT),
run_time=3, # 持续 3 秒(默认 1 秒)
rate_func=smooth, # 缓动曲线(默认 smooth)
)
run_time默认 1 秒rate_func默认smooth(起步慢→中间快→结尾慢,最有"质感")- 其他常用:
linear(匀速)、there_and_back(去再回来)
链式动作是"同时"进行
09_animate_basics.py 里这段:
python
self.play(
c.animate.shift(LEFT * 3) # 同时左移
.rotate(-PI / 3) # 同时旋转
.set_fill(GREEN, opacity=0.8) # 同时变色
.scale(0.5), # 同时缩小
run_time=2,
)
4 个动作 2 秒内同时完成。想让它们依次 进行?阶段 2 的 Succession。
动画 vs 瞬时修改(最后一个易错点)
| 调用方式 | 效果 |
|---|---|
circle.set_color(RED) |
立即变色(0 秒) |
self.play(circle.animate.set_color(RED)) |
1 秒渐变 |
mob.method(...) 是立刻调用 ,self.play(mob.animate.method(...)) 是动画化。这一字之差决定了你的代码"动不动"。
同时播放多个动画
10_multiple.py:
python
self.play(
circle.animate.shift(DOWN), # 圆下移
square.animate.shift(UP), # 方上移
triangle.animate.set_color(YELLOW), # 三角变色
run_time=2,
)
self.play() 的所有参数是同时 播放的(除非用 Succession,阶段 2 会讲)。
阶段 1 产出:圆 → 方 → 三角变身动画
11_final.py ------ 综合运用阶段 1 所有知识点,21 个动画一气呵成:
python
"""
阶段 1 产出 - 圆 → 方 → 三角变身动画
综合运用:
- VGroup + arrange
- Create 登场
- Transform 变形
- .animate 链式动作
- FadeOut 对称退场
"""
from manim import *
class ShapeMorph(Scene):
def construct(self):
# ---- 1. SETUP ----
circle = Circle(color=BLUE, fill_opacity=0.5)
square = Square(color=RED, fill_opacity=0.5)
triangle = Triangle(color=GREEN, fill_opacity=0.5)
shapes = VGroup(circle, square, triangle).arrange(RIGHT, buff=1.5)
title = Text("Shape Morph", font_size=42).to_edge(UP)
# ---- 2. 标题登场 ----
self.play(Write(title))
self.wait(0.3)
# ---- 3. 三个形状依次 Create ----
self.play(Create(circle))
self.wait(0.2)
self.play(Create(square))
self.wait(0.2)
self.play(Create(triangle))
self.wait(0.5)
# ---- 4. 整体脉冲(链式 animate) ----
self.play(
shapes.animate.scale(0.8).set_fill(opacity=0.8),
run_time=1,
)
self.play(
shapes.animate.scale(1.25).set_fill(opacity=0.5),
run_time=1,
)
self.wait(0.5)
# ---- 5. 变身:圆 → 方 ----
circle_target = Square(color=YELLOW, fill_opacity=0.5).move_to(circle)
self.play(Transform(circle, circle_target), run_time=1.5)
self.wait(0.3)
# ---- 6. 变身:方 → 三角 ----
square_target = Triangle(color=PURPLE, fill_opacity=0.5).move_to(square)
self.play(Transform(square, square_target), run_time=1.5)
self.wait(0.5)
# ---- 7. 全部变蓝 ----
self.play(shapes.animate.set_color(BLUE), run_time=1.5)
self.wait(0.5)
# ---- 8. 旋转离场 ----
self.play(
shapes.animate.rotate(PI).set_fill(WHITE, opacity=0.2),
run_time=1.5,
)
self.wait(0.3)
# ---- 9. CLEANUP:对称退场 ----
self.play(FadeOut(shapes), FadeOut(title))
self.wait(0.5)
渲染命令:manim 11_final.py ShapeMorph,约 30 秒出片。
我踩过的 5 个坑
Create对已存在对象无效 → 不要先add()再play(Create())self.add()不推进时间 → 它只是瞬时插入Transform后原变量还在场景 (阶段 2 会用ReplacementTransform解决)- macOS SIP 限制 → 渲染输出要放到可写目录(用
manim.cfg配media_dir) - 方法链别太长 → 3 步以上拆行更可读
下一篇预告
阶段 1 写的 Transform(circle, target) 后,circle 这个变量还在场景里------这是个坑。下一篇会解决它,并进入真正的"节奏感":
ReplacementTransform:原对象消失,新对象登场LaggedStart/Succession:让多个形状像波浪一样依次登场- 完整 easing 矩阵 :
ease_out_bounce等 20+ 种缓动 positioning5 件套 :move_to/shift/next_to/align_to/to_edge