文章目录
本文档解释了 Manim 的构建模块,并将为你提供开始制作自己视频所需的所有工具。
基本上,Manim 提供了三个不同的概念,你可以将它们组合在一起以制作数学动画:数学对象(简称 mobject),动画(Animation),以及场景(Scene)。如我们将在后续部分中看到的,每个概念在 Manim 中都作为一个单独的类来实现:Mobject 类、Animation 类和 Scene 类。
注意 : 建议在阅读本页之前先阅读教程《快速入门》和《Manim 的输出设置》。
Mobjects
Mobjects 是所有 Manim 动画的基本构建模块。每个从 Mobject 派生的类都表示一个可以显示在屏幕上的对象。例如,简单的形状如 Circle(圆)、Arrow(箭头)和 Rectangle(矩形)都是 mobjects。更复杂的构造如 Axes(坐标轴)、FunctionGraph(函数图)或 BarChart(柱状图)也是 mobjects
。
如果你尝试在屏幕上显示一个 Mobject 的实例,你只会看到一个空框架。原因是 Mobject
类是所有其他 mobjects
的抽象基类,也就是说它没有任何预先确定的视觉形状可以显示在屏幕上。它只是一个可以显示事物的骨架。因此,你很少需要使用纯粹的 Mobject
实例;相反,你更可能会创建它的派生类的实例。其中一个派生类是 VMobject
。V代表向量化 Mobject。本质上,VMobject 是使用矢量图形显示的 mobject
。大多数情况下,你将处理 VMobjects
,尽管我们将继续使用 “mobject” 这一术语来指代可以显示在屏幕上的形状类,因为它更为通用。
注意 : 任何可以显示在屏幕上的对象都是 mobject,即使它不一定具有数学性质。
提示 :要查看从 Mobject 派生的类的示例,请参见几何模块。事实上,其中大多数也派生自 VMobject。
创建和显示 mobjects
正如 Quickstart 中所解释的,manim 脚本中的所有代码通常都放在场景类的 construct() 方法中。要在屏幕上显示一个 mobject,请调用包含场景的 add()
方法。这是在屏幕上显示未做动画的 mobject 的主要方法。要从屏幕上移除移动对象,只需调用包含场景的 remove()
方法。
from manim import *
class CreatingMobjects(Scene):
def construct(self):
circle = Circle()
self.add(circle)
self.wait(1)
self.remove(circle)
self.wait(1)
放置移动对象mobjects
让我们定义一个名为 Shapes
的新场景,并向其中add()
一些 mobjects。该脚本会生成一张静态图片,显示一个圆形、一个正方形和一个三角形:
from manim import *
class Shapes(Scene):
def construct(self):
circle = Circle()
square = Square()
triangle = Triangle()
circle.shift(LEFT)
square.shift(UP)
triangle.shift(RIGHT)
self.add(circle, square, triangle)
self.wait(1)
默认情况下,mobjects 在首次创建时会放置在坐标中心或原点。它们还被赋予了一些默认颜色。此外,"形状 "场景会使用 shift()
方法来放置 mobjects。正方形从原点向上移动一个单位,而圆形和三角形则分别向左和向右移动一个单位。
注意 : 与其他图形软件不同,manim 将坐标原点放置在屏幕的中心。正方向垂直向上,正方向水平向右。另见常量模块中的 ORIGIN、UP、DOWN、LEFT、RIGHT 等常量。
在屏幕上放置 mobjects(数学对象)还有许多其他可能的方法,例如 move_to()
、next_to()
和 align_to()
。下一个场景 MobjectPlacement
使用了这三种方法。
from manim import *
class MobjectPlacement(Scene):
def construct(self):
circle = Circle() # 创建一个圆形
square = Square() # 创建一个正方形
triangle = Triangle()# 创建一个三角形
# 将圆形移动到距离原点左边两单位的位置
circle.move_to(LEFT * 2)
# 将正方形放置在圆形的左边
square.next_to(circle, LEFT)
# 将三角形的左边界与圆形的左边界对齐
triangle.align_to(circle, LEFT)
self.add(circle, square, triangle) # 将圆形、正方形和三角形添加到场景中
self.wait(1) # 等待 1 秒钟
-
move_to()
:将一个 mobject 移动到指定的位置。在这里,circle.move_to(LEFT * 2)
将圆形移动到距离原点左边两单位的位置。 -
next_to()
:将一个 mobject 放置在另一个 mobject 的旁边。square.next_to(circle, LEFT)
将正方形放在圆形的左边。 -
align_to()
:将一个 mobject 的某个边界对齐到另一个 mobject 的相同边界。triangle.align_to(circle, LEFT)
将三角形的左边界与圆形的左边界对齐。
方法链式调用的技巧:
- 方法链:在 Manim 中,许多方法可以链式调用。例如,以下两行代码:
可以简化为一行:square = Square() square.shift(LEFT)
这是因为大多数方法在调用后会返回被修改的 mobject 本身。因此,你可以继续对返回的 mobject 调用其他方法,实现方法的链式调用。square = Square().shift(LEFT)
这种链式调用不仅可以使代码更加简洁,还可以在一步中完成多个操作,使代码更易读和维护。
设计 mobjects
下面的场景更改了移动对象的默认美观效果。
from manim import *
class MobjectStyling(Scene):
def construct(self):
circle = Circle().shift(LEFT)
square = Square().shift(UP)
triangle = Triangle().shift(RIGHT)
circle.set_stroke(color=GREEN, width=20)
square.set_fill(YELLOW, opacity=1.0)
triangle.set_fill(PINK, opacity=0.5)
self.add(circle, square, triangle)
self.wait(1)
这个场景使用了两个主要的函数来改变 mobject 的视觉样式:set_stroke()
和 set_fill()
。前者用于更改 mobject 边界的视觉样式,而后者则更改其内部的样式。默认情况下,大多数 mobject 的内部是完全透明的,因此必须指定 opacity
(不透明度)参数才能显示颜色。opacity
值为 1.0 表示完全不透明,而 0.0 则表示完全透明。
只有 VMobject
的实例实现了 set_stroke()
和 set_fill()
方法。而 Mobject
的实例则实现了 set_color()
方法。绝大多数预定义的类都是从 VMobject
派生的,所以通常可以安全地假设你可以使用 set_stroke()
和 set_fill()
方法。
Mobject 屏幕显示顺序
接下来的场景与上一节的 MobjectStyling
场景完全相同,除了有一行代码不同。
示例:MobjectZOrder
from manim import *
class MobjectZOrder(Scene):
def construct(self):
circle = Circle().shift(LEFT)
square = Square().shift(UP)
triangle = Triangle().shift(RIGHT)
circle.set_stroke(color=GREEN, width=20)
square.set_fill(YELLOW, opacity=1.0)
triangle.set_fill(PINK, opacity=0.5)
self.add(triangle, square, circle)
self.wait(1)
这里唯一的区别(除了场景名称之外)是 mobject 添加到场景中的顺序。在 MobjectStyling
中,我们将它们以 add(circle, square, triangle)
的顺序添加,而在 MobjectZOrder
中,我们以 add(triangle, square, circle)
的顺序添加。
正如你所看到的,add()
方法的参数顺序决定了 mobjects 在屏幕上的显示顺序,左边的参数被放置在后面。
动画
动画是 Manim 的核心。通常,你可以通过调用 play()
方法将动画添加到场景中。
示例:SomeAnimations
from manim import *
class SomeAnimations(Scene):
def construct(self):
square = Square()
# 一些动画显示 mobjects...
self.play(FadeIn(square))
# ...一些动画移动或旋转 mobjects...
self.play(Rotate(square, PI/4))
# 一些动画将 mobjects 从屏幕上移除
self.play(FadeOut(square))
self.wait(1)
简单来说,动画是指在两个 mobjects 之间插值的过程。例如,FadeIn(square)
从一个完全透明的方块开始,并以一个完全不透明的方块结束,通过逐渐增加透明度来插值它们之间的状态。FadeOut
则相反:它从完全不透明插值到完全透明。另一个例子是 Rotate
,它以传递给它的 mobject 为起点,以旋转了一定角度后的同一对象为终点,这次插值的是 mobject 的角度而不是透明度。
动画方法
在 Manim 中,任何可以改变 mobject 属性的操作都可以被动画化。实际上,任何改变 mobject 属性的方法都可以通过使用 animate()
转化为动画。
示例:AnimateExample
from manim import *
class AnimateExample(Scene):
def construct(self):
square = Square().set_fill(RED, opacity=1.0)
self.add(square)
# 动画化颜色的变化
self.play(square.animate.set_fill(WHITE))
self.wait(1)
# 动画化位置的变化和旋转,同时进行
self.play(square.animate.shift(UP).rotate(PI / 3))
self.wait(1)
参考:Animation
animate()
是所有 mobjects 的一个属性,它能够将后续的方法动画化。例如,square.set_fill(WHITE)
设置方块的填充颜色为白色,而 square.animate.set_fill(WHITE)
则会将这个操作动画化。
动画运行时间
默认情况下,传递给 play()
的任何动画都会持续一秒钟。可以使用 run_time
参数来控制动画的持续时间。
示例:RunTime
from manim import *
class RunTime(Scene):
def construct(self):
square = Square()
self.add(square)
self.play(square.animate.shift(UP), run_time=3)
self.wait(1)
创建自定义动画
虽然 Manim 提供了许多内置动画,但有时你可能需要从一个 Mobject 的状态平滑地过渡到另一个状态。在这种情况下,可以定义自己的自定义动画。可以通过扩展 Animation
类并重写其 interpolate_mobject()
方法来实现这一点。
interpolate_mobject()
方法接收一个参数 alpha
,它在动画过程中从 0 逐渐变化到 1。你只需在 interpolate_mobject
方法中根据 alpha
的值来操纵 self.mobject
。通过这种方式,你可以享受 Animation
提供的所有好处,例如在不同的运行时间内播放动画或使用不同的速率函数。
假设你有一个数字,想要创建一个从一个数字平滑过渡到另一个目标数字的动画。可以使用 FadeTransform
来实现这一点,它会淡出起始数字并淡入目标数字。但更直观的方式是通过平滑地递增或递减数字来实现这一点。Manim 允许你通过定义自己的自定义动画来实现这种行为。
你可以从创建一个继承自 Animation
的 Count
类开始。这个类的构造函数可以包含三个参数:DecimalNumber
Mobject、起始值和结束值。构造函数会将 DecimalNumber
Mobject 传递给父类构造函数(即 Animation
构造函数),并设置起始值和结束值。
你需要做的唯一事情是定义动画每一步该如何显示。在 interpolate_mobject()
方法中,Manim 会根据视频的帧率、速率函数和动画播放时间为你提供 alpha
值,alpha
参数的值在 0 到 1 之间,表示当前动画的步骤。例如,0 表示动画的开始,0.5 表示动画的中途,而 1 表示动画的结束。
对于 Count
动画,你只需确定在给定的 alpha
值时显示的数字,并在 Count
动画的 interpolate_mobject()
方法中设置该值。假设你从 50 开始递增,到动画结束时 DecimalNumber
达到 100。
- 如果
alpha
为 0,值应为 50。 - 如果
alpha
为 0.5,值应为 75。 - 如果
alpha
为 1,值应为 100。
通常,你从起始数字开始,根据 alpha
值增加一定部分的增量值。因此,每一步计算显示数字的逻辑将是 50 + alpha * (100 - 50)
。一旦你为 DecimalNumber
设置了计算的值,就完成了。
定义完 Count
动画后,你可以在 Scene
中为任何 DecimalNumber
播放此动画,持续时间和速率函数可以任意设置。
示例:CountingScene
from manim import *
class Count(Animation):
def __init__(self, number: DecimalNumber, start: float, end: float, **kwargs) -> None:
# 传递 number 作为动画的 mobject
super().__init__(number, **kwargs)
# 设置起始值和结束值
self.start = start
self.end = end
def interpolate_mobject(self, alpha: float) -> None:
# 根据 alpha 设置 DecimalNumber 的值
value = self.start + (alpha * (self.end - self.start))
self.mobject.set_value(value)
class CountingScene(Scene):
def construct(self):
# 创建 DecimalNumber 并将其添加到场景中
number = DecimalNumber().set_color(WHITE).scale(5)
# 添加一个更新器,在数值变化时使 DecimalNumber 保持居中
number.add_updater(lambda number: number.move_to(ORIGIN))
self.add(number)
self.wait()
# 播放 Count 动画,在 4 秒钟内从 0 计数到 100
self.play(Count(number, 0, 100), run_time=4, rate_func=linear)
self.wait()
参考:Animation
、DecimalNumber
、interpolate_mobject()
、play()
使用 mobject 的坐标
mobjects 包含定义其边界的点。这些点可以用来将其他 mobjects 以相应的方式添加到它们上面,例如通过 get_center()
、get_top()
和 get_start()
等方法。以下是一些重要坐标的示例:
示例:MobjectExample
from manim import *
class MobjectExample(Scene):
def construct(self):
p1 = [-1, -1, 0]
p2 = [1, -1, 0]
p3 = [1, 1, 0]
p4 = [-1, 1, 0]
a = Line(p1, p2).append_points(Line(p2, p3).points).append_points(Line(p3, p4).points)
point_start = a.get_start()
point_end = a.get_end()
point_center = a.get_center()
self.add(Text(f"a.get_start() = {np.round(point_start, 2).tolist()}", font_size=24).to_edge(UR).set_color(YELLOW))
self.add(Text(f"a.get_end() = {np.round(point_end, 2).tolist()}", font_size=24).next_to(self.mobjects[-1], DOWN).set_color(RED))
self.add(Text(f"a.get_center() = {np.round(point_center, 2).tolist()}", font_size=24).next_to(self.mobjects[-1], DOWN).set_color(BLUE))
self.add(Dot(a.get_start()).set_color(YELLOW).scale(2))
self.add(Dot(a.get_end()).set_color(RED).scale(2))
self.add(Dot(a.get_top()).set_color(GREEN_A).scale(2))
self.add(Dot(a.get_bottom()).set_color(GREEN_D).scale(2))
self.add(Dot(a.get_center()).set_color(BLUE).scale(2))
self.add(Dot(a.point_from_proportion(0.5)).set_color(ORANGE).scale(2))
self.add(*[Dot(x) for x in a.points])
self.add(a)
在这个示例中,我们创建了一个由四个点组成的线段 a
。我们使用 get_start()
、get_end()
和 get_center()
方法来获取线段的起点、终点和中心点,并将这些点显示为彩色的圆点。我们还用 point_from_proportion()
方法在 a
上标记了中点。
将 mobjects 转换为其他 mobjects
你还可以将一个 mobject 转换为另一个 mobject,如下所示:
示例:ExampleTransform
from manim import *
class ExampleTransform(Scene):
def construct(self):
self.camera.background_color = WHITE
m1 = Square().set_color(RED)
m2 = Rectangle().set_color(RED).rotate(0.2)
self.play(Transform(m1, m2))
Transform
函数将前一个 mobject 的点映射到下一个 mobject 的点。这可能会导致奇怪的行为,例如当一个 mobject 的点按顺时针排列而另一个 mobject 的点按逆时针排列时。在这种情况下,可以使用 flip
函数和 numpy
的 roll
函数来重新定位点:
示例:ExampleRotation
from manim import *
class ExampleRotation(Scene):
def construct(self):
self.camera.background_color = WHITE
m1a = Square().set_color(RED).shift(LEFT)
m1b = Circle().set_color(RED).shift(LEFT)
m2a = Square().set_color(BLUE).shift(RIGHT)
m2b = Circle().set_color(BLUE).shift(RIGHT)
points = m2a.points
points = np.roll(points, int(len(points) / 4), axis=0)
m2a.points = points
self.play(Transform(m1a, m1b), Transform(m2a, m2b), run_time=1)
在这个示例中,我们首先创建了两个不同的形状 m1a
和 m1b
以及 m2a
和 m2b
。为了使 m2a
的点与 m2b
的点对齐,我们使用 numpy.roll()
来重新定位 m2a
的点。这确保了 Transform
动画在形状转换时表现得更加自然。
场景(Scenes)
Scene
类是 Manim 的核心部分,起到了连接和管理各个 mobject 和动画的作用。在 Manim 中,所有的 mobject 必须添加到一个场景中才能被显示,或者从场景中移除以停止显示。每个动画也必须由场景来播放,而每一个没有动画发生的时间段则由调用 wait()
方法来决定。你的视频的所有代码都必须包含在一个继承自 Scene
类的类的 construct()
方法中。最后,一个文件中可以包含多个 Scene
子类,以便同时渲染多个场景。