#《Manim 实战入门:用代码创造数学动画》
Tong Li
前言
欢迎来到 Manim 的世界!Manim 是一个强大的动画引擎,最初由 Grant Sanderson (3Blue1Brown) 开发,用于制作精美的数学解释视频。它使用 Python 编程语言,让您能够通过代码精确控制动画的每一个细节,创造出专业级的可视化效果。
这本书是为初学者设计的,特别是那些希望通过实际案例学习 Manim 的朋友们。我们将直接从您提供的这些优秀 Python 脚本入手,逐个剖析其中的知识点。每个概念都会结合具体的代码进行讲解,让您不仅知道“是什么”,更明白“怎么做”以及“为什么这么做”。
无论您是学生、教师、科普爱好者,还是仅仅对用代码创作动画感兴趣,这本书都将为您打开一扇新的大门。
本书结构
我们将从最基础的 Manim 场景搭建开始,逐步深入到更复杂的动画技巧、图形绘制、布局管理和自定义配置。每一章都会围绕一个或多个核心知识点,并从您提供的脚本中选取最相关的代码片段进行分析。
准备工作
在开始之前,请确保您已经:
- 安装了 Python 环境。
- 按照 Manim 官方文档的指引,成功安装了 Manim 及其依赖项(如 LaTeX, FFmpeg等)。
- 准备一个您喜欢的代码编辑器(如 VS Code, PyCharm 等)。
让我们开始这段奇妙的 Manim 之旅吧!
第一章:Manim 初探——搭建你的第一个场景
Manim 的核心是 场景 (Scene)。一个场景就是一段独立的动画。每个 Manim 脚本通常包含一个或多个继承自 manim.Scene
的类。
1.1 基本场景结构
让我们看一个最简单的场景结构,这在您提供的几乎所有脚本中都有体现:
from manim import * # 导入 Manim 库的所有内容
class MyFirstScene(Scene): # 定义一个场景类,继承自 Scene
def construct(self): # construct 方法是场景的入口点
# 在这里编写动画逻辑
text = Text("你好,Manim!") # 创建一个文本对象
self.play(Write(text)) # 播放“书写”文本的动画
self.wait(1) # 等待1秒
from manim import *
: 这行代码导入了 Manim 库中所有可用的类、函数和常量。对于初学者来说,这很方便。class MyFirstScene(Scene):
: 我们定义了一个名为MyFirstScene
的类,它继承了 Manim 的Scene
基类。这意味着MyFirstScene
将拥有Scene
类的所有功能。def construct(self):
: 这是每个场景类中最重要的方法。Manim 会自动调用这个方法来构建和播放场景中的动画。self
代表场景对象本身。text = Text("你好,Manim!")
:Text
是 Manim 中用于创建文本的类 (Mobject - Manim Object)。这里我们创建了一个内容为“你好,Manim!”的文本对象。self.play(Write(text))
:self.play()
是播放动画的指令。Write(text)
是一种动画效果,它会模拟笔迹书写出text
对象。self.wait(1)
: 动画播放完毕后,让场景暂停1秒。
1.2 运行 Manim 脚本
要运行上面的脚本(假设保存为 my_scene.py
),你可以在终端中使用以下命令:
manim -pql my_scene.py MyFirstScene
manim
: 调用 Manim 命令行工具。-pql
: 选项的组合。p
: 表示预览 (preview),即播放动画。q
: 表示质量 (quality)。l
: 表示低质量 (low quality),渲染速度快,适合调试。其他选项有m
(medium),h
(high),k
(4k)。
my_scene.py
: 你的 Python 文件名。MyFirstScene
: 你想渲染的场景类名。
1.3 场景背景色和默认颜色
在您提供的多个脚本中,我们看到了设置场景背景色和Mobject默认颜色的方法:
25_Parabola_equation_function_graph_changes.txt
26_trigonometric_function.txt
27_find the equation of the line.txt
28_find_the_value_of_function.txt
29_find_angles_of_triangles.txt
30_binary_tree.txt
31_rotational_kinetic_energy_and_angular_momentum.txt
这些脚本中,通常在 construct
方法的开头或 __init__
(或 setup
) 方法中设置:
# 摘自 25_Parabola_equation_function_graph_changes.txt (稍作修改以便说明)
class CombinedScene(Scene):
def setup(self): # setup 方法在 construct 之前被调用
Scene.setup(self) # 调用父类的 setup
Mobject.set_default(color=BLACK) # 设置所有 Mobject 的默认颜色为黑色
self.camera.background_color = WHITE # 设置场景背景色为白色
Text.set_default(font_size=28, color=BLACK)
Title.set_default(font_size=48, color=BLACK) # Title 是一个自定义或特定用途的类
MathTex.set_default(font_size=40, color=BLACK)
def construct(self):
# self.camera.background_color = WHITE # 也可以在这里设置
# ... 其他代码 ...
pass
self.camera.background_color = WHITE
:self.camera
是场景的相机对象,通过修改其background_color
属性可以改变背景颜色。WHITE
是 Manim 预定义的颜色常量。Mobject.set_default(color=BLACK)
:Mobject
是 Manim 中所有图形对象的基类。这行代码将所有后续创建的 Mobject 的默认颜色设置为黑色。Text.set_default(...)
和MathTex.set_default(...)
: 类似地,可以为特定类型的 Mobject (如Text
,MathTex
) 设置默认属性。
1.4 字体设置 (特别是中文字体)
Manim 默认的 LaTeX 环境可能不直接支持中文。在处理中文文本时,需要进行特殊配置。您提供的脚本 31_rotational_kinetic_energy_and_angular_momentum.txt
和 26_trigonometric_function.txt
展示了如何配置中文字体,特别是针对 MathTex
(使用 LaTeX 渲染数学公式和文本)。
# 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt
# ... (导入 os, sys)
# from manim_utils import ..., get_available_font, ... # 假设的工具函数
CJK_FONT_NAME = "Songti SC" # 定义中文字体名称 (宋体-简)
cjk_template = TexTemplate(
tex_compiler="xelatex", # 使用 xelatex 编译器,对 Unicode 支持更好
output_format=".xdv",
preamble=rf"""
\usepackage{{amsmath}}
\usepackage{{amssymb}}
\usepackage{{fontspec}}
\usepackage{{xeCJK}}
\setCJKmainfont{{{CJK_FONT_NAME}}}
"""
)
class CombinedScene(Scene):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# ...
final_font = get_available_font() # 假设这个函数能获取一个可用的系统字体
if final_font:
Text.set_default(font=final_font) # 设置 Text 对象的默认字体
MathTex.set_default(tex_template=cjk_template) # 为 MathTex 设置自定义的 TeX 模板
Text.set_default(color=self.text_color) # self.text_color 可能是 BLACK
MathTex.set_default(color=self.text_color)
# ...
CJK_FONT_NAME
: 指定了要在 LaTeX 中使用的中文字体名称。你需要确保你的系统上安装了这个字体,并且 LaTeX 能够找到它。常见的选择有 "SimSun", "Microsoft YaHei", "Songti SC", "PingFang SC" 等。TexTemplate
: Manim 允许你自定义用于渲染MathTex
和Tex
对象的 LaTeX 模板。tex_compiler="xelatex"
:xelatex
是一个对 Unicode 和现代字体格式 (如 OpenType) 支持非常好的 LaTeX 编译器,推荐用于处理中文。preamble
: LaTeX 文档的导言区。\usepackage{fontspec}
: 允许使用系统安装的字体。\usepackage{xeCJK}
: 提供了在xelatex
中处理中日韩文字的宏包。\setCJKmainfont{{{CJK_FONT_NAME}}}
: 将指定的中文字体设置为主 CJK 字体。
MathTex.set_default(tex_template=cjk_template)
: 将这个自定义的cjk_template
应用于所有MathTex
对象。Text.set_default(font=final_font)
: 对于普通的Text
对象(不通过 LaTeX 渲染),可以直接指定字体名称。get_available_font()
是一个自定义函数,用于获取一个系统中可用的字体名称。如果你的系统中有中文字体,并且 Manim 能找到它,Text
对象也能显示中文。
1.5 辅助工具:场景编号和布局管理
您的许多脚本都使用了一个名为 update_scene_number
的方法和一个 Layout
类。这些通常是用户自定义的辅助工具,用于更好地组织和管理复杂的动画项目。
update_scene_number
:# 类似这样的实现,在多个脚本中出现 # self.current_scene_num_mob = None # 通常在 __init__ 或 setup 中初始化 def update_scene_number(self, number_str): new_scene_num = Text(number_str, font_size=24, color=self.text_color).to_corner(UR, buff=MED_LARGE_BUFF).set_z_index(100) animations = [FadeIn(new_scene_num, run_time=0.5)] if self.current_scene_num_mob: animations.append(FadeOut(self.current_scene_num_mob, run_time=0.5)) self.play(*animations) self.current_scene_num_mob = new_scene_num
这个方法的作用是在屏幕右上角显示当前的场景(或小节)编号,并在更新时带有淡入淡出效果。这对于制作系列视频或长视频时标记进度非常有用。
Text(...).to_corner(UR, ...)
: 创建文本并将其定位到右上角 (Upper Right)。.set_z_index(100)
: 确保编号显示在其他元素的上方。FadeIn
和FadeOut
: 淡入淡出动画。
Layout
类:# 伪代码,基于脚本中的使用推断 # from manim_utils import Layout, LayoutAtom, LayoutDirection # layout = Layout(LayoutDirection.VERTICAL, { # "title_area": (1.0, LayoutAtom()), # 权重1,空的原子布局 # "content_area": (8.0, Layout(LayoutDirection.HORIZONTAL, { # 权重8,嵌套水平布局 # "left_area": (5.0, LayoutAtom()), # "right_area": (5.0, LayoutAtom()) # })) # }).resolve(self) # self 指的是 Scene 对象,用于获取屏幕尺寸 # layout["title_area"].place(title_scene02) # layout["content_area"]["left_area"].place(left_vgroup, aligned_edge=UL, buff=0.3)
这个
Layout
类似乎是一个自定义的工具,用于将屏幕划分为不同的区域,并按比例分配空间。这对于组织复杂的场景元素非常方便,可以避免手动计算坐标。- 它支持垂直和水平方向的布局。
- 可以使用字典来命名和定义区域及其相对权重或大小。
.resolve(self)
可能是用来根据场景的实际尺寸计算出各区域的具体边界。.place(mobject, ...)
方法用于将 Manim 对象放置到指定的布局区域中,可能还支持对齐 (aligned_edge
) 和边距 (buff
)。
这些自定义工具虽然不是 Manim 内置的,但它们展示了如何通过封装来简化复杂场景的创建流程。在后续章节中,我们将更详细地探讨 Manim 内置的 Mobject、动画和布局方法。
本章小结
- Manim 动画是在
Scene
类的construct
方法中定义的。 - 使用
manim
命令运行脚本并渲染场景。 - 可以通过
self.camera.background_color
设置背景色。 Mobject.set_default()
、Text.set_default()
等可以设置对象的默认属性。- 通过
TexTemplate
和xelatex
可以配置MathTex
以支持中文字体。 - 自定义辅助函数和类(如
update_scene_number
,Layout
)可以提高开发效率。
在下一章,我们将深入了解 Manim 中的核心元素:Mobject,以及如何创建和操作它们。
第二章:Manim 的基石——Mobject (Manim 对象)
在 Manim 中,屏幕上显示的一切几乎都是 Mobject (Manim Object) 或其子类的实例。文本、数学公式、几何图形、SVG 图像等等,都是 Mobject。理解 Mobject 是掌握 Manim 的关键。
2.1 常见的 Mobject 类型
让我们从您提供的脚本中找出一些常见的 Mobject 类型:
文本与公式 (Text & MathTex)
Text("一些文本", font="字体名称", font_size=大小, color=颜色, weight=BOLD)
: 用于创建普通文本。font
: 指定字体。font_size
: 字体大小。color
: 文本颜色。weight
: 字体粗细,如BOLD
。
MathTex(r"LaTeX代码", font_size=大小, color=颜色)
: 用于创建使用 LaTeX 渲染的文本和数学公式。r"..."
: Python 的原始字符串 (raw string),在包含反斜杠\
的 LaTeX 代码中很有用。
# 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt title_scene02 = Text("问题 (a): 计算转动动能", font_size=48, weight=BOLD, color=self.text_color) kr_formula = MathTex(r"K_R = \frac{1}{2} I \omega^2", font_size=32) # 摘自 26_trigonometric_function.txt angle_label = MathTex(r"\theta", color=BLACK).scale(0.8) # .scale() 用于缩放
几何图形 (Geometric Shapes)
Circle(radius=半径, color=颜色, fill_opacity=填充不透明度, fill_color=填充色, stroke_width=边框宽度)
Square(side_length=边长, ...)
Rectangle(width=宽度, height=高度, ...)
Polygon(点1, 点2, 点3, ..., color=颜色, ...)
: 通过一系列顶点定义多边形。Line(起点, 终点, color=颜色, stroke_width=宽度)
DashedLine(起点, 终点, ...)
: 虚线。Arrow(起点, 终点, buff=缓冲区, color=颜色, stroke_width=宽度)
: 箭头。Dot(坐标点, radius=半径, color=颜色)
: 点。Triangle(color=颜色, fill_opacity=填充不透明度)
: 等边三角形。
# 摘自 24_why_sky_is_blue.txt sun = Circle(radius=1.0, color=MY_SUN_YELLOW, fill_opacity=1.0) prism = Triangle(color=MY_GRAY, fill_opacity=0.3).scale(0.8).rotate(PI/2) light_beam = Line(sun.get_right(), prism.get_left(), color=MY_YELLOW, stroke_width=0) # stroke_width=0 通常意味着不可见 # 摘自 27_find the equation of the line.txt point1 = Dot(axes.coords_to_point(-3, 5), color=self.point_color) line = Line(self.axes.coords_to_point(-3, 5), self.axes.coords_to_point(2, -1), color=BLACK) # 摘自 22_refraction_of_light.txt interface = Line([diagram_center_x - diagram_width / 2, interface_y, 0], [diagram_center_x + diagram_width / 2, interface_y, 0], color=MY_DARK_TEXT) air_rect = Rectangle(width=diagram_width, height=air_height, fill_color=MY_LIGHT_BLUE_BG, fill_opacity=MY_AIR_ALPHA)
注意:坐标通常是 NumPy 数组,例如
RIGHT
(即np.array([1,0,0])
),[x, y, z]
。角度与标记 (Angles & Markers)
Angle(line1, line2, radius=半径, color=颜色)
: 创建两条线之间的角弧。RightAngle(line1, line2, length=长度, color=颜色)
: 创建直角标记。Brace(mobject, direction=方向向量, color=颜色)
: 在一个 Mobject旁边创建花括号。
# 摘自 26_trigonometric_function.txt angle = Angle( Line(triangle.get_vertices()[0], triangle.get_vertices()[1]), Line(triangle.get_vertices()[0], triangle.get_vertices()[2]), color=BLACK ) right_angle = RightAngle( Line(triangle.get_vertices()[1], triangle.get_vertices()[2]), Line(triangle.get_vertices()[1], triangle.get_vertices()[0]), color=BLACK ) # 摘自 23_what_is_the_vector.txt brace_mag = Brace(vector_arrow, direction=vector_arrow.copy().rotate(PI/2).get_unit_vector(), color=BLACK)
坐标系 (Coordinate Systems)
Axes(x_range=[xmin, xmax, xstep], y_range=[ymin, ymax, ystep], x_length=长度, y_length=长度, axis_config={...}, ...)
: 创建二维坐标轴。NumberLine(x_range=[xmin, xmax, xstep], length=长度, include_numbers=True, ...)
: 创建数轴。
# 摘自 25_Parabola_equation_function_graph_changes.txt axes = Axes( x_range=[-4, 4, 1], y_range=[-7, 7, 2], x_length=5, y_length=5, axis_config={"color": BLACK, "include_tip": True, "stroke_width": 2}, tips=True, x_axis_config={"stroke_width": 2, "color": BLACK}, y_axis_config={"stroke_width": 2, "color": BLACK} ).add_coordinates() # add_coordinates() 会添加刻度数字 # 摘自 28_find_the_value_of_function.txt number_line = NumberLine( x_range=[-6, 12, 1], length=8, include_numbers=True, color=BLACK )
2.2 Mobject 的属性与方法
每个 Mobject 都有一系列属性(如颜色、位置、大小)和方法(如移动、旋转、缩放)。
颜色 (Color):
mobject.set_color(NEW_COLOR)
或mobject.color = NEW_COLOR
fill_color
,stroke_color
fill_opacity
,stroke_opacity
- 颜色可以用预定义常量(
RED
,BLUE
,WHITE
,BLACK
等)或十六进制字符串("#FF0000"
)表示。 - 您提供的脚本中大量使用了自定义颜色常量,如
MY_SUN_YELLOW
(来自24_why_sky_is_blue.txt
)。
位置 (Positioning):
mobject.move_to(坐标点或另一个Mobject)
: 移动到指定位置。mobject.shift(方向向量)
: 按向量平移,如mobject.shift(UP * 2)
。mobject.to_edge(方向, buff=间距)
: 移动到屏幕边缘,如mobject.to_edge(LEFT, buff=0.5)
。mobject.next_to(另一个Mobject, direction=方向, buff=间距)
: 放置在另一个 Mobject旁边。mobject.align_to(另一个Mobject, 方向)
: 对齐。mobject.get_center()
,mobject.get_left()
,mobject.get_right()
,mobject.get_top()
,mobject.get_bottom()
: 获取 Mobject 的特定点坐标。
# 摘自 24_why_sky_is_blue.txt sun = Circle(...).shift(LEFT * 5 + UP * 2) # 创建后立即平移 atmosphere_label = Text("Atmosphere", font_size=24).next_to(atmosphere, RIGHT, buff=MED_LARGE_BUFF) # 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt new_scene_num = Text(...).to_corner(UR, buff=MED_LARGE_BUFF)
大小与变换 (Size & Transformation):
mobject.scale(缩放因子)
: 缩放。mobject.scale(0.5)
缩小一半。mobject.rotate(角度, axis=旋转轴, about_point=旋转中心)
: 旋转。角度用弧度表示 (PI
,DEGREES
)。mobject.flip(axis=翻转轴)
: 翻转。
# 摘自 24_why_sky_is_blue.txt prism = Triangle(...).scale(0.8).rotate(PI/2) # 链式调用
Z-Index (叠放次序):
mobject.set_z_index(值)
: 控制 Mobject 的叠放顺序,值越大越靠前。
# 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt new_scene_num.set_z_index(100)
2.3 Mobject 组合:VGroup
VGroup
(Vectorized Group) 是一个非常有用的 Mobject,它可以将多个 Mobject 组合成一个单一的 Mobject。对 VGroup
进行的操作(如移动、缩放、变色)会同时应用于其包含的所有子 Mobject。
# 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt
left_vgroup = VGroup(
kr_formula_text, kr_formula,
conversion_intro_text, omega_given, omega_conversion_formula, omega_conversion_calc, omega_converted_value,
).arrange(DOWN, buff=0.25, aligned_edge=LEFT) # arrange 用于排列组内元素
# 播放动画时,可以对整个组进行操作
self.play(FadeOut(left_vgroup))
VGroup(mobj1, mobj2, ...)
: 创建一个包含这些 Mobject 的组。vgroup.add(mobj)
: 向组中添加 Mobject。vgroup.remove(mobj)
: 从组中移除 Mobject。vgroup.arrange(direction, buff=间距, aligned_edge=对齐边缘)
: 自动排列组内的 Mobject。direction
: 如DOWN
,RIGHT
,UP
,LEFT
。aligned_edge
: 如LEFT
,RIGHT
,CENTER
。
2.4 坐标与向量常量
Manim 提供了一些方便的向量常量来表示方向和位置:
ORIGIN
:np.array([0,0,0])
,原点。UP
:np.array([0,1,0])
DOWN
:np.array([0,-1,0])
LEFT
:np.array([-1,0,0])
RIGHT
:np.array([1,0,0])
UL
:UP + LEFT
(左上)DR
:DOWN + RIGHT
(右下)PI
: 数学常数 π。DEGREES
:PI / 180
,用于将角度转换为弧度 (例如30 * DEGREES
)。TAU
:2 * PI
。
2.5 ValueTracker
和 always_redraw
(动态更新)
当需要 Mobject 根据某个变化的值动态更新时,ValueTracker
和 always_redraw
非常有用。这在 25_Parabola_equation_function_graph_changes.txt
中有很好的体现。
# 摘自 25_Parabola_equation_function_graph_changes.txt
b_tracker = ValueTracker(0) # 创建一个值为0的追踪器
# label 会根据 b_tracker 的值自动重绘
label = always_redraw(
lambda: MathTex(f"b = {b_tracker.get_value():.1f}", color=PURPLE)
)
# parabola 也会根据 b_tracker 的值自动重绘
parabola = always_redraw(
lambda: axes.plot(
lambda x: a_val * x ** 2 + b_tracker.get_value() * x + c_val,
color=PURPLE,
x_range=plot_x_range
)
)
self.add(parabola, label) # 添加到场景后,它们会在每一帧自动更新
# 改变 b_tracker 的值,label 和 parabola 会随之变化
self.play(b_tracker.animate.set_value(3), run_time=3.0)
ValueTracker(initial_value)
: 创建一个可以追踪数值变化的对象。tracker.get_value()
: 获取追踪器的当前值。tracker.set_value(new_value)
: 设置追踪器的新值 (通常在动画中使用tracker.animate.set_value(...)
)。always_redraw(lambda_function)
: 创建一个 Mobject,该 Mobject 的外观由lambda_function
返回的 Mobject 决定。这个函数会在每一帧被调用,从而实现动态更新。
本章小结
- Mobject 是 Manim 中所有可见元素的基础。
- Manim 提供了丰富的 Mobject 类型,包括文本、公式、几何图形和坐标系。
- Mobject 具有颜色、位置、大小等属性,并可以通过方法进行变换。
VGroup
用于组合和管理多个 Mobject。ValueTracker
和always_redraw
可以创建动态更新的 Mobject。
在下一章,我们将学习如何让这些 Mobject 动起来,探索 Manim 强大的动画系统。
第三章:赋予生命——Manim 动画
Manim 的核心魅力在于其强大的动画能力。通过 self.play()
方法,我们可以将 Mobject 的创建、变换和消失过程以动画形式展现出来。
3.1 self.play()
的基本用法
self.play(Animation1, Animation2, ..., run_time=秒数, rate_func=速率函数)
Animation
: Manim 中的动画类实例,例如Write(text_mobject)
。- 可以同时播放多个动画,它们会并行执行。
run_time
: 动画的持续时间,单位是秒。默认通常是1秒。rate_func
: 控制动画的速率曲线,例如:linear
: 线性,匀速。smooth
: 平滑,两头慢中间快 (默认)。rush_into
: 开始快,然后减速。rush_from
: 开始慢,然后加速。there_and_back
: 先正向再反向。- 等等...
3.2 常见的动画类型
创建与书写 (Creation & Writing)
Create(mobject)
: 逐渐显示一个 Mobject,通常用于几何图形。Write(text_or_mathtex_mobject)
: 模拟笔迹书写文本或公式。DrawBorderThenFill(mobject)
: 先绘制边框,然后填充。ShowCreation(mobject)
: 与Create
类似,但有时效果略有不同。
# 摘自 29_find_angles_of_triangles.txt title = Text("Triangle Angle Problem", font_size=38, color=BLACK) problem = Text("In triangle ABC...", font_size=28, color=BLACK) triangle = Triangle(fill_opacity=0.2, color=self.triangle_color) self.play(Write(title), run_time=1) self.play(Write(problem), run_time=2) self.play(Create(triangle), run_time=1.5) # 摘自 27_find the equation of the line.txt # self.play(Create(axes), run_time=1) # self.play(Create(line), run_time=1.5)
消失与移除 (Fading & Removal)
FadeOut(mobject)
: 将 Mobject 淡出。Uncreate(mobject)
:Create
的逆向动画。FadeOutAndShift(mobject, direction=向量)
: 淡出并向指定方向移动。
# 摘自 29_find_angles_of_triangles.txt # previous_objects = VGroup(self.problem, self.triangle, self.labels) # self.play(FadeOut(previous_objects), run_time=1) # 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt # if self.current_scene_num_mob: # animations.append(FadeOut(self.current_scene_num_mob, run_time=0.5))
引入 (Introduction)
FadeIn(mobject)
: 将 Mobject 淡入。FadeInFrom(mobject, direction=向量)
: 从指定方向淡入。GrowFromCenter(mobject)
: 从中心放大出现。GrowFromEdge(mobject, edge=方向)
: 从指定边缘生长出现。
# 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt # self.play(FadeIn(kr_formula_text, shift=UP * 0.2), run_time=0.7) # shift=UP*0.2 是 FadeIn 的一个参数,表示淡入时向上轻微移动 # 摘自 23_what_is_the_vector.txt # self.play(FadeIn(desc_group[0]), GrowFromCenter(brace_mag), FadeIn(brace_mag_label), run_time=2.0)
变换 (Transformation)
Transform(mobject1, mobject2)
: 将mobject1
平滑地变成mobject2
。mobject1
会从场景中移除,mobject2
会被添加。ReplacementTransform(mobject1, mobject2)
: 与Transform
类似,但更强调替换的语义。TransformMatchingTex(mathtex1, mathtex2)
: 专门用于MathTex
对象的变换,它会尝试匹配 LaTeX 字符串中相同的部分,使变换更自然。TransformMatchingShapes(shape1, shape2)
: 类似地,用于形状的智能匹配变换。
# 摘自 25_Parabola_equation_function_graph_changes.txt # graph = axes.plot(lambda x: x ** 2, color=BLUE, x_range=plot_x_range) # graph_a2 = axes.plot(lambda x: 2 * x ** 2, x_range=plot_x_range) # label = MathTex("a=1", color=BLUE) # label_a2 = MathTex("a=2", color=GREEN) # self.play(Transform(graph, graph_a2), Transform(label, label_a2), run_time=1.5) # 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt # omega_conversion_formula = MathTex(r"\omega (\text{rad/s}) = ...", font_size=32) # omega_conversion_calc = MathTex(r"= 1200 \times \frac{2\pi}{60} ...", font_size=32) # self.play(TransformMatchingTex(omega_conversion_formula.copy(), omega_conversion_calc), run_time=1.2) # 注意 .copy() 的使用:Transform 会消耗原始对象,如果后续还需要原始对象,或者要基于原始对象进行多次变换,就需要复制。
移动与路径 (Movement & Paths)
mobject.animate.shift(向量)
mobject.animate.move_to(目标点)
mobject.animate.scale(缩放因子)
mobject.animate.rotate(角度)
mobject.animate.set_color(颜色)
MoveAlongPath(mobject, path_mobject)
: 让 Mobject 沿着一个路径 Mobject (如Line
,Arc
,VMobject
) 移动。
# 假设有一个 circle 对象 # self.play(circle.animate.shift(RIGHT * 2).scale(0.5).set_color(RED), run_time=2) # .animate 语法允许我们将 Mobject 的属性修改方法变成动画。 # 摘自 25_Parabola_equation_function_graph_changes.txt (ValueTracker 驱动的动画) # b_tracker = ValueTracker(0) # self.play(b_tracker.animate.set_value(3), run_time=3.0) # 这里是 ValueTracker 对象的 .animate 接口
强调 (Emphasis)
Indicate(mobject, color=高亮色, scale_factor=缩放因子)
: 短暂高亮一个 Mobject (通常是放大一点并变色,然后恢复)。Flash(mobject_or_point, color=闪光色, flash_radius=半径)
: 在 Mobject 或某点处产生一个闪光效果。FocusOn(mobject_or_point)
: 将镜头焦点移向某处。Circumscribe(mobject, color=圆圈颜色)
: 用一个圆圈框住 Mobject。
# 摘自 22_refraction_of_light.txt # self.play(Indicate(snell_law_formula.get_part_by_tex("n_1"), color=MY_DARK_TEXT), run_time=0.8) # 摘自 27_find the equation of the line.txt # self.play( # Flash(self.point1, color=self.point_color, flash_radius=0.5), # Flash(self.point2, color=self.point_color, flash_radius=0.5), # run_time=1.5 # )
3.3 动画组与同步
AnimationGroup(*animations, lag_ratio=延迟比例)
: 将多个动画组合在一起播放。lag_ratio
控制组内动画的启动延迟。lag_ratio = 0
: 所有动画同时开始。lag_ratio = 1
: 动画依次播放,一个结束后下一个开始。0 < lag_ratio < 1
: 动画之间有部分重叠。
# 摘自 22_refraction_of_light.txt # self.play(AnimationGroup( # Create(red_ray), # Create(green_ray), # Create(blue_ray), # lag_ratio=0.3 # 红、绿、蓝光线依次出现,但有重叠 # ), run_time=2.0) # 摘自 24_why_sky_is_blue.txt # self.play(AnimationGroup(*[FadeIn(p, shift=UP*0.1) for p in summary_group], lag_ratio=0.3), run_time=4.0) # 列表推导式与 * 解包操作,优雅地为 VGroup 中的每个元素创建 FadeIn 动画
同步语音与动画: 您提供的脚本中广泛使用了
custom_voiceover_tts
上下文管理器来同步语音和动画。这是一个非常实用的自定义工具。其核心思想是:- 获取语音的持续时间
tracker.duration
。 - 计算动画的总持续时间
anim_duration
。 - 通过
self.wait(max(0, tracker.duration - anim_duration))
来确保动画播放的总时长与语音时长匹配。如果动画比语音短,就等待差值;如果动画比语音长,则语音可能会提前结束。
# 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt # voice_text_s02_p1 = "对于问题 (a),..." # with custom_voiceover_tts(voice_text_s02_p1) as tracker: # if tracker.audio_path and tracker.duration > 0: self.add_sound(tracker.audio_path) # self.play(FadeIn(kr_formula_text, shift=UP * 0.2), run_time=0.7) # self.play(Write(kr_formula), run_time=1.2) # anim_duration = 0.7 + 1.2 # wait_time = max(0.001, tracker.duration - anim_duration) if tracker.duration > 0 else 0.5 # self.wait(wait_time)
虽然
custom_voiceover_tts
是自定义的,但这种同步逻辑是通用的。- 获取语音的持续时间
3.4 self.add()
与 self.remove()
有时候,我们希望一个 Mobject 立即出现在屏幕上或从屏幕上消失,而不需要动画效果。
self.add(mobject1, mobject2, ...)
: 将一个或多个 Mobject 添加到场景中,它们会立即显示。self.remove(mobject1, mobject2, ...)
: 将一个或多个 Mobject 从场景中移除,它们会立即消失。
这在管理动态更新的 Mobject (如 always_redraw
创建的) 或在复杂动画序列中切换元素时很有用。
# 摘自 25_Parabola_equation_function_graph_changes.txt
# label = always_redraw(...)
# parabola = always_redraw(...)
# self.add(parabola, label) # 添加后,它们会在每一帧根据 ValueTracker 自动更新
本章小结
self.play()
是执行动画的核心方法。- Manim 提供了丰富的动画类型,用于创建、消失、变换和强调 Mobject。
.animate
语法提供了一种便捷的方式来动画化 Mobject 的属性变化。AnimationGroup
可以组合多个动画并控制它们的播放顺序。- 通过计算动画和语音时长,可以实现两者之间的同步。
self.add()
和self.remove()
用于即时显示或隐藏 Mobject。
下一章,我们将学习如何在 Manim 中绘制图形和函数,这对于数学可视化至关重要。
第四章:绘制图形与函数——坐标系的力量
数学可视化离不开坐标系和函数图像。Manim 提供了强大的工具来创建和操作它们。
4.1 Axes
:二维坐标系
Axes
类用于创建标准的二维笛卡尔坐标系。
基本创建:
# 摘自 25_Parabola_equation_function_graph_changes.txt axes = Axes( x_range=[-4, 4, 1], # x轴范围:[最小值, 最大值, 步长] y_range=[-7, 7, 2], # y轴范围 x_length=5, # x轴在屏幕上的显示长度 y_length=5, # y轴在屏幕上的显示长度 axis_config={"color": BLACK, "include_tip": True, "stroke_width": 2}, # 坐标轴通用配置 tips=True, # 是否显示箭头 (Manim CE 中 tips 默认为 True, include_tip 更常用) x_axis_config={"stroke_width": 2, "color": BLACK}, # x轴特定配置 y_axis_config={"stroke_width": 2, "color": BLACK} # y轴特定配置 ).add_coordinates() # .add_coordinates() 会自动添加刻度数字
x_range
和y_range
定义了坐标系的数学范围和刻度步长。x_length
和y_length
定义了坐标轴在屏幕上的物理长度。axis_config
可以统一配置两条轴的样式。x_axis_config
和y_axis_config
可以分别配置 x 轴和 y 轴。.add_coordinates()
是一个方便的方法,用于在轴上添加刻度值。你也可以通过x_axis_config={"include_numbers": True}
来实现。
坐标轴标签: 通常使用
Text
或MathTex
创建标签,然后用.next_to()
定位。# 摘自 27_find the equation of the line.txt x_label = Text("x") y_label = Text("y") x_label.next_to(axes.x_axis.get_end(), RIGHT) y_label.next_to(axes.y_axis.get_end(), UP)
axes.x_axis
和axes.y_axis
是Axes
对象内部的NumberLine
对象,代表 x 轴和 y 轴本身。手动添加刻度标签 (如果不想用
add_coordinates
或需要更精细的控制):26_trigonometric_function.txt
和21_Geometric_Meaning_of_Euler_s_Formula.txt
展示了如何手动为Axes
添加刻度标签,特别是当标签包含 π 时。# 摘自 26_trigonometric_function.txt (略作简化) axes = Axes( x_range=[0, 2 * PI + 0.1, PI / 2], y_range=[-1.5, 1.5, 1], # ... axis_config={"include_numbers": False}, # 禁用自动数字 ) x_labels_mobs = VGroup(*( # 使用 VGroup 组合所有标签 MathTex(tex_str, font_size=24, color=BLACK) .next_to(axes.c2p(x_val, 0), DOWN, buff=SMALL_BUFF) for x_val, tex_str in ( # 元组列表,定义 (数学值, LaTeX字符串) (0, r"0"), (PI / 2, r"\frac{\pi}{2}"), (PI, r"\pi"), (3 * PI / 2, r"\frac{3\pi}{2}"), (2 * PI, r"2\pi"), ) )) # y_labels_mobs 类似创建 self.play(Create(axes), Write(x_labels_mobs), Write(y_labels_mobs))
这里使用了
axes.c2p(x, y)
方法,见下节。
4.2 坐标转换:coords_to_point
(c2p
) 和 point_to_coords
(p2c
)
Axes
对象提供了在数学坐标和屏幕坐标之间转换的方法:
axes.coords_to_point(x, y)
或axes.c2p(x, y)
: 将数学坐标(x, y)
转换为屏幕上的点 (一个 NumPy 数组[screen_x, screen_y, 0]
)。axes.point_to_coords(point)
或axes.p2c(point)
: 将屏幕上的点转换回数学坐标。
这在需要在坐标系上精确放置 Dot
、Text
或其他 Mobject 时非常有用。
# 摘自 27_find the equation of the line.txt
point1_coords = (-3, 5)
point1_on_screen = axes.coords_to_point(point1_coords[0], point1_coords[1])
point1_dot = Dot(point1_on_screen, color=self.point_color)
# 摘自 21_Geometric_Meaning_of_Euler_s_Formula.txt (单位圆上的点)
# theta_value = PI / 3
# point_x = np.cos(theta_value)
# point_y = np.sin(theta_value)
# point_P_coord_on_screen = axes.c2p(point_x, point_y)
# self.point_P = Dot(point_P_coord_on_screen, color=BLUE)
4.3 绘制函数图像:axes.plot()
axes.plot(function, x_range=[xmin, xmax], color=颜色, ...)
方法用于绘制函数图像。
# 摘自 25_Parabola_equation_function_graph_changes.txt
# a_val = 1, b_val = 0, c_val = 0
parabola_function = lambda x: a_val * x ** 2 + b_val * x + c_val
graph = axes.plot(parabola_function, color=BLUE, x_range=[-2.5, 2.5])
self.play(Create(graph))
function
: 一个 Python lambda 函数或普通函数,接受一个参数 (x 值) 并返回 y 值。x_range
: 可选参数,指定绘制函数的 x 值范围。如果未提供,则使用Axes
的x_range
。
4.4 NumberLine
:数轴
NumberLine
用于创建一维数轴。它的配置与 Axes
中的单轴类似。
# 摘自 28_find_the_value_of_function.txt
number_line = NumberLine(
x_range=[-6, 12, 1], # 数学范围和步长
length=8, # 屏幕显示长度
include_numbers=True, # 显示刻度数字
color=BLACK
)
# 在数轴上标点
x_point_math_coord = 4
x_point_screen_coord = number_line.n2p(x_point_math_coord) # number_to_point
x_dot = Dot(x_point_screen_coord, color=BLACK)
x_label = MathTex("x = 4").next_to(x_dot, DOWN)
number_line.n2p(number)
: 将数轴上的一个数字转换为屏幕上的点。number_line.p2n(point)
: 将屏幕上的点转换为数轴上的数字。
4.5 单位圆示例 (结合 Axes
和 Circle
)
21_Geometric_Meaning_of_Euler_s_Formula.txt
和 26_trigonometric_function.txt
都展示了如何在 Axes
上绘制单位圆。
# 摘自 26_trigonometric_function.txt (单位圆部分)
axes = Axes(
x_range=[-1.5, 1.5, 1], y_range=[-1.5, 1.5, 1], x_length=5, y_length=5,
# ...
)
# 单位圆的半径是1 (数学单位)。需要将其转换为屏幕单位。
# axes.x_axis.unit_size 是 x 轴上一个数学单位对应的屏幕长度。
# axes.y_axis.unit_size 是 y 轴上一个数学单位对应的屏幕长度。
# 如果 x_length/x_range_span != y_length/y_range_span,那么 x 和 y 的 unit_size 可能不同,圆会变形。
# 通常,我们会确保 x_length / (xmax-xmin) == y_length / (ymax-ymin) 来保持比例。
# 或者,如果 Axes 的 x_length 和 y_length 相等,且 x_range 和 y_range 的跨度也设计成相等,
# 那么可以直接使用 x_axis.unit_size 或 y_axis.unit_size。
# 一个更稳妥的方式是,如果圆心在原点:
radius_on_screen = np.linalg.norm(axes.c2p(1, 0) - axes.c2p(0, 0)) # 计算原点到(1,0)的屏幕距离
circle = Circle(radius=radius_on_screen, stroke_width=2).move_to(axes.c2p(0,0)) # 圆心在原点
# 动态点 P 在单位圆上
theta_tracker = ValueTracker(0) # 角度追踪器
point_P = Dot(color=BLUE)
point_P.add_updater(
lambda m: m.move_to(axes.c2p(np.cos(theta_tracker.get_value()), np.sin(theta_tracker.get_value())))
)
# 其他元素如半径、投影线、角度标签等也使用 add_updater 动态更新
这个例子展示了如何结合 Axes
的坐标转换、Circle
的创建以及 ValueTracker
和 add_updater
(或 always_redraw
) 来实现动态的单位圆可视化。
4.6 绘制参数方程图像:axes.plot_parametric_curve()
虽然您提供的脚本中没有直接使用 plot_parametric_curve
,但它是绘制更复杂曲线(如圆、椭圆、螺旋线等)的有用工具。 axes.plot_parametric_curve(function, t_range=[tmin, tmax, tstep], ...)
function
: 一个接受参数t
并返回(x, y)
坐标对的函数。
例如,绘制单位圆:
unit_circle_parametric = axes.plot_parametric_curve(
lambda t: np.array([np.cos(t), np.sin(t), 0]), # 返回 [x,y,z]
t_range=[0, TAU, 0.01], # t 从 0 到 2π
color=RED
)
本章小结
Axes
类用于创建二维坐标系,可以配置范围、长度和样式。axes.c2p()
和axes.p2c()
在数学坐标和屏幕坐标间转换。axes.plot()
用于绘制标准函数y = f(x)
的图像。NumberLine
用于创建数轴,n2p()
和p2n()
进行坐标转换。- 结合
Circle
、ValueTracker
和updater
可以在Axes
上创建动态的单位圆。 axes.plot_parametric_curve()
可以绘制参数方程定义的曲线。
在下一章,我们将探讨如何组织和管理复杂的场景,特别是当场景中包含大量元素或多个动画阶段时。
第五章:组织复杂场景——布局与管理
当动画变得复杂,包含许多元素或多个阶段时,良好的组织结构至关重要。本章将探讨如何使用 VGroup
、自定义布局类以及将场景分解为多个方法来管理复杂性。
5.1 VGroup
:Mobject 的容器
我们之前已经接触过 VGroup
,它是组织多个 Mobject 的基本工具。
- 集中控制:对
VGroup
进行的变换(移动、缩放、旋转、变色)会应用到其所有子 Mobject。 - 批量动画:可以对整个
VGroup
应用一个动画,如FadeIn(my_vgroup)
。 - 排列:
vgroup.arrange(direction, buff, aligned_edge)
方法可以方便地排列组内元素。
# 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt
left_vgroup = VGroup(
kr_formula_text, kr_formula,
conversion_intro_text, omega_given, omega_conversion_formula, omega_conversion_calc, omega_converted_value,
).arrange(DOWN, buff=0.25, aligned_edge=LEFT)
right_vgroup = VGroup(
kr_calc_intro_text, kr_substitution, kr_calc_step1, kr_calc_step2, kr_final_exact,
kr_approx_intro, kr_approx_calc, kr_approx_value
).arrange(DOWN, buff=0.25, aligned_edge=LEFT)
# 之后可以将这些 VGroup 放置到布局区域
# layout["content_area"]["left_area"].place(left_vgroup, aligned_edge=UL, buff=0.3)
# layout["content_area"]["right_area"].place(right_vgroup, aligned_edge=UL, buff=0.3)
这里,多个 Text
和 MathTex
对象被分别组合到 left_vgroup
和 right_vgroup
中,并使用 arrange
方法进行垂直排列和左对齐。
5.2 自定义 Layout
类:屏幕区域划分
您提供的许多脚本(如 31_...
, 25_...
, 22_...
, 24_...
, 23_...
, 26_...
, 27_...
, 28_...
, 29_...
, 30_...
, 21_...
)都使用了一个名为 Layout
的自定义类。这个类(通常在 manim_utils.py
中定义,这里我们根据用法推断其功能)极大地简化了屏幕空间的管理。
- 目的:将屏幕划分为命名区域,并按比例分配空间,使得 Mobject 可以方便地放置到这些区域中,而无需手动计算绝对坐标。
- 典型用法:
- 定义布局结构:使用嵌套字典描述区域的层次、方向(垂直/水平)和相对权重/大小。
- 解析布局:调用
.resolve(self)
(self
指场景对象)来根据当前屏幕尺寸计算各区域的实际边界。 - 放置 Mobject:使用
layout["area_name"].place(mobject, ...)
将 Mobject 放入指定区域。
# 摘自 25_Parabola_equation_function_graph_changes.txt
# 假设 Layout, LayoutAtom, LayoutDirection 已定义
layout = Layout(LayoutDirection.VERTICAL, { # 顶层是垂直布局
"title": (1.5, LayoutAtom()), # "title"区域,权重1.5,是一个原子布局单元
"body": (7.0, Layout(LayoutDirection.HORIZONTAL, { # "body"区域,权重7.0,内部是水平布局
"text_group": (5.0, LayoutAtom()), # "body"内的"text_group"区域,权重5.0
"graph_group": (3.0, Layout(LayoutDirection.VERTICAL, { # "body"内的"graph_group"区域,权重3.0,内部垂直
"formula": (1.0, LayoutAtom()),
"label": (1.0, LayoutAtom()),
"graph": (6.0, LayoutAtom()),
})),
})),
}).resolve(self) # self 是场景对象
title_mobject = Title("Parabola Equation Changes")
layout["title"].place(title_mobject) # 将 title_mobject 放入 "title" 区域
text_content = VGroup(...)
layout["body"]["text_group"].place(text_content, aligned_edge=UL, buff=0.5) # 放入嵌套区域
axes_mobject = Axes(...)
layout["body"]["graph_group"]["graph"].place(axes_mobject) # 放入更深层嵌套区域
LayoutDirection.VERTICAL
/LayoutDirection.HORIZONTAL
: 指定子区域是垂直排列还是水平排列。LayoutAtom()
: 表示一个基本的、不可再分的布局单元。- 字典的键是区域名称,值是一个元组
(weight, sub_layout_or_atom)
。weight
用于按比例分配空间。 .place()
方法通常还允许指定对齐方式 (aligned_edge
) 和边距 (buff
)。
虽然这个 Layout
类是自定义的,但它体现了一种重要的设计模式:声明式布局。你描述了你想要的布局结构,而不是通过命令式地计算每个元素的坐标。这使得代码更易读、更易维护,并且能更好地适应不同的屏幕比例。
5.3 将场景分解为多个方法
对于较长的动画,将所有逻辑都放在一个 construct
方法中会使其变得臃肿难读。一个好的做法是将动画分解为多个逻辑阶段,每个阶段用一个单独的方法实现。
您提供的脚本广泛采用了这种模式,例如:
29_find_angles_of_triangles.txt
:play_scene_01()
,play_scene_02()
,play_scene_03()
,play_scene_04()
25_Parabola_equation_function_graph_changes.txt
:play_section_01()
,play_section_02()
, ...22_refraction_of_light.txt
:play_scene_01()
,play_scene_04()
,play_scene_05()
(注意编号不连续,但逻辑上是分段的)
# 摘自 29_find_angles_of_triangles.txt
class CombinedScene(Scene):
def __init__(self):
super().__init__()
self.current_scene_num_mob = None
# ... 其他初始化 ...
def construct(self):
self.camera.background_color = WHITE
# 按顺序调用各个场景片段
self.play_scene_01() # Problem statement
self.play_scene_02() # Setup equation
self.play_scene_03() # Solve equation
self.play_scene_04() # Draw triangle with correct angles
self.fadeout_all() # Clean fadeout at the end
def update_scene_number(self, number_str):
# ... (如第一章所述) ...
pass
def play_scene_01(self):
self.update_scene_number("01")
# ... 场景1的逻辑 ...
# 将场景1中创建的关键 Mobject 保存为 self.属性,以便后续场景片段使用
self.title = title_mobject
self.problem = problem_mobject
# ...
pass
def play_scene_02(self):
self.update_scene_number("02")
# ... 场景2的逻辑 ...
# 可能需要 FadeOut 场景1中的某些元素
# self.play(FadeOut(self.problem), ...)
# 然后创建和动画化场景2的元素
pass
# ... 其他 play_scene_XX 方法 ...
def fadeout_all(self):
"""Fadeout all elements for a clean ending."""
self.play(FadeOut(*self.mobjects), run_time=1.5) # *self.mobjects 获取场景中所有顶层 Mobject
- 主
construct
方法: 变得非常简洁,只负责按顺序调用各个片段方法。 - 片段方法 (
play_scene_XX
): 每个方法负责一部分动画逻辑。- 通常以
self.update_scene_number()
(如果使用) 开始。 - 创建和动画化该片段所需的 Mobject。
- 如果需要,将关键 Mobject 保存为
self.attribute_name
,以便其他片段方法可以访问或移除它们。 - 在片段开始时,可能需要
FadeOut
上一个片段中不再需要的元素。
- 通常以
- 状态管理: 通过
self.attribute_name
在不同片段方法之间共享 Mobject 状态。 - 清理:
fadeout_all()
或类似方法用于在整个动画结束时清理屏幕。
5.4 清理屏幕元素 (clear_section
/ clear_and_reset
)
在分段场景中,管理哪些元素应该保留、哪些应该移除是很重要的。您提供的 22_refraction_of_light.txt
和 23_what_is_the_vector.txt
中有名为 clear_and_reset
或 clear_section
的方法。
# 摘自 22_refraction_of_light.txt
class CombinedScene(Scene):
def setup(self):
# ...
self.diagram_elements = VGroup() # 用于存储当前“部分”的元素
def clear_and_reset(self):
mobjects_to_clear = list(self.mobjects)
for mob in mobjects_to_clear:
if mob is not None and hasattr(mob, 'get_updaters') and mob.get_updaters():
mob.clear_updaters() # 清除 updater,防止错误
valid_mobjects = [m for m in self.mobjects if m is not None]
if valid_mobjects:
self.play(FadeOut(Group(*valid_mobjects)), run_time=0.5) # 淡出所有当前屏幕上的对象
self.clear() # Manim Scene 的方法,移除所有 mobjects 和 updaters
self.diagram_elements = VGroup() # 重置存储器
self.wait(0.1)
def play_scene_01(self):
self.update_scene_number("01")
# ... 创建 title, interface, media_bg, media_labels ...
# self.diagram_elements.add(title, interface, media_bg, ...) # (示例,原脚本可能直接操作 self.mobjects)
# ... 动画 ...
pass # 结束后不清理,元素保留到下一个 clear_and_reset 调用
def construct(self):
self.play_scene_01()
self.clear_and_reset() # 清理场景1的元素
self.play_scene_04()
self.clear_and_reset() # 清理场景4的元素
# ...
23_what_is_the_vector.txt
中的 clear_section
稍微不同,它操作的是一个名为 self.section_elements
的 VGroup
,只清理这个组中的元素,而不是整个屏幕。这允许在不同“部分”之间保留一些共同的元素(如标题)。
# 摘自 23_what_is_the_vector.txt
class CombinedScene(Scene):
def setup(self):
# ...
self.section_elements = VGroup() # 用于存储当前“部分”的元素
def clear_section(self):
# ... (清除 self.section_elements 中的 updater) ...
valid_elements = [elem for elem in self.section_elements if elem is not None]
if valid_elements:
self.play(FadeOut(Group(*valid_elements)), run_time=0.75)
self.section_elements = VGroup() # 重置
self.wait(0.1)
def play_scene_01(self): # 介绍部分
self.update_scene_number("01")
title = Text("What is a Vector?")
# self.add(title) # 将标题直接添加到场景,不加入 self.section_elements
definition_text = Text("A vector is ...")
vector_arrow = Arrow(...)
self.section_elements.add(definition_text, vector_arrow) # 这些会被 clear_section 清理
# ... 动画 ...
pass
def construct(self):
self.play_scene_01()
self.clear_section() # 清理 definition_text 和 vector_arrow,但 title 还在
# ...
选择哪种清理方式取决于你的具体需求。
本章小结
VGroup
是组织和批量操作 Mobject 的基础。- 自定义
Layout
类(或类似的机制)有助于声明式地管理屏幕空间,使复杂布局更易于实现和维护。 - 将
construct
方法分解为多个play_scene_XX
或play_section_XX
方法,可以提高代码的可读性和模块性。 - 在分段动画中,需要仔细管理哪些 Mobject 在何时被添加、移除或在不同片段间共享。自定义的清理方法可以帮助实现这一点。
下一章,我们将关注 Manim 的高级定制选项,包括颜色、字体(特别是 CJK)、以及 TexTemplate
的使用。
第六章:个性化你的动画——颜色、字体与模板
Manim 提供了丰富的自定义选项,让你的动画独具风格。本章将重点介绍如何使用自定义颜色、配置字体(特别是中文字体),以及利用 TexTemplate
来精细控制 LaTeX 渲染。
6.1 自定义颜色
Manim 内置了许多颜色常量 (如 RED
, BLUE
, GREEN
, WHITE
, BLACK
, YELLOW
, PURPLE
, ORANGE
, PINK
, TEAL
, GOLD
等)。但为了动画风格的统一或特殊需求,你经常会定义自己的颜色集。
您提供的脚本中,22_refraction_of_light.txt
和 24_why_sky_is_blue.txt
是很好的例子:
# 摘自 22_refraction_of_light.txt
# --- Custom Colors ---
MY_LIGHT_BLUE_BG = "#E3F2FD" # Light blue background
MY_WATER_BLUE = "#90CAF9" # Water medium color
MY_AIR_ALPHA = 0.0 # Air is transparent
MY_DARK_TEXT = "#1E293B" # Dark text color
MY_HIGHLIGHT_RED = "#E53935" # Incident angle, etc.
MY_RAY_YELLOW = "#FFCA28" # Light ray color
# ... 其他颜色定义 ...
class CombinedScene(Scene):
def setup(self):
# ...
self.camera.background_color = MY_LIGHT_BLUE_BG # 使用自定义背景色
# ...
def play_scene_01(self):
# ...
title = Title("Refraction of Light", font_size=48, color=MY_DARK_TEXT) # 使用自定义文本颜色
interface = Line(..., color=MY_DARK_TEXT)
water_rect = Rectangle(..., fill_color=MY_WATER_BLUE, fill_opacity=0.3)
incident_ray = Arrow(..., color=MY_RAY_YELLOW)
angle_inc_arc = Arc(..., color=MY_HIGHLIGHT_RED)
# ...
- 定义方式:通常在脚本的开头将颜色值赋给大写常量名。颜色值可以是:
- 十六进制字符串:如
"#E3F2FD"
。 - Manim 预定义颜色:如
BLUE
。 - RGB 元组:如
(1.0, 0.5, 0.0)
(每个分量在 0 到 1 之间)。
- 十六进制字符串:如
- 使用:在创建 Mobject 或设置其属性时,直接使用这些自定义常量。
- 好处:
- 一致性:确保整个动画或系列动画的颜色风格统一。
- 易修改:如果想改变某个主题色,只需修改顶部的定义即可,无需在代码中多处查找替换。
- 可读性:
MY_WATER_BLUE
比"#90CAF9"
更能表达颜色的用途。
6.2 字体配置
Text
对象的字体: 对于通过Text
创建的 Mobject(不使用 LaTeX 渲染),可以直接在创建时或通过Text.set_default()
指定字体。# 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt # from manim_utils import get_available_font # 假设的工具函数 class CombinedScene(Scene): def __init__(self, **kwargs): super().__init__(**kwargs) # ... final_font = get_available_font() # 获取一个系统可用的字体名称 if final_font: Text.set_default(font=final_font) # 设置 Text 对象的默认字体 # ... # 单独设置 my_text = Text("特定文本", font="Arial")
get_available_font()
是一个自定义函数,它的作用可能是检查系统中安装了哪些字体,并返回一个可用的字体名称。你需要确保指定的字体名称是你的操作系统和 Manim (底层依赖 Pango/Cairo) 能够识别的。对于中文字体,可以是 "SimSun", "Microsoft YaHei", "PingFang SC", "Songti SC" 等。MathTex
和Tex
对象的字体 (通过 LaTeX): 当文本或公式需要通过 LaTeX 渲染时(主要是为了数学排版或复杂的 CJK 字符支持),字体配置就涉及到TexTemplate
。
6.3 TexTemplate
:自定义 LaTeX 环境
Manim 使用 LaTeX 来渲染 MathTex
和 Tex
对象。通过 TexTemplate
,你可以完全控制用于渲染的 LaTeX 文档的导言区 (preamble) 和编译器。
这对于支持特定语言(如中文)、使用特殊 LaTeX 宏包或自定义文档类非常重要。
# 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt
CJK_FONT_NAME = "Songti SC" # 例如 "SimHei" 或 "Source Han Sans CN"
cjk_template = TexTemplate(
tex_compiler="xelatex", # 推荐使用 xelatex 处理 CJK
output_format=".xdv", # xelatex 的输出格式
preamble=rf"""
\usepackage{{amsmath}}
\usepackage{{amssymb}}
\usepackage{{fontspec}} % 允许使用系统字体
\usepackage{{xeCJK}} % xelatex 的 CJK 处理宏包
\setCJKmainfont{{{CJK_FONT_NAME}}} % 设置 CJK 主字体
% 你还可以添加其他需要的宏包,例如:
% \usepackage{physics} % 物理公式常用
% \usepackage{chemfig} % 化学结构式
"""
)
class CombinedScene(Scene):
def __init__(self, **kwargs):
# ...
MathTex.set_default(tex_template=cjk_template) # 将此模板设为 MathTex 的默认模板
# ...
# 使用示例
chinese_formula = MathTex(r"\text{能量 } E = mc^2", tex_template=cjk_template) # 也可以为单个对象指定模板
# 如果已经设置了默认模板,则无需再次指定:
# default_chinese_formula = MathTex(r"\text{动能 } K_R = \frac{1}{2} I \omega^2")
tex_compiler
:"latex"
: 传统的 LaTeX 编译器。"pdflatex"
: 生成 PDF 的 LaTeX 编译器。"xelatex"
: 对 Unicode 和现代字体 (OpenType, TrueType) 支持良好,是处理中文、日文、韩文等非拉丁字符的首选。"lualatex"
: 另一个现代 LaTeX 编译器,功能与xelatex
类似。
output_format
:".dvi"
:latex
的输出。".pdf"
:pdflatex
的输出。".xdv"
:xelatex
的输出。Manim 之后会将其转换为 SVG。
preamble
: LaTeX 文档的导言区。\usepackage{...}
: 加载 LaTeX 宏包。amsmath
,amssymb
: 数学公式常用。fontspec
:xelatex
或lualatex
用来加载系统字体。xeCJK
:xelatex
用来处理 CJK 字符的宏包。
\setCJKmainfont{字体名称}
: 使用xeCJK
设置 CJK 字符的字体。你需要确保你的系统安装了该字体,并且 LaTeX 能够找到它。\setCJKsansfont{...}
,\setCJKmonofont{...}
: 可以分别设置无衬线和等宽 CJK 字体。rf"""..."""
: Python 的原始多行字符串,非常适合包含 LaTeX 代码,因为反斜杠\
不需要额外转义。
应用模板:
MathTex.set_default(tex_template=my_template)
: 将模板设置为所有MathTex
对象的默认模板。Tex.set_default(tex_template=my_template)
: 设置Tex
对象的默认模板。mobj = MathTex("...", tex_template=my_template)
: 为单个 Mobject 指定模板,会覆盖默认设置。
6.4 manim_utils.py
的作用
在您提供的脚本中,经常出现 from manim_utils import ...
。这个 manim_utils.py
文件(未提供)很可能包含了项目中复用的一些工具函数和类,例如:
get_available_font()
: 获取系统可用字体。custom_voiceover_tts()
: 处理文本到语音及字幕生成。Layout
,LayoutAtom
,LayoutDirection
: 自定义布局系统。Title
: 可能是一个预设样式的Text
或MathTex
子类。
这是一种良好的编程实践,将通用的辅助功能模块化,使得主场景脚本更专注于动画逻辑本身。
本章小结
- 通过定义颜色常量,可以方便地管理和应用自定义颜色方案,增强动画的视觉风格和一致性。
Text
对象的字体可以通过font
参数或Text.set_default(font=...)
设置。- 对于需要 LaTeX 渲染的
MathTex
和Tex
对象,尤其是涉及 CJK 字符时,应使用TexTemplate
进行配置。- 推荐使用
xelatex
编译器和xeCJK
宏包处理中文字体。 - 确保在
preamble
中正确设置 CJK 字体。
- 推荐使用
- 将常用的辅助函数和类放到如
manim_utils.py
这样的模块中,有助于代码的组织和复用。
下一章,我们将通过几个具体的案例分析,看看这些知识点是如何在实际的 Manim 项目中综合运用的。
第七章:实战演练——案例分析
现在,我们已经学习了 Manim 的基础知识,包括场景、Mobject、动画、坐标系、布局和自定义。接下来,我们将深入分析您提供的几个脚本,看看这些概念是如何在实际项目中应用的。
7.1 案例一:求解三角形角度 (源自 29_find_angles_of_triangles.txt
)
这个脚本的目的是求解一个三角形的内角,已知三内角之比为 2:3:4。它清晰地展示了问题陈述、方程建立、求解过程和结果可视化的步骤。
核心知识点回顾:
- 场景分段 (
play_scene_01
到play_scene_04
)。 - 自定义
Layout
类进行屏幕布局。 Text
和MathTex
用于显示问题、公式和解题步骤。- 基本形状
Triangle
,Dot
,Angle
。 VGroup
组织元素。- 动画
Write
,Create
,FadeOut
,TransformMatchingTex
(虽然此脚本中未使用,但类似场景常用)。 - 自定义颜色。
代码片段分析:
场景搭建与问题陈述 (
play_scene_01
)# class CombinedScene(Scene): # def play_scene_01(self): # self.update_scene_number("01") # layout = Layout(LayoutDirection.VERTICAL, { # 定义布局 # "title": (1.5, LayoutAtom()), # "content": (7.0, Layout(LayoutDirection.HORIZONTAL, { # "problem": (4.0, LayoutAtom()), # "visual": (5.0, LayoutAtom()) # })) # }).resolve(self) # title = Text("Triangle Angle Problem", ...) # layout["title"].place(title) # problem = Text("In triangle ABC, the angles are in the ratio 2:3:4...", ...) # layout["content"]["problem"].place(problem) # triangle = Triangle(fill_opacity=0.2, color=self.triangle_color) # # ... (添加 A, B, C 标签) # triangle_group = VGroup(triangle, labels) # layout["content"]["visual"].place(triangle_group) # # ... (动画播放) # self.title = title # 保存 Mobject 供后续使用 # self.problem = problem # # ...
- 清晰地使用
Layout
将屏幕分为标题区和内容区(内容区又分为问题文本区和视觉展示区)。 - 创建
Text
(问题) 和Triangle
(示意图)。 - 使用
VGroup
组合三角形和其顶点标签。 - 将创建的 Mobject 保存到
self
的属性中,以便在后续的play_scene_XX
方法中可以引用或移除它们。
- 清晰地使用
建立方程 (
play_scene_02
)# def play_scene_02(self): # self.update_scene_number("02") # # ... (布局) # layout["title"].place(self.title) # 复用之前的标题对象 # eq_text = Text("Let's set up an equation...", ...) # step1 = MathTex(r"\text{If we call the angles } \angle A, \angle B, \text{ and } \angle C", ...) # # ... (step2 到 step7) # steps = VGroup(step1, step2, step3, step4, step5, step6, step7).arrange(DOWN * 0.5, buff=0.4) # layout["equations"].place(steps) # 假设布局中有 "equations" 区域 # # ... (动画) # previous_objects = VGroup(self.problem, self.triangle, self.labels) # self.play(FadeOut(previous_objects), run_time=1) # 淡出上一场景的元素 # self.play(Write(eq_text), run_time=1) # self.play(Write(step1), run_time=1) # 逐步展示方程 # # ...
- 复用了
self.title
。 - 使用
FadeOut
清理了上一个场景片段中不再需要的元素 (self.problem
,self.triangle
,self.labels
)。 - 用
MathTex
展示方程的推导步骤,并通过VGroup
和arrange
进行排版。 - 通过一系列
Write
动画逐步呈现解题思路。
- 复用了
绘制精确角度的三角形 (
play_scene_04
) 这个片段比较复杂,因为它尝试根据计算出的角度 (40°, 60°, 80°) 来精确绘制三角形。# def play_scene_04(self): # # ... (计算顶点坐标 vA, vB, vC 使用三角函数和边长) # # base_length = 3 # # angle_A_rad = 40 * DEGREES # # side_c = base_length # # side_a = side_c * np.sin(angle_A_rad) / np.sin(angle_C_rad) # # vB = np.array([0, 0, 0]) # # vA = np.array([side_c, 0, 0]) # # x_comp = side_b * np.cos(angle_C_rad) # 原代码这里似乎应该是 angle_A_rad 或 B # # y_comp = side_b * np.sin(angle_C_rad) # 同上 # # vC = np.array([x_comp, y_comp, 0]) # # vertices = [vA, vB, vC] # # final_triangle = Polygon(*vertices, ...) # # 使用 Manim 的 Angle 类绘制角度弧 # # actual_vertices = final_triangle.get_vertices() # # A, B, C = actual_vertices # # angle_A_arc = Angle.from_three_points(C, A, B, radius=radius, ...) # # angle_B_arc = Angle.from_three_points(A, B, C, radius=radius, ...) # # angle_C_arc = Angle.from_three_points(B, C, A, radius=radius, ...)
- 精确绘图的挑战:Manim 本身是基于坐标的。要绘制一个特定角度的三角形,你需要计算出顶点的坐标。这通常涉及到三角函数(正弦定理、余弦定理)。
Polygon(*vertices, ...)
: 通过顶点列表创建多边形。Angle.from_three_points(P1, P2, P3, ...)
: P2是角的顶点,从P2-P1到P2-P3形成角。这个方法比手动创建Arc
更方便。- 标签定位:角度标签的位置计算使用了角平分线的思路 (
bisector_A = normalize(vec_AB + vec_AC)
),这是一个很好的细节处理。
7.2 案例二:抛物线方程参数变化 (源自 25_Parabola_equation_function_graph_changes.txt
)
这个脚本通过改变二次函数 y = ax^2 + bx + c
中的系数 a
, b
, c
,动态展示抛物线形状和位置的变化。
核心知识点回顾:
Axes
创建坐标系。axes.plot()
绘制函数图像。ValueTracker
和always_redraw
实现动态更新。Transform
动画用于平滑改变图像和标签。MathTex
显示公式和参数值。- 自定义
Layout
。
代码片段分析:
参数
a
的影响 (play_section_01
)# def play_section_01(self): # # ... (布局, 标题, 坐标轴) # label = MathTex("a=1", color=BLUE).next_to(axes.c2p(2, 4), UR, buff=0.1) # graph = axes.plot(lambda x: x ** 2, color=BLUE, x_range=plot_x_range) # self.play(Create(graph), Write(label), run_time=1.5) # graph_a2 = axes.plot(lambda x: 2 * x ** 2, x_range=plot_x_range) # label_a2 = MathTex("a=2", color=GREEN).move_to(label) # 移动到原标签位置 # self.play(Transform(graph, graph_a2), Transform(label, label_a2), run_time=1.5) # # ... (a=0.5, a=-1 的情况类似)
- 为
a
的不同取值分别创建axes.plot()
对象和对应的MathTex
标签。 - 使用
Transform
动画将前一个图形/标签平滑过渡到新的图形/标签。注意,Transform
会消耗第一个参数 Mobject,并将其替换为第二个参数 Mobject。
- 为
参数
b
的影响 (play_section_02
) - 动态更新# def play_section_02(self): # # ... (布局, 标题, 坐标轴) # b_tracker = ValueTracker(0) # 初始 b=0 # a_val = 1 # c_val = 0 # # label 和 parabola 会自动重绘 # label = always_redraw( # lambda: layout["body"]["graph_group"]["label"].place( # 假设 place 返回 Mobject # MathTex(f"b = {b_tracker.get_value():.1f}", color=PURPLE) # ) # ) # parabola = always_redraw( # lambda: axes.plot( # lambda x: a_val * x ** 2 + b_tracker.get_value() * x + c_val, # color=PURPLE, # x_range=plot_x_range # ) # ) # self.add(parabola, label) # 添加到场景,开始自动更新 # self.play(b_tracker.animate.set_value(3), run_time=3.0) # b 从 0 变到 3 # self.wait(1.5) # self.play(b_tracker.animate.set_value(-3), run_time=3.0) # b 从 3 变到 -3 # # ...
b_tracker = ValueTracker(0)
: 创建一个追踪参数b
值的对象。always_redraw(...)
:label
和parabola
都是动态 Mobject。它们的lambda
函数会在每一帧被调用:label
的lambda
函数根据b_tracker.get_value()
创建新的MathTex
。parabola
的lambda
函数根据b_tracker.get_value()
调用axes.plot()
创建新的抛物线图像。
self.add(parabola, label)
: 将这些动态 Mobject 添加到场景后,它们就会开始根据b_tracker
的值自动更新。self.play(b_tracker.animate.set_value(3), ...)
: 动画地改变b_tracker
的值。由于label
和parabola
依赖于b_tracker
,它们也会平滑地变化,形成动画效果。
7.3 案例三:光的折射 (源自 22_refraction_of_light.txt
)
这个脚本用图示和公式解释了光的折射现象及斯涅尔定律。它展示了如何构建更复杂的几何图形和标签,并使用自定义颜色方案。
核心知识点回顾:
- 自定义颜色常量。
Rectangle
,Line
,Arrow
,Arc
,DashedLine
构建图示。MathTex
显示公式和希腊字母。- 三角函数 (
np.sin
,np.cos
,np.radians
,np.arcsin
) 用于计算角度和位置。 rotate_vector
(Manim 内置或自定义) 用于计算光线方向。AnimationGroup
和lag_ratio
控制动画序列。Indicate
动画强调公式部分。
代码片段分析:
图示构建
# def play_scene_01(self): # # ... (布局, 背景, 标题) # interface = Line(...) # 介质分界面 # air_rect = Rectangle(...) # 空气区域 # water_rect = Rectangle(...) # 水区域 # media_bg = VGroup(air_rect, water_rect).set_z_index(-5) # # 光线计算 (涉及斯涅尔定律) # incident_angle_deg = 45 # incident_angle_rad = np.radians(incident_angle_deg) # n1 = 1.0; n2 = 1.33 # sin_theta2 = (n1 / n2) * np.sin(incident_angle_rad) # refracted_angle_rad = np.arcsin(sin_theta2) # incidence_point = np.array([diagram_center_x, interface_y, 0]) # 入射点 # # 使用 rotate_vector (假设是 Manim 的方法或自定义的) # # incident_vector = rotate_vector(UP * 2.5, PI - incident_angle_rad) # # ray_start = incidence_point + incident_vector # # refracted_vector = rotate_vector(DOWN * 2.5, refracted_angle_rad) # # ray_end = incidence_point + refracted_vector # # 更常见的是直接用角度计算终点: # ray_start = incidence_point + 2.5 * np.array([-np.sin(incident_angle_rad), np.cos(incident_angle_rad), 0]) # ray_end = incidence_point + 2.5 * np.array([np.sin(refracted_angle_rad), -np.cos(refracted_angle_rad), 0]) # incident_ray = Arrow(ray_start, incidence_point, ...) # refracted_ray = Arrow(incidence_point, ray_end, ...) # normal = DashedLine(...) # 法线 # angle_inc_arc = Arc(radius=0.8, start_angle=normal.get_angle() - PI / 2, angle=angle_inc_rad, ...) # theta1_label = MathTex(r"\theta_1", ...).move_to(temp_arc1.point_from_proportion(0.5)) # # ... (其他标签和元素)
- 大量使用基本形状来构建视觉场景。
np.radians()
将角度转为弧度,np.sin()
,np.cos()
,np.arcsin()
进行三角计算,这在物理和几何可视化中非常常见。- 光线的起点和终点是根据角度和长度计算出来的。
Arc
用于表示角度,其start_angle
和angle
参数需要仔细设置。arc.point_from_proportion(0.5)
获取弧中点,用于放置角度标签。
斯涅尔定律公式
# snell_law_formula = MathTex( # r"n_1 \sin \theta_1", r"=", r"n_2 \sin \theta_2", # 分割字符串以便分别上色 # font_size=48, color=MY_FORMULA_BLUE # ) # snell_law_formula.set_color_by_tex("n_1", MY_DARK_TEXT) # snell_law_formula.set_color_by_tex(r"\theta_1", MY_HIGHLIGHT_RED) # # ... (其他部分上色) # self.play(Write(snell_law_formula)) # self.play(Indicate(snell_law_formula.get_part_by_tex("n_1"), ...)) # 强调公式的 n1 部分
MathTex
的一个有用特性是,如果将 LaTeX 字符串分割成多个子字符串传递给它,之后可以用mobject.set_color_by_tex("子字符串", color)
或mobject.get_part_by_tex("子字符串")
来单独操作公式的特定部分。这对于教学中分步解释公式或高亮特定项非常有用。
7.4 案例四:二叉树 (源自 30_binary_tree.txt
)
这个脚本展示了如何用 Manim 可视化二叉树数据结构,包括其基本结构、术语、不同类型的树以及遍历。
核心知识点回顾:
- 自定义 Python 类 (
BinaryTreeNode
) 辅助创建和管理树的逻辑。 - 递归或迭代思想构建树的可视化对象。
Circle
和Text
组合成节点。Line
连接节点形成边。VGroup
组织整个树。- 复杂的布局和定位计算。
代码片段分析:
BinaryTreeNode
辅助类# class BinaryTreeNode: # def __init__(self, value, position=ORIGIN, level=0): # self.value = value # self.position = position # 节点在屏幕上的位置 # self.left = None # self.right = None # self.level = level # def create_circle(self, radius=0.4, color="#1E88E5", text_color="#FFFFFF", ...): # circle = Circle(radius=radius, color=color, fill_opacity=0.8, fill_color=color) # text = Text(str(self.value), color=text_color, ...) # text.move_to(circle.get_center()) # return VGroup(circle, text).move_to(self.position)
- 这个辅助类并不直接是 Manim Mobject,但它存储了树节点的数据(值、子节点)以及其在 Manim 场景中的位置 (
self.position
) 和层级 (self.level
)。 create_circle()
方法负责生成该节点对应的 Manim Mobject (一个包含Circle
和Text
的VGroup
)。
- 这个辅助类并不直接是 Manim Mobject,但它存储了树节点的数据(值、子节点)以及其在 Manim 场景中的位置 (
创建树的可视化 (
create_tree_visualization
方法)# def create_tree_visualization(self, root_value=1, depth=3, ...): # root = BinaryTreeNode(root_value) # 根节点,位置默认 ORIGIN # nodes_queue = [(root, 0, 0)] # (node_obj, horizontal_screen_pos_offset, level) # all_nodes_data = [] # 存储 BinaryTreeNode 对象 # edges_coords = [] # 存储边的 (起点坐标, 终点坐标) # while nodes_queue: # node_obj, h_pos_offset, level = nodes_queue.pop(0) # node_obj.position = np.array([h_pos_offset, -self.level_height * level, 0]) # 计算实际屏幕位置 # if level >= depth: continue # # 计算子节点的位置 (相对父节点进行偏移) # # h_spacing_factor = self.horizontal_spacing / (2 ** (level + 1)) # 水平间距随层级减小 # # left_child_h_offset = h_pos_offset - h_spacing_factor # # right_child_h_offset = h_pos_offset + h_spacing_factor # # ... (创建左子节点 BinaryTreeNode 对象) # # node_obj.left = BinaryTreeNode(...) # # nodes_queue.append((node_obj.left, left_child_h_offset, level + 1)) # # edges_coords.append((node_obj.position, node_obj.left.position)) # 错误:此时子节点 position 未定 # # all_nodes_data.append(node_obj.left) # # ... (右子节点类似) # # 修正:应在子节点 position 确定后再记录 edge # # 应该在循环外,或在子节点 position 计算后,重新遍历收集 edges # # 或者,在创建子节点时,其 position 已经基于父节点和偏移计算好了,可以直接用 # # 创建 Manim Mobjects # node_mobjects = VGroup() # node_mobjects.add(root.create_circle(...)) # 创建根节点的 Mobject # for node_data in all_nodes_data: # all_nodes_data 应包含除 root 外的其他 BinaryTreeNode 对象 # node_mobjects.add(node_data.create_circle(...)) # edge_mobjects = VGroup() # # 再次遍历或使用已存储的正确坐标创建 Line 对象 # # for start_pos, end_pos in edges_coords: # # edge_mobjects.add(Line(start_pos, end_pos, ...)) # tree_viz = VGroup(edge_mobjects, node_mobjects) # tree_viz.move_to(shift_vector) # 整体移动 # return tree_viz, root_data_obj, all_nodes_data_objs
- 逻辑与可视化的分离:首先使用
BinaryTreeNode
构建树的逻辑结构并计算每个节点在屏幕上的目标位置。这个计算过程通常是递归的或基于层序遍历的。 - 位置计算:节点的 y 坐标通常取决于其层级 (
-self.level_height * level
)。x 坐标则更复杂,需要确保子节点在父节点下方且互不重叠,通常子节点间的水平间距会随着层级的增加而减小。 - Mobject 生成:在所有节点的位置都计算完毕后,遍历
BinaryTreeNode
对象列表,为每个对象调用其create_circle()
方法生成对应的 Manim Mobject。同时,根据父子关系和它们的位置创建Line
Mobject作为边。 - 整体组合:最后将所有的节点 Mobject 和边 Mobject 添加到一个大的
VGroup
中,形成完整的树的可视化。
- 逻辑与可视化的分离:首先使用
这个案例展示了当内置 Mobject 不足以直接表达复杂结构时,如何通过自定义辅助类和精心的位置计算来构建自定义的可视化。
本章小结
通过分析这些案例,我们可以看到:
- 规划是关键:在编写代码之前,先构思动画的步骤和最终效果。
- 分解问题:将复杂的动画分解为小的、可管理的片段或方法。
- 善用工具:
VGroup
,Layout
(自定义),ValueTracker
等工具能极大提高效率。 - 数学计算:对于几何和物理相关的可视化,精确的数学计算(三角函数、向量运算等)是必不可少的。
- 代码复用:将通用的功能(如场景编号更新、布局)封装成函数或类。
- 细节处理:如标签的精确定位、颜色的搭配、动画的节奏感,共同决定了最终动画的质量。
这仅仅是 Manim 能力的冰山一角。希望通过这些案例的分析,您对如何使用 Manim 构建自己的数学动画有了更具体的认识。
第八章:画龙点睛——语音与字幕
虽然 Manim 本身专注于视觉动画的生成,但在许多科普视频中,同步的语音解说和字幕是不可或缺的。您提供的脚本中使用了 custom_voiceover_tts
这个自定义工具来集成语音和(推测)字幕。
8.1 custom_voiceover_tts
的工作模式推测
从脚本中的使用方式来看,custom_voiceover_tts
可能是一个上下文管理器 (context manager),它做了以下几件事情:
- 接收文本:
with custom_voiceover_tts(voice_text_string) as tracker:
- 文本转语音 (TTS):在后台调用某个 TTS 引擎(如 gTTS, Azure TTS, Edge TTS 等)将
voice_text_string
转换为音频文件。 - 获取音频信息:
tracker
对象可能包含转换后的音频文件路径 (tracker.audio_path
) 和音频时长 (tracker.duration
)。 - (可选)生成字幕:可能还会根据文本和时长信息生成
.srt
或类似格式的字幕文件。 - 同步:允许在
with
代码块内部的 Manim 动画与语音时长进行同步。
# 伪代码,演示其核心用法
# from manim_utils import custom_voiceover_tts # 假设的导入
# voice_text = "这是一段解说词。"
# with custom_voiceover_tts(voice_text) as tracker:
# # tracker.audio_path 存放生成的音频文件路径
# # tracker.duration 存放音频时长(秒)
# if tracker.audio_path and tracker.duration > 0:
# self.add_sound(tracker.audio_path) # Manim 的方法,在视频中添加音轨
# # --- Manim 动画代码 ---
# animation_start_time = self.renderer.time # 获取当前渲染时间 (Manim CE)
# # 或者直接计算动画时长
# self.play(Write(some_text), run_time=2)
# self.play(Create(some_shape), run_time=1.5)
# # --- 结束 Manim 动画代码 ---
# # 计算动画总时长
# # anim_duration = self.renderer.time - animation_start_time
# anim_duration = 2 + 1.5
# # 计算需要额外等待的时间以匹配语音
# wait_time = 0
# if tracker.duration > 0: # 确保语音已生成
# wait_time = max(0.001, tracker.duration - anim_duration)
# else: # 如果语音失败,可以设置一个默认等待时间
# wait_time = 0.5
# self.wait(wait_time)
8.2 Manim 的 self.add_sound()
Manim 场景类本身提供了一个 self.add_sound(audio_file_path, time_offset=0, gain=None)
方法。
audio_file_path
: 要添加的音频文件的路径。time_offset
: 音频相对于场景开始播放的偏移时间(秒)。默认是0,即从当前self.play
或self.wait
开始时播放。在with
块中使用时,通常意味着音频与该块内的动画同步开始。gain
: 音量增益。
当 Manim 渲染视频时,它会使用 FFmpeg 将这些指定的音轨混入最终的视频文件中。
8.3 字幕的集成
Manim 本身不直接生成或渲染字幕到视频帧上(除非你手动创建 Text
Mobject 作为字幕)。custom_voiceover_tts
工具如果生成了 .srt
文件,那么这个字幕文件可以:
- 在视频编辑软件中导入,与最终视频合并。
- 上传到视频平台(如 YouTube, Bilibili)时,作为独立的字幕文件上传。
8.4 如果没有 custom_voiceover_tts
怎么办?
如果你的项目中没有这个特定的工具,你仍然可以实现类似的功能:
- 手动录制或生成语音:使用任何你喜欢的 TTS 工具(如 Microsoft Edge 浏览器的朗读功能、在线 TTS 服务、Audacity 等录音软件)为你的解说词生成音频文件 (如
.mp3
,.wav
)。 - 获取音频时长:使用音频编辑软件或工具(如
ffprobe
, Python 的mutagen
库)获取每个音频文件的准确时长。 - 在 Manim 中同步:
# 假设你已经为 scene_part1_voice.mp3 测得时长为 5.2 秒 voice1_duration = 5.2 self.add_sound("path/to/scene_part1_voice.mp3") anim1_duration = 0 self.play(Write(text1), run_time=2); anim1_duration += 2 self.play(Create(shape1), run_time=1.5); anim1_duration += 1.5 # ... 其他动画 ... wait_time = max(0, voice1_duration - anim1_duration) self.wait(wait_time)
- 手动创建字幕文件:根据你的解说词和对应的时间轴,手动编写
.srt
文件。一个.srt
条目的格式如下:1 00:00:05,000 --> 00:00:08,500 这是第一句字幕,从第5秒显示到第8.5秒。 2 00:00:09,200 --> 00:00:12,000 这是第二句字幕。
8.5 语音和动画节奏的重要性
无论是使用自动化工具还是手动操作,确保语音解说和屏幕上的视觉动画节奏合拍至关重要。
- 动画不宜过快,以免观众跟不上解说。
- 动画也不宜过慢,以免语音结束后长时间等待。
- 重要的视觉变化应该与解说中提到它的时间点大致吻合。
这通常需要反复调整动画的 run_time
和 self.wait()
的时长。
本章小结
- 通过
self.add_sound()
方法,Manim 可以在最终渲染的视频中加入音轨。 - 自定义工具如
custom_voiceover_tts
可以自动化文本到语音的转换、音频时长的获取以及与 Manim 动画的同步过程。 - 即使没有自动化工具,也可以通过手动生成语音、测量时长并在 Manim 中精心安排
self.wait()
来实现同步。 - 字幕文件(如
.srt
)通常是独立于 Manim 生成的,之后与视频合并或在视频平台上传。 - 保持语音和动画的节奏协调对于提升观看体验非常重要。
第九章:融会贯通——更复杂的案例剖析
在前面的章节中,我们已经分别探讨了 Manim 的核心组件和常用技巧。现在,让我们来看一些更复杂的脚本,观察这些知识点是如何综合运用以构建出信息丰富且视觉吸引的动画的。
9.1 案例一:欧拉公式的几何意义 (源自 21_Geometric_Meaning_of_Euler_s_Formula.txt
)
这个脚本旨在解释欧拉公式 (e^{i\theta} = \cos\theta + i\sin\theta) 的几何意义,涉及到复平面、单位圆、三角函数以及动态可视化。
关键技术点:
- 场景分段与清理:使用
play_section_XX
方法组织内容,并通过clear_section
管理元素,允许在不同部分之间保留核心元素(如坐标轴、单位圆)。# self.play_section_02() # 引入复平面 # self.clear_section(keep_refs=['axes', 'axes_labels']) # 清理,但保留坐标轴 # self.play_section_03() # 单位圆上的点 cos + i sin # self.clear_section(keep_refs=['axes', 'axes_labels', 'unit_circle', ...]) # 保留更多
keep_refs
参数允许指定哪些self
的属性(即 Mobject)不被clear_section
移除。 - 复平面与单位圆:
- 使用
Axes
创建复平面,并手动添加 "Re" 和 "Im" 标签。 - 根据
Axes
的单位尺寸计算并创建Circle
作为单位圆。 - 使用
ValueTracker
(theta_tracker) 和add_updater
(或always_redraw
) 使单位圆上的点point_P
、半径radius_line
、角度弧angle_arc
、角度值文本theta_value_text
以及点标签e_label
动态地随着角度theta_tracker
的变化而更新。
# theta_tracker = ValueTracker(0) # self.point_P = Dot(color=BLUE, radius=0.08) # self.point_P.add_updater( # lambda m: m.move_to(self.axes.c2p(np.cos(theta_tracker.get_value()), np.sin(theta_tracker.get_value()))) # ) # # ... 其他 updaters ... # self.play(theta_tracker.animate.set_value(TAU), run_time=animation_duration, rate_func=linear)
- 使用
TracedPath
:trace = TracedPath(self.point_P.get_center, ...)
用于追踪point_P
的运动轨迹,在动态展示点P绕单位圆运动时,会画出圆的轨迹。- 公式的逐步呈现与变换:
formula = MathTex(r"e^{i\theta}", r"=", r"\cos\theta", r"+", r"i\sin\theta")
将公式拆分为多个部分,便于分步Write
或分别设置颜色。ReplacementTransform(self.p_label_complex, self.e_label)
将标签从\cos\theta + i\sin\theta
平滑地变成e^{i\theta}
,强调两者的等价性。
这个案例的亮点:
- 动态关联:通过
ValueTracker
和updater
将数学参数(角度 θ)与视觉元素(点的位置、线的方向、弧的大小)紧密联系起来,实现了真正意义上的动态数学可视化。 - 概念递进:从复平面基础,到单位圆上的三角表示,再到欧拉公式的引入和动态演示,逻辑清晰,层层深入。
- 元素管理:在多阶段场景中,通过
clear_section
和keep_refs
有效地管理了元素的生命周期,避免了屏幕混乱,同时保留了必要的上下文。
9.2 案例二:天空为什么是蓝色的 (源自 24_why_sky_is_blue.txt
)
这个脚本解释了瑞利散射导致天空呈现蓝色的物理现象。它涉及太阳光、大气层、光的散射等多个概念,并使用了丰富的视觉元素。
关键技术点:
- 自定义颜色方案:脚本开头定义了大量自定义颜色(
MY_SUN_YELLOW
,MY_SKY_BLUE
,MY_ATMOSPHERE_BLUE
等),用于统一视觉风格。 - 多层视觉元素:
- 太阳 (
Circle
)、棱镜 (Triangle
)、光谱 (VGroup
ofLine
s) 来解释白光分解。 - 地球 (
Circle
+Polygon
s for continents)、大气层 (Annulus
)、大量分子 (VGroup
ofDot
s) 来模拟大气环境。 - 入射光线 (
Arrow
) 与分子交互后产生散射光线 (多个Arrow
s)。
- 太阳 (
- 随机性与模拟:
- 大气中分子的位置 (
np.random.uniform
) 和散射光线的方向 (np.random.uniform
) 引入了随机性,使模拟更自然。
# molecules = VGroup() # for _ in range(50): # 创建50个分子 # angle = np.random.uniform(0, TAU) # radius = np.random.uniform(2.6, 2.9) # 在大气层环内随机半径 # pos = atmosphere.get_center() + radius * (np.cos(angle)*RIGHT + np.sin(angle)*UP) # mol = Dot(pos, radius=0.03, color=MY_MOLECULE_COLOR) # molecules.add(mol)
- 大气中分子的位置 (
- 概念图示分离:
- “瑞利散射”部分 (
play_section_04
):用一个中心分子和不同颜色的散射光线(蓝/紫光线多且散射角度大,红/橙光线少且主要向前)来形象地解释散射与波长的关系。 - “为什么我们看到蓝色”部分 (
play_section_05
):用地面观察者和天空中大量蓝色散射点源来到达观察者眼睛的示意图。
- “瑞利散射”部分 (
- 解释“为什么不是紫色”:通过并列展示简化的太阳光谱能量分布图和人眼敏感度曲线图(用
Axes
和plot
构建)来解释这一常见疑问。
这个案例的亮点:
- 复杂现象的简化表达:将复杂的物理过程(光的组成、大气成分、散射原理、人眼感知)分解为一系列可理解的视觉步骤。
- 视觉元素的巧妙运用:例如用
Annulus
表示大气层,用大量随机Dot
表示分子,用不同颜色和数量的箭头表示不同波长光的散射情况。 - 多角度解释:不仅解释了为什么是蓝色,还进一步解释了为什么不是散射更强的紫色,使解释更完整。
- 信息密度与节奏控制:在有限的时间内呈现了大量信息,并通过分段和语音同步(假设
custom_voiceover_tts
正常工作)来控制节奏。
9.3 案例三:转动动能与角动量 (源自 31_rotational_kinetic_energy_and_angular_momentum.txt
)
这是一个物理问题求解的脚本,计算飞轮的转动动能。它侧重于公式的展示、单位换算和代入计算。
关键技术点:
- 中文字体与
TexTemplate
:如第六章所述,正确配置了cjk_template
以支持MathTex
中的中文文本和公式。 - 自定义
Layout
:使用了Layout
类将屏幕划分为标题区和左右内容区,用于清晰展示公式、已知条件和计算步骤。 MathTex
的精细使用:- 公式、单位、数值计算都使用
MathTex
,排版美观。 TransformMatchingTex
用于在公式推导的不同步骤之间平滑过渡,例如从角速度单位换算公式到代入数值,再到计算结果。
注意这里# omega_conversion_formula = MathTex(r"\omega (\text{rad/s}) = \omega (\text{rpm}) \times \frac{2\pi ...}{...}", ...) # omega_conversion_calc = MathTex(r"= 1200 \times \frac{2\pi}{60} ...", ...) # omega_converted_value = MathTex(r"= 40\pi \, \text{rad/s}", ..., color=self.highlight_color) # self.play(Write(omega_conversion_formula)) # self.play(TransformMatchingTex(omega_conversion_formula.copy(), omega_conversion_calc)) # self.play(TransformMatchingTex(omega_conversion_calc.copy(), omega_converted_value))
.copy()
的使用,因为TransformMatchingTex
会消耗第一个参数,如果后续还需要基于它进行变换或它本身就是动态更新的一部分,就需要复制。- 公式、单位、数值计算都使用
- 逐步展示 (Staged Animation):计算过程不是一次性全部显示,而是通过多个
self.play
调用,结合Write
,FadeIn
,TransformMatchingTex
逐步呈现,符合教学的逻辑。 - 高亮:使用
self.highlight_color
(如PURE_RED
) 突出显示重要的中间结果或最终答案。
这个案例的亮点:
- 教学流程的模拟:动画的组织方式非常贴近一个老师在黑板上演算物理题的过程:给出公式 -> 列出已知 -> 单位换算 -> 代入计算 -> 得出结果 -> 近似计算。
TransformMatchingTex
的有效应用:使得公式的演变过程非常自然和连贯,帮助观众理解每一步的联系。- 排版清晰:通过
Layout
和VGroup.arrange
,即使公式和步骤较多,屏幕依然保持整洁有序。
通用启示:
- 模块化与复用:
update_scene_number
,Layout
,custom_voiceover_tts
这些自定义工具在多个脚本中出现,体现了良好编程实践中的模块化和代码复用。 - 注释与可读性:虽然这里分析的是最终代码,但在开发复杂场景时,良好的注释和有意义的变量命名至关重要。
- 迭代与调试:创建复杂的 Manim 动画往往是一个迭代的过程。使用低质量快速渲染 (
-ql
) 进行调试,逐步完善细节。
通过这些案例,我们可以看到,掌握了 Manim 的基本构建块之后,创造复杂而精美的数学或科学动画就变成了如何巧妙地组合这些构建块,并用清晰的逻辑来组织动画流程的问题。
第十章:进阶之路与学习资源
恭喜你!通过前面的章节和案例分析,你已经对如何使用 Manim 编写代码来创作数学动画有了坚实的基础。Manim 是一个非常强大的工具,它的潜力远不止于此。本章将为你指明一些进阶方向和有用的学习资源。
10.1 探索更多 Manim 功能
- 3D 动画 (
ThreeDScene
): Manim 不仅限于2D,它也支持3D场景、3D图形 (Sphere
,Cube
,ParametricSurface
等) 和3D相机操作。class My3DScene(ThreeDScene): def construct(self): self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) axes = ThreeDAxes() sphere = Sphere() self.add(axes, sphere) self.begin_ambient_camera_rotation(rate=0.5) # 自动旋转相机 self.wait(5)
- SVG 图像 (
SVGMobject
): 你可以导入 SVG 文件并在 Manim 中对其进行动画处理。这对于使用矢量图形软件(如 Inkscape, Illustrator)设计的复杂图形非常有用。 - 自定义 Mobject: 当内置图形不满足需求时,可以继承
Mobject
或VMobject
(Vectorized Mobject) 来创建自己的图形类,完全控制其形状和行为。30_binary_tree.txt
中的BinaryTreeNode
虽然不是直接的 Mobject,但其create_circle
方法生成了 Mobject,这种封装思想可以扩展到创建自定义 Mobject。 - 更高级的动画:
LaggedStart
: 类似AnimationGroup
但提供更细致的动画启动延迟控制。Succession
: 按顺序播放一系列动画。- 自定义动画:通过继承
Animation
类并重写interpolate_mobject
方法,你可以创建全新的动画效果。
- 交互性 (ManimGL): 如果你使用的是 ManimGL (Grant Sanderson 最初使用的 OpenGL 版本,与社区版 ManimCE 不同),它支持实时渲染和一些基本的交互功能。但目前社区版 ManimCE 更侧重于高质量的视频输出。
- 着色器 (Shaders): 对于非常高级的视觉效果,可以利用着色器编程(GLSL),但这需要图形学背景。
10.2 提升代码质量与效率
- 面向对象编程 (OOP): 更深入地运用类和对象来组织你的 Mobject 和动画逻辑,特别是对于大型项目。
- 代码复用: 将常用的 Mobject 组合、动画序列或辅助函数封装到单独的 Python 模块中(就像
manim_utils.py
那样)。 - 配置文件: Manim 支持通过配置文件 (
manim.cfg
) 设置默认参数,如输出目录、背景颜色、帧率等,避免在每个脚本中重复设置。 - 版本控制 (Git): 对于任何编程项目,使用 Git 进行版本控制都是一个好习惯,便于追踪修改、协作和回滚。
10.3 学习资源
- Manim 官方文档:
- Manim Community Edition (ManimCE): https://docs.manim.community/ 这是目前最活跃和推荐的版本,拥有详尽的文档和示例。
- 示例库:
- 官方文档中包含大量示例。
- GitHub 上搜索 "Manim examples" 可以找到许多用户分享的项目。
- 社区:
- Manim Community Discord 服务器: https://www.manim.community/discord/ 非常活跃的社区,可以提问、分享作品、获取帮助。
- Reddit r/manim: https://www.reddit.com/r/manim/
- 教程视频:
- YouTube 上有许多 Manim 教程,搜索 "Manim tutorial" 即可。一些知名的 Manim 内容创作者(除了 3Blue1Brown)也会分享他们的制作经验。
- Bilibili 上也有不少中文 Manim 教程和作品分享。
- 阅读优秀代码:
- 研究 3Blue1Brown 的旧视频代码 (manim-old 或 manim_sandbox GitHub 仓库,主要是 ManimGL)。
- 查看 Manim Community 维护者和其他贡献者的代码。
- 您提供的这些脚本本身就是很好的学习材料,尝试修改它们,实现新的效果。
10.4 实践,实践,再实践!
学习编程和使用像 Manim 这样的库,最重要的方法就是动手实践。
- 从小处着手: 尝试修改现有示例,改变颜色、文本、动画时长。
- 设定目标: 选择一个你感兴趣的数学或科学概念,尝试用 Manim 将其可视化。
- 分解问题: 将复杂的动画目标分解为小的、可实现的任务。
- 不怕犯错: 调试是学习过程的一部分。仔细阅读错误信息,利用社区资源寻求帮助。
- 分享你的作品: 将你的动画分享出去,获取反馈,也能激励自己继续学习。
结语
Manim 是一个充满创造力的工具,它将编程的严谨与艺术的表达完美结合。通过本书的学习,你已经迈出了用代码创造数学之美的第一步。前方的道路充满挑战,但也同样充满乐趣和成就感。
希望您提供的这些脚本和本书的讲解能为您打下坚实的基础。不断探索,不断创造,用 Manim 将抽象的概念变得生动直观!
祝您在 Manim 的世界里玩得开心!