ManimGL 官方示例场景全解:从交互式调试到三维曲面,读懂 example_scenes.py 的九个官方场景
本文基于 manim(manimgl,OpenGL 渲染版本)仓库中的官方文档 Example Scenes 与示例文件 example_scenes.py,逐一拆解其中提供的各个场景:交互式开发(self.embed())、.animate 方法动画语法、Text/Tex 文字系统、TransformMatchingTex 等匹配变换、Updater 更新器机制、坐标系与函数图像绘制、三维曲面与相机控制,以及一个综合二维场景。读完本文,你不仅能完整复制运行这些官方场景,还能在源码层面理解每一段示例背后的实现机制,将其应用到自己的数学动画创作中。
一、如何运行这些示例场景
示例文件 example_scenes.py 位于仓库根目录,文件头部注释给出了运行方式与常用命令行参数:
# To watch one of these scenes, run the following:
# manimgl example_scenes.py OpeningManimExample
# Use -s to skip to the end and just save the final frame
# Use -w to write the animation to a file
# Use -o to write it to a file and open it once done
# Use -n <number> to skip ahead to the n'th animation of a scene.
即 manimgl <场景文件> <场景类名>,配合 -s(跳过动画直接保存最后一帧)、-w(写出视频文件)、-o(写出并自动打开)、-n <n>(跳到场景内第 n 个动画)。场景类都继承 Scene 并实现 construct(self) 方法;二维场景使用默认相机,三维场景则继承 ThreeDScene 或通过 CONFIG 指定 ThreeDCamera。
以下按文档 InteractiveDevelopment → AnimatingMethods → TextExample → TexTransformExample → UpdatersExample → CoordinateSystemExample → GraphExample → SurfaceExample → OpeningManimExample 的顺序逐一讲解。
二、InteractiveDevelopment:交互式场景开发
该场景与快速入门(见 Quick Start)中写的场景类似,其价值在于演示 self.embed() 打开交互式终端后如何高效开发场景。完整代码:
from manimlib import *
class InteractiveDevelopment(Scene):
def construct(self):
circle = Circle()
circle.set_fill(BLUE, opacity=0.5)
circle.set_stroke(BLUE_E, width=4)
square = Square()
self.play(ShowCreation(square))
self.wait()
# This opens an iPython terminal where you can keep writing
# lines as if they were part of this construct method.
# In particular, 'square', 'circle' and 'self' will all be
# part of the local namespace in that terminal.
self.embed()
# Try copying and pasting some of the lines below into
# the interactive shell
self.play(ReplacementTransform(square, circle))
self.wait()
self.play(circle.animate.stretch(4, 0))
self.play(Rotate(circle, 90 * DEG))
self.play(circle.animate.shift(2 * RIGHT).scale(0.25))
text = Text("""
In general, using the interactive shell
is very helpful when developing new scenes
""")
self.play(Write(text))
# In the interactive shell, you can just type
# play, add, remove, clear, wait, save_state and restore,
# instead of self.play, self.add, self.remove, etc.
# To interact with the window, type touch(). You can then
# scroll in the window, or zoom by holding down 'z' while scrolling,
# and change camera perspective by holding down 'd' while moving
# the mouse. Press 'r' to reset to the standard camera position.
# Press 'q' to stop interacting with the window and go back to
# typing new commands into the shell.
# In principle you can customize a scene to be responsive to
# mouse and keyboard interactions
always(circle.move_to, self.mouse_point)
源码视角:embed() 到底做了什么
embed 方法定义在 Scene.embed:仅当场景带有窗口(交互模式)时才生效,它先 stop_skipping() 并强制绘制当前帧,然后创建 InteractiveSceneEmbed(self).launch() 启动内嵌 IPython 终端;退出终端后默认抛出 EndScene 结束场景。
真正的终端逻辑在 InteractiveSceneEmbed 中,几个关键机制都可在源码中验证:
- 局部命名空间注入:
get_ipython_shell_for_embedded_scene通过回溯调用栈拿到self.embed()所在construct的局部变量,把square、circle、self等变量注入终端模块命名空间——这就是文档所说“终端里可以直接使用这些变量”的原因。 - 快捷键:get_shortcuts 向终端注入
play、wait、add、remove、clear、save_state、undo、redo、checkpoint_paste、reload等名字,所以终端里可以直接敲play(...)而不是self.play(...),与文档注释完全一致。 - 出错视觉反馈:
ensure_flash_on_error给终端注册了自定义异常钩子,代码出错时场景窗口边框会闪红(FullScreenRectangle加红色描边播放VFadeInThenOut动画),见 scene_embed.py。 - checkpoint 粘贴:
checkpoint_paste会读取剪贴板代码块,若代码块以注释开头,则恢复/记录该注释对应的场景快照,便于反复试验同一段代码。
文档注释中提到的窗口交互快捷键(touch() 后滚动平移、按住 z 缩放、按住 d 改变三维视角、r 复位相机、q 返回终端)对应场景窗口事件循环的交互逻辑,窗口交互提示亦可见 Scene 交互提示 中的日志文案。最后一行 always(circle.move_to, self.mouse_point) 演示了场景响应鼠标的能力:self.mouse_point 是场景内置的鼠标位置追踪对象,配合 always 更新器让圆形始终跟随鼠标。
三、AnimatingMethods:用 .animate 语法动画化任意方法
这个场景引入了两个新用法:.get_grid() 与 self.play(mob.animate.method(args))。完整代码:
class AnimatingMethods(Scene):
def construct(self):
grid = OldTex(r"\pi").get_grid(10, 10, height=4)
self.add(grid)
# You can animate the application of mobject methods with the
# ".animate" syntax:
self.play(grid.animate.shift(LEFT))
# Both of those will interpolate between the mobject's initial
# state and whatever happens when you apply that method.
# For this example, calling grid.shift(LEFT) would shift the
# grid one unit to the left, but both of the previous calls to
# "self.play" animate that motion.
# The same applies for any method, including those setting colors.
self.play(grid.animate.set_color(YELLOW))
self.wait()
self.play(grid.animate.set_submobject_colors_by_gradient(BLUE, GREEN))
self.wait()
self.play(grid.animate.set_height(TAU - MED_SMALL_BUFF))
self.wait()
# The method Mobject.apply_complex_function lets you apply arbitrary
# complex functions, treating the points defining the mobject as
# complex numbers.
self.play(grid.animate.apply_complex_function(np.exp), run_time=5)
self.wait()
# Even more generally, you could apply Mobject.apply_function,
# which takes in functions form R^3 to R^3
self.play(
grid.animate.apply_function(
lambda p: [
p[0] + 0.5 * math.sin(p[1]),
p[1] + 0.5 * math.sin(p[0]),
p[2]
]
),
run_time=5,
)
self.wait()
两个新概念的展开
.get_grid(n, m, height=...):在 Mobject.get_grid 中定义,返回一个新 mobject,包含原 mobject 的多份副本并按 n 列 m 行排成网格,height指定整体高度。示例中用它把 π 符号铺成 10×10 的网格,作为后续形变的“测试布”。.animate语法:其语义是把 mobject 的某个“修改型方法”转换成动画——在对象当前状态与方法调用后的目标状态之间做插值。例如grid.shift(LEFT)会立即整体左移 1 个单位,而self.play(grid.animate.shift(LEFT))则播放这段移动的动画。对任何方法都成立,包括set_color、set_height这类看起来与“位置”无关的方法。apply_complex_function/apply_function:前者把定义 mobject 的点当作复数,应用任意复函数(示例中np.exp会产出著名的螺旋形变);后者接受R^3 → R^3的普通函数,示例中用正弦扰动生成波浪状形变。
四、TextExample:Text 的文字系统与逐字样式
完整代码:
class TextExample(Scene):
def construct(self):
# To run this scene properly, you should have "Consolas" font in your computer
text = Text("Here is a text", font="Consolas", font_size=90)
difference = Text(
"""
The most important difference between Text and TexText is that\n
you can change the font more easily, but can't use the LaTeX grammar
""",
font="Arial", font_size=24,
# t2c is a dict that you can choose color for different text
t2c={"Text": BLUE, "TexText": BLUE, "LaTeX": ORANGE}
)
VGroup(text, difference).arrange(DOWN, buff=1)
self.play(Write(text))
self.play(FadeIn(difference, UP))
self.wait(3)
fonts = Text(
"And you can also set the font according to different words",
font="Arial",
t2f={"font": "Consolas", "words": "Consolas"},
t2c={"font": BLUE, "words": GREEN}
)
fonts.set_width(FRAME_WIDTH - 1)
slant = Text(
"And the same as slant and weight",
font="Consolas",
t2s={"slant": ITALIC},
t2w={"weight": BOLD},
t2c={"slant": ORANGE, "weight": RED}
)
VGroup(fonts, slant).arrange(DOWN, buff=0.8)
self.play(FadeOut(text), FadeOut(difference, shift=DOWN))
self.play(Write(fonts))
self.wait()
self.play(Write(slant))
self.wait()
文档对新类的总结:
Text:创建纯文字(不经过 LaTeX),可直接指定系统字体(font)。注意示例注释:要在本机正确运行,需要安装对应字体(如 Consolas、Arial)。VGroup:把多个 mobject 组合成整体,.arrange(DOWN, buff=1)表示按DOWN方向依次排列,间距为buff。Write:书写效果动画;FadeIn/FadeOut:淡入/淡出动画,第二个位置参数表示淡入/淡出的方向。
t2c / t2f / t2s / t2w:逐词自定义样式
这是 Text 最实用的能力,四个字典参数分别对“子串”生效:
| 参数 | 含义 | 示例 |
|---|---|---|
t2c |
子串 → 颜色 | t2c={"font": BLUE} |
t2f |
子串 → 字体 | t2f={"font": "Consolas"} |
t2s |
子串 → 斜体样式 | t2s={"slant": ITALIC} |
t2w |
子串 → 字重 | t2w={"weight": BOLD} |
即可以为同一句话里的不同单词设置不同字体、颜色、斜体与粗细,Text 的实现位于 text_mobject.py(StringMobject 体系)。fonts.set_width(FRAME_WIDTH - 1) 则演示了把文字宽度撑满屏幕。
五、TexTransformExample:公式间的匹配变换
这是本文件中最体现 manim“把数学函数可视化为变换”理念的示例。文档版完整代码(使用 OldTex 与 isolate 切分子 mobject):
class TexTransformExample(Scene):
def construct(self):
to_isolate = ["B", "C", "=", "(", ")"]
lines = VGroup(
# Passing in muliple arguments to Tex will result
# in the same expression as if those arguments had
# been joined together, except that the submobject
# hierarchy of the resulting mobject ensure that the
# Tex mobject has a subject corresponding to
# each of these strings. For example, the Tex mobject
# below will have 5 subjects, corresponding to
# the expressions [A^2, +, B^2, =, C^2]
OldTex("A^2", "+", "B^2", "=", "C^2"),
OldTex("A^2", "=", "C^2", "-", "B^2"),
# Alternatively, you can pass in the keyword argument
# "isolate" with a list of strings that should be out as
# their own submobject.
OldTex("A^2 = (C + B)(C - B)", isolate=["A^2", *to_isolate]),
OldTex("A = \\sqrt{(C + B)(C - B)}", isolate=["A", *to_isolate])
)
lines.arrange(DOWN, buff=LARGE_BUFF)
for line in lines:
line.set_color_by_tex_to_color_map({
"A": BLUE,
"B": TEAL,
"C": GREEN,
})
play_kw = {"run_time": 2}
self.add(lines[0])
self.play(
TransformMatchingTex(
lines[0].copy(), lines[1],
path_arc=90 * DEG,
),
**play_kw
)
self.wait()
self.play(
TransformMatchingTex(lines[1].copy(), lines[2]),
**play_kw
)
self.wait()
# If, however, we want the C^2 to go to C, and B^2 to go to B,
# we can specify that with a key map.
self.play(FadeOut(lines[2]))
self.play(
TransformMatchingTex(
lines[1].copy(), lines[2],
key_map={
"C^2": "C",
"B^2": "B",
}
),
**play_kw
)
self.wait()
# Let the exponent "^2" transform into the square root symbol
new_line2 = OldTex("A^2 = (C + B)(C - B)", isolate=["A", *to_isolate])
new_line2.replace(lines[2])
new_line2.match_style(lines[2])
self.play(
TransformMatchingTex(
new_line2, lines[3],
transform_mismatches=True,
),
**play_kw
)
self.wait(3)
self.play(FadeOut(lines, RIGHT))
source = Text("the morse code", height=1)
target = Text("here come dots", height=1)
self.play(Write(source))
self.wait()
kw = {"run_time": 3, "path_arc": PI / 2}
self.play(TransformMatchingShapes(source, target, **kw))
self.wait()
self.play(TransformMatchingShapes(target, source, **kw))
self.wait()
文档对四个新类的总结:Tex(LaTeX 数学公式)、TexText(LaTeX 文字)、TransformMatchingTex(按 tex 子串的异同自动对齐变换)、TransformMatchingShapes(按点集形状相似性直接对齐变换)。
源码视角:匹配算法是怎么实现的
这些动画全部继承自 TransformMatchingParts(TransformMatchingShapes 是其别名,TransformMatchingTex 继承 TransformMatchingStrings),核心流程可概括为三步:
- 收集匹配对:
TransformMatchingStrings.matching_blocks(见 transform_matching_parts.py)先取用户显式指定的key_map(如{"C^2": "C"})和matched_keys作为最高优先级匹配,再对两侧符号子串列表使用difflib.SequenceMatcher反复找“最长公共子串”自动匹配——这就是“按 tex 相似性自动对齐”的底层算法。 - 按形状补齐:
find_pairs_with_matching_shapes对尚未匹配的碎片两两调用has_same_shape_as,形状相同的碎片走match_animation(默认Transform),否则走mismatch_animation。 - 处理剩余部分:源侧未匹配的碎片
FadeOutToPoint到目标中心,目标侧未匹配的碎片FadeInFromPoint从源中心淡入。transform_mismatches=True时,形状不一致的碎片之间也强制建立Transform,于是示例中^2指数可以“长”成\sqrt根号。
path_arc 参数让每个碎片沿圆弧路径旋转到位,文档注释解释这正是“重排公式”观感的来源。另外,isolate 参数(以及 Tex("A^2", "+", "B^2", ...) 的多参数写法)决定了子 mobject 的切分粒度,直接决定哪些部分能作为整体参与匹配——示例最后一幕先把 A^2 拆成独立的 A 与 ^2,再让 ^2 单独变换为根号,正是利用了这一点。
说明:仓库 example_scenes.py 中的同名场景使用了功能等价的
Tex与TransformMatchingStrings(配合matched_keys、key_map={"2": R"\sqrt"}写法),二者来自同一实现体系,可互换参考。
六、UpdatersExample:更新器(Updater)机制
Updater 是 manim 中“每帧自动执行某段逻辑”的机制,典型用途是让括号、标签实时跟随变形的对象。完整代码:
class UpdatersExample(Scene):
def construct(self):
square = Square()
square.set_fill(BLUE_E, 1)
# On all all frames, the constructor Brace(square, UP) will
# be called, and the mobject brace will set its data to match
# that of the newly constructed object
brace = always_redraw(Brace, square, UP)
text, number = label = VGroup(
Text("Width = "),
DecimalNumber(
0,
show_ellipsis=True,
num_decimal_places=2,
include_sign=True,
)
)
label.arrange(RIGHT)
# This ensures that the method deicmal.next_to(square)
# is called on every frame
always(label.next_to, brace, UP)
# You could also write the following equivalent line
# label.add_updater(lambda m: m.next_to(brace, UP))
# If the argument itself might change, you can use f_always,
# for which the arguments following the initial Mobject method
# should be functions returning arguments to that method.
# The following line ensures that decimal.set_value(square.get_y())
# is called every frame
f_always(number.set_value, square.get_width)
# You could also write the following equivalent line
# number.add_updater(lambda m: m.set_value(square.get_width()))
self.add(square, brace, label)
# Notice that the brace and label track with the square
self.play(
square.animate.scale(2),
rate_func=there_and_back,
run_time=2,
)
self.wait()
self.play(
square.animate.set_width(5, stretch=True),
run_time=3,
)
self.wait()
self.play(
square.animate.set_width(2),
run_time=3
)
self.wait()
# In general, you can alway call Mobject.add_updater, and pass in
# a function that you want to be called on every frame. The function
# should take in either one argument, the mobject, or two arguments,
# the mobject and the amount of time since the last frame.
now = self.time
w0 = square.get_width()
square.add_updater(
lambda m: m.set_width(w0 * math.cos(self.time - now))
)
self.wait(4 * PI)
文档对新用法的总结:
always_redraw():每帧重建一个 mobject(示例中每帧重新构造Brace(square, UP),使括号始终贴合当前方形宽度);DecimalNumber:可变数字 mobject,拆成Text字符,数字变化时逐位刷新;always(f, x):每帧执行f(x)(x 为固定值);f_always(f, g):每帧执行f(g()),适用于参数本身也会变化的场景(示例中每帧把square.get_width()的最新值写入数字);.add_updater():注册一个每帧调用的回调,回调可接收 1 个参数(mobject)或 2 个参数(mobject、距上帧的时间dt);.to_edge()/.center()/.set_y()等定位方法按文档描述分别为靠边、居中、设置纵坐标。
源码视角:always / f_always / always_redraw 的实现
三者都在 mobject_update_utils.py 中,本质都是 add_updater 的语法糖:
always(method, *args):断言参数是 Mobject 方法,取出其未绑定函数func与宿主mobject,注册lambda m: func(m, *args)——即文档中“等价于label.add_updater(lambda m: m.next_to(brace, UP))”的来源;f_always(method, *arg_generators):参数是“生成器函数”,updater 内先逐个调用生成器取到实参再执行方法,故能处理“参数随时间变化”的情况;always_redraw(func, *args):立即调用一次func得到 mobject,再注册lambda m: mob.become(func(*args))——每帧用become把 mobject 的数据替换为全新构造结果,这就是括号能“贴合变形”的机制。
此外,Mobject 还提供 mob.always.xxx / mob.f_always.xxx 的属性式写法(如 label.always.next_to(brace, UP)),由 Mobject 中的 _UpdaterBuilder / _FunctionalUpdaterBuilder 实现:__getattr__ 捕获任意方法名并自动包装成 updater。仓库 example_scenes.py 中的 UpdatersExample 还演示了时间驱动型 updater:lambda m, dt: m.rotate(dt) 按帧间时间增量旋转对象。
七、CoordinateSystemExample:坐标系与坐标映射
完整代码:
class CoordinateSystemExample(Scene):
def construct(self):
axes = Axes(
# x-axis ranges from -1 to 10, with a default step size of 1
x_range=(-1, 10),
# y-axis ranges from -2 to 2 with a step size of 0.5
y_range=(-2, 2, 0.5),
# The axes will be stretched so as to match the specified
# height and width
height=6,
width=10,
# Axes is made of two NumberLine mobjects. You can specify
# their configuration with axis_config
axis_config={
"stroke_color": GREY_A,
"stroke_width": 2,
},
# Alternatively, you can specify configuration for just one
# of them, like this.
y_axis_config={
"include_tip": False,
}
)
# Keyword arguments of add_coordinate_labels can be used to
# configure the DecimalNumber mobjects which it creates and
# adds to the axes
axes.add_coordinate_labels(
font_size=20,
num_decimal_places=1,
)
self.add(axes)
# Axes descends from the CoordinateSystem class, meaning
# you can call call axes.coords_to_point, abbreviated to
# axes.c2p, to associate a set of coordinates with a point,
# like so:
dot = Dot(fill_color=RED)
dot.move_to(axes.c2p(0, 0))
self.play(FadeIn(dot, scale=0.5))
self.play(dot.animate.move_to(axes.c2p(3, 2)))
self.wait()
self.play(dot.animate.move_to(axes.c2p(5, 0.5)))
self.wait()
# Similarly, you can call axes.point_to_coords, or axes.p2c
# print(axes.p2c(dot.get_center()))
# We can draw lines from the axes to better mark the coordinates
# of a given point.
# Here, the always_redraw command means that on each new frame
# the lines will be redrawn
h_line = always_redraw(lambda: axes.get_h_line(dot.get_left()))
v_line = always_redraw(lambda: axes.get_v_line(dot.get_bottom()))
self.play(
ShowCreation(h_line),
ShowCreation(v_line),
)
self.play(dot.animate.move_to(axes.c2p(3, -2)))
self.wait()
self.play(dot.animate.move_to(axes.c2p(1, 1)))
self.wait()
# If we tie the dot to a particular set of coordinates, notice
# that as we move the axes around it respects the coordinate
# system defined by them.
f_always(dot.move_to, lambda: axes.c2p(1, 1))
self.play(
axes.animate.scale(0.75).to_corner(UL),
run_time=2,
)
self.wait()
self.play(FadeOut(VGroup(axes, dot, h_line, v_line)))
要点解析:
Axes的构造参数:x_range/y_range指定取值范围与步长(三元素形式(min, max, step)),height/width指定整体尺寸(坐标系会拉伸以匹配);axis_config把参数透传给内部两条NumberLine(Axes由两条NumberLine组成),y_axis_config只针对 y 轴,例如include_tip=False去掉箭头;add_coordinate_labels(font_size=20, num_decimal_places=1)的 kwargs 用来配置其内部创建的DecimalNumber;axes.c2p(x, y)(coords_to_point的缩写)把数学坐标映射为屏幕点,axes.p2c(point)反之;always_redraw(lambda: axes.get_h_line(...))每帧重绘水平/垂直参考线,使参考线始终从坐标轴指向动点;- 末段用
f_always(dot.move_to, lambda: axes.c2p(1, 1))把点“绑定”到坐标 (1,1):即使整组坐标轴被scale(0.75).to_corner(UL)搬走缩放,点依然跟随坐标系走——这是“点跟随坐标系而非屏幕”的关键技巧; - 文档注释提示还可尝试
ThreeDAxes、NumberPlane、ComplexPlane,相关实现位于 coordinate_systems.py。
八、GraphExample:在坐标系上绘制函数图像
完整代码:
class GraphExample(Scene):
def construct(self):
axes = Axes((-3, 10), (-1, 8))
axes.add_coordinate_labels()
self.play(Write(axes, lag_ratio=0.01, run_time=1))
# Axes.get_graph will return the graph of a function
sin_graph = axes.get_graph(
lambda x: 2 * math.sin(x),
color=BLUE,
)
# By default, it draws it so as to somewhat smoothly interpolate
# between sampled points (x, f(x)). If the graph is meant to have
# a corner, though, you can set use_smoothing to False
relu_graph = axes.get_graph(
lambda x: max(x, 0),
use_smoothing=False,
color=YELLOW,
)
# For discontinuous functions, you can specify the point of
# discontinuity so that it does not try to draw over the gap.
step_graph = axes.get_graph(
lambda x: 2.0 if x > 3 else 1.0,
discontinuities=[3],
color=GREEN,
)
# Axes.get_graph_label takes in either a string or a mobject.
# If it's a string, it treats it as a LaTeX expression. By default
# it places the label next to the graph near the right side,
# and has it match the color of the graph
sin_label = axes.get_graph_label(sin_graph, "\\sin(x)")
relu_label = axes.get_graph_label(relu_graph, Text("ReLU"))
step_label = axes.get_graph_label(step_graph, Text("Step"), x=4)
self.play(
ShowCreation(sin_graph),
FadeIn(sin_label, RIGHT),
)
self.wait(2)
self.play(
ReplacementTransform(sin_graph, relu_graph),
FadeTransform(sin_label, relu_label),
)
self.wait()
self.play(
ReplacementTransform(relu_graph, step_graph),
FadeTransform(relu_label, step_label),
)
self.wait()
parabola = axes.get_graph(lambda x: 0.25 * x**2)
parabola.set_stroke(BLUE)
self.play(
FadeOut(step_graph),
FadeOut(step_label),
ShowCreation(parabola)
)
self.wait()
# You can use axes.input_to_graph_point, abbreviated
# to axes.i2gp, to find a particular point on a graph
dot = Dot(fill_color=RED)
dot.move_to(axes.i2gp(2, parabola))
self.play(FadeIn(dot, scale=0.5))
# A value tracker lets us animate a parameter, usually
# with the intent of having other mobjects update based
# on the parameter
x_tracker = ValueTracker(2)
f_always(
dot.move_to,
lambda: axes.i2gp(x_tracker.get_value(), parabola)
)
self.play(x_tracker.animate.set_value(4), run_time=3)
self.play(x_tracker.animate.set_value(-2), run_time=3)
self.wait()
关键 API 归纳(均在 Axes/CoordinateSystem 体系内,实现见 coordinate_systems.py):
| API | 作用 | 示例细节 |
|---|---|---|
axes.get_graph(f, **kwargs) |
采样 f(x) 并连线成图 |
默认平滑插值;折线函数用 use_smoothing=False(ReLU 例);断点函数传 discontinuitie s=[3] 避免跨缺口连线 |
axes.get_graph_label(graph, label, x=...) |
生成图例标签 | 字符串按 LaTeX 处理,mobject 则直接用;默认放在图像右端并继承图像颜色 |
axes.i2gp(x, graph) |
input_to_graph_point 缩写,求函数图上的点 |
用于放置动点 |
ValueTracker |
可动画的参数载体 | x_tracker.animate.set_value(...) 动画化数值变化,配合 f_always 让动点沿抛物线滑动 |
动点沿曲线滑动的组合技值得单独记住:ValueTracker 持有自变量 x 的当前值 → f_always(dot.move_to, lambda: axes.i2gp(x_tracker.get_value(), parabola)) 每帧重算点位置 → self.play(x_tracker.animate.set_value(...)) 提供动画驱动。ValueTracker 定义于 value_tracker.py。
九、SurfaceExample:三维曲面、相机与光源
这是文档中的三维场景示例。注意文档版使用 CONFIG = {"camera_class": ThreeDCamera} 指定相机,而仓库 example_scenes.py 中的等价实现直接继承 ThreeDScene 并使用 self.frame,两种写法都可用。文档版完整代码:
class SurfaceExample(Scene):
CONFIG = {
"camera_class": ThreeDCamera,
}
def construct(self):
surface_text = Text("For 3d scenes, try using surfaces")
surface_text.fix_in_frame()
surface_text.to_edge(UP)
self.add(surface_text)
self.wait(0.1)
torus1 = Torus(r1=1, r2=1)
torus2 = Torus(r1=3, r2=1)
sphere = Sphere(radius=3, resolution=torus1.resolution)
# You can texture a surface with up to two images, which will
# be interpreted as the side towards the light, and away from
# the light. These can be either urls, or paths to a local file
# in whatever you've set as the image directory in
# the custom_config.yml file
# day_texture = "EarthTextureMap"
# night_texture = "NightEarthTextureMap"
day_texture = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Whole_world_-_land_and_oceans.jpg/1280px-Whole_world_-_land_and_oceans.jpg"
night_texture = "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/The_earth_at_night.jpg/1280px-The_earth_at_night.jpg"
surfaces = [
TexturedSurface(surface, day_texture, night_texture)
for surface in [sphere, torus1, torus2]
]
for mob in surfaces:
mob.shift(IN)
mob.mesh = SurfaceMesh(mob)
mob.mesh.set_stroke(BLUE, 1, opacity=0.5)
# Set perspective
frame = self.camera.frame
frame.set_euler_angles(
theta=-30 * DEG,
phi=70 * DEG,
)
surface = surfaces[0]
self.play(
FadeIn(surface),
ShowCreation(surface.mesh, lag_ratio=0.01, run_time=3),
)
for mob in surfaces:
mob.add(mob.mesh)
surface.save_state()
self.play(Rotate(surface, PI / 2), run_time=2)
for mob in surfaces[1:]:
mob.rotate(PI / 2)
self.play(
Transform(surface, surfaces[1]),
run_time=3
)
self.play(
Transform(surface, surfaces[2]),
# Move camera frame during the transition
frame.animate.increment_phi(-10 * DEG),
frame.animate.increment_theta(-20 * DEG),
run_time=3
)
# Add ambient rotation
frame.add_updater(lambda m, dt: m.increment_theta(-0.1 * dt))
# Play around with where the light is
light_text = Text("You can move around the light source")
light_text.move_to(surface_text)
light_text.fix_in_frame()
self.play(FadeTransform(surface_text, light_text))
light = self.camera.light_source
self.add(light)
light.save_state()
self.play(light.animate.move_to(3 * IN), run_time=5)
self.play(light.animate.shift(10 * OUT), run_time=5)
drag_text = Text("Try moving the mouse while pressing d or s")
drag_text.move_to(light_text)
drag_text.fix_in_frame()
self.play(FadeTransform(light_text, drag_text))
self.wait()
文档对该场景的要点总结(配合源码理解):
- 三维曲面:
Sphere、Torus(r1, r2)生成参数化曲面,TexturedSurface(surface, day_texture, night_texture)贴上最多两张贴图,分别对应“朝向光源面”与“背向光源面”;贴图可以是 URL,也可以是custom_config.yml指定 image 目录下的本地文件。相关实现位于 surface.py。 SurfaceMesh:为曲面生成线框网格,set_stroke(BLUE, 1, opacity=0.5)设置线框样式,ShowCreation(surface.mesh, lag_ratio=0.01)让网格逐线绘制。.fix_in_frame():使对象不随视角变化而改变,始终显示在屏幕固定位置(示例中的提示文字)。- 相机控制:
self.camera.frame是三维相机的取景框 mobject,set_euler_angles(theta, phi)设置初始视角;由于它本身是 mobject,可以用frame.animate.increment_phi(...)/increment_theta(...)动画化改变视角,甚至用时间型 updaterlambda m, dt: m.increment_theta(-0.1 * dt)添加“环境旋转”。 - 光源:
self.camera.light_source也是 mobject,可save_state()后用animate.move_to/shift移动,实时改变曲面光照。 - 文档结尾提示可在渲染窗口中按住
d(改变视角)或s(平移)拖动鼠标体验交互。
十、OpeningManimExample:二维综合场景
作为压轴示例,该场景综合了文字、网格、矩阵、复平面与函数变换,完整代码:
class OpeningManimExample(Scene):
def construct(self):
intro_words = Text("""
The original motivation for manim was to
better illustrate mathematical functions
as transformations.
""")
intro_words.to_edge(UP)
self.play(Write(intro_words))
self.wait(2)
# Linear transform
grid = NumberPlane((-10, 10), (-5, 5))
matrix = [[1, 1], [0, 1]]
linear_transform_words = VGroup(
Text("This is what the matrix"),
IntegerMatrix(matrix, include_background_rectangle=True),
Text("looks like")
)
linear_transform_words.arrange(RIGHT)
linear_transform_words.to_edge(UP)
linear_transform_words.set_stroke(BLACK, 10, background=True)
self.play(
ShowCreation(grid),
FadeTransform(intro_words, linear_transform_words)
)
self.wait()
self.play(grid.animate.apply_matrix(matrix), run_time=3)
self.wait()
# Complex map
c_grid = ComplexPlane()
moving_c_grid = c_grid.copy()
moving_c_grid.prepare_for_nonlinear_transform()
c_grid.set_stroke(BLUE_E, 1)
c_grid.add_coordinate_labels(font_size=24)
complex_map_words = TexText("""
Or thinking of the plane as $\\mathds{C}$,\\
this is the map $z \\rightarrow z^2$
""")
complex_map_words.to_corner(UR)
complex_map_words.set_stroke(BLACK, 5, background=True)
self.play(
FadeOut(grid),
Write(c_grid, run_time=3),
FadeIn(moving_c_grid),
FadeTransform(linear_transform_words, complex_map_words),
)
self.wait()
self.play(
moving_c_grid.animate.apply_complex_function(lambda z: z**2),
run_time=6,
)
self.wait(2)
该场景串起了前几节的大部分能力:
- 线性变换演示:
NumberPlane((-10, 10), (-5, 5))建立网格;IntegerMatrix(matrix, include_background_rectangle=True)渲染带背景矩形的矩阵;grid.animate.apply_matrix(matrix)让整张网格按矩阵[[1,1],[0,1]]做切变——直观展示“矩阵即变换”;set_stroke(BLACK, 10, background=True)给文字加黑色背景描边以便在网格上阅读。 - 复函数演示:
ComplexPlane()建立复平面;prepare_for_nonlinear_transform()预先加密网格采样,使非线性变换时网格不变形撕裂;复制体moving_c_grid执行apply_complex_function(lambda z: z**2),静态的c_grid保持原样作参照,这正是可视化z → z²映射的标准手法。 - 动画组合:
ShowCreation、FadeTransform、FadeIn/FadeOut、Write在同一self.play(...)中并行播放,展示 manim 的多动画并发能力。
文档在此场景后总结:看完这些场景,你已经掌握了 manim 的大部分用法;更多示例可参考 3b1b 的开源视频代码仓库(3b1b/videos,在 example_scenes.py 尾部注释中有提及)。
十一、能力地图:从九个场景提炼的学习路径
把文档九个场景按知识点归类,可作为自学路线:
| 场景 | 核心知识点 | 关键源码 |
|---|---|---|
| InteractiveDevelopment | self.embed() 交互终端、窗口快捷键、鼠标响应 |
scene.py、scene_embed.py |
| AnimatingMethods | .animate 语法、.get_grid()、apply_complex_function/apply_function |
mobject.py |
| TextExample | Text 的 t2c/t2f/t2s/t2w、VGroup.arrange |
text_mobject.py |
| TexTransformExample | Tex 子对象切分(多参数/isolate)、TransformMatchingTex、key_map、transform_mismatches、TransformMatchingShapes |
transform_matching_parts.py |
| UpdatersExample | always/f_always/always_redraw/add_updater、DecimalNumber |
mobject_update_utils.py |
| CoordinateSystemExample | Axes 配置、c2p/p2c、参考线重绘 |
coordinate_systems.py |
| GraphExample | get_graph/get_graph_label/i2gp、ValueTracker |
value_tracker.py |
| SurfaceExample | 三维曲面、贴图、线框、相机帧与光源动画、fix_in_frame |
surface.py |
| OpeningManimExample | NumberPlane/ComplexPlane/IntegerMatrix、矩阵与复函数变换的综合应用 |
coordinate_systems.py |
实际使用时,先按 manimgl example_scenes.py <SceneName> 逐个跑通这些场景,在 self.embed() 终端里对每一段代码做局部修改观察效果,再对照上文源码链接理解行为背后的机制,即可把这些官方示例中的技巧迁移到自己的场景文件中。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00