Manim 的构建块

本文档解释了 Manim 的构建块,并将为您提供开始制作自己的视频所需的所有工具。

Manim 本质上为您提供了三个不同的概念,您可以将它们协同编排以制作数学动画:**数学对象**(简称 **mobject**)、**动画**和**场景**。正如我们将在以下部分中看到的那样,这三个概念中的每一个都在 Manim 中作为单独的类实现:`Mobject``Animation``Scene` 类。

注意

建议您在阅读本页之前阅读教程快速入门Manim 的输出设置

Mobject 对象

Mobject 对象是所有 Manim 动画的基本构建块。每个从 `Mobject` 派生的类都代表一个可以在屏幕上显示的对象。例如,`Circle`(圆形)、`Arrow`(箭头)和 `Rectangle`(矩形)等简单形状都是 mobject 对象。更复杂的结构,例如`Axes`(坐标轴)、`FunctionGraph`(函数图)或 `BarChart`(条形图)也都是 mobject 对象。

如果您尝试在屏幕上显示一个 `Mobject` 实例,您将只会看到一个空帧。原因是 `Mobject` 类是所有其他 mobject 对象的抽象基类,即它没有任何预先确定的视觉形状可以显示在屏幕上。它只是一个*可能*被显示的对象的骨架。因此,您很少需要使用 `Mobject` 的普通实例;相反,您很可能会创建其派生类的实例。其中一个派生类是 `VMobject`。这里的 `V` 代表矢量化 Mobject。本质上,vmobject 是一种使用矢量图形进行显示的 mobject 对象。大多数情况下,您将处理 vmobject 对象,但我们将继续使用术语“mobject”来指代可以在屏幕上显示的形状类别,因为它更通用。

注意

任何可以在屏幕上显示的对象都是一个 `mobject`,即使它本质上不一定是*数学的*。

提示

要查看从 `Mobject` 派生的类的示例,请参阅 `geometry` 模块。其中大多数实际上也派生自 `VMobject`

创建和显示 mobject 对象

快速入门中所述,Manim 脚本中的所有代码通常都放在 `Scene` 类的 `construct()` 方法中。要在屏幕上显示 mobject 对象,请调用包含该对象的 `Scene``add()` 方法。这是在没有动画的情况下在屏幕上显示 mobject 对象的主要方式。要从屏幕上移除 mobject 对象,只需调用包含该对象的 `Scene``remove()` 方法。

示例:创建 Mobject 对象

from manim import *

class CreatingMobjects(Scene):
    def construct(self):
        circle = Circle()
        self.add(circle)
        self.wait(1)
        self.remove(circle)
        self.wait(1)
class CreatingMobjects(Scene):
    def construct(self):
        circle = Circle()
        self.add(circle)
        self.wait(1)
        self.remove(circle)
        self.wait(1)

放置 mobject 对象

让我们定义一个新的 `Scene`,名为 `Shapes`,并向其中`add()`一些 mobject 对象。此脚本生成一张静态图片,其中显示一个圆形、一个正方形和一个三角形。

示例:形状

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)
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)

默认情况下,mobject 对象在首次创建时放置在坐标中心,即*原点*。它们还会被赋予一些默认颜色。此外,`Shapes` 场景通过使用 `shift()` 方法放置 mobject 对象。正方形从原点向上移动一个单位,而圆形和三角形则分别向左和向右移动一个单位。

注意

与其他图形软件不同,Manim 将坐标中心放置在屏幕中央。垂直正方向向上,水平正方向向右。另请参阅 `constants` 模块中定义的常量 `ORIGIN`(原点)、`UP`(上)、`DOWN`(下)、`LEFT`(左)、`RIGHT`(右)及其他。

还有许多其他方法可以在屏幕上放置 mobject 对象,例如 `move_to()``next_to()``align_to()`。下一个场景 `MobjectPlacement` 将使用这三种方法。

示例:Mobject 放置

from manim import *

class MobjectPlacement(Scene):
    def construct(self):
        circle = Circle()
        square = Square()
        triangle = Triangle()

        # place the circle two units left from the origin
        circle.move_to(LEFT * 2)
        # place the square to the left of the circle
        square.next_to(circle, LEFT)
        # align the left border of the triangle to the left border of the circle
        triangle.align_to(circle, LEFT)

        self.add(circle, square, triangle)
        self.wait(1)
class MobjectPlacement(Scene):
    def construct(self):
        circle = Circle()
        square = Square()
        triangle = Triangle()

        # place the circle two units left from the origin
        circle.move_to(LEFT * 2)
        # place the square to the left of the circle
        square.next_to(circle, LEFT)
        # align the left border of the triangle to the left border of the circle
        triangle.align_to(circle, LEFT)

        self.add(circle, square, triangle)
        self.wait(1)

`move_to()` 方法使用绝对单位(相对于 `ORIGIN` 测量),而 `next_to()` 使用相对单位(从作为第一个参数传递的 mobject 对象测量)。`align_to()` 使用 `LEFT`(左边)不是作为测量单位,而是作为确定对齐边界的方式。mobject 对象边界的坐标是使用其周围的虚拟边界框确定的。

提示

Manim 中的许多方法可以链式调用。例如,这两行代码

square = Square()
square.shift(LEFT)

可以替换为

square = Square().shift(LEFT)

从技术上讲,这是可能的,因为大多数方法调用都会返回修改后的 mobject 对象。

设置 mobject 对象样式

以下场景更改了 mobject 对象的默认美学样式。

示例:Mobject 样式设置

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)
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`(不透明度)参数才能显示颜色。不透明度 `1.0` 表示完全不透明,而 `0.0` 表示完全透明。

只有 `VMobject` 的实例实现了 `set_stroke()` 和 `set_fill()``Mobject` 的实例则实现了 `set_color()`。绝大多数预定义类都派生自 `VMobject`,因此通常可以安全地假设您可以使用 `set_stroke()` 和 `set_fill()`

Mobject 对象屏幕显示顺序

下一个场景与上一节的 `MobjectStyling` 场景完全相同,除了其中一行代码。

示例:Mobject Z 轴顺序

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)
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()` 方法的参数顺序决定了 mobject 对象在屏幕上的显示顺序,最左边的参数被放在最底层。

动画

Manim 的核心是动画。通常,您可以通过调用 `play()` 方法将动画添加到场景中。

示例:一些动画

from manim import *

class SomeAnimations(Scene):
    def construct(self):
        square = Square()

        # some animations display mobjects, ...
        self.play(FadeIn(square))

        # ... some move or rotate mobjects around...
        self.play(Rotate(square, PI/4))

        # some animations remove mobjects from the screen
        self.play(FadeOut(square))

        self.wait(1)
class SomeAnimations(Scene):
    def construct(self):
        square = Square()

        # some animations display mobjects, ...
        self.play(FadeIn(square))

        # ... some move or rotate mobjects around...
        self.play(Rotate(square, PI/4))

        # some animations remove mobjects from the screen
        self.play(FadeOut(square))

        self.wait(1)

简单来说,动画是在两个 mobject 对象之间进行插值的过程。例如,`FadeIn(square)` 从完全透明的 `square` 版本开始,以完全不透明的版本结束,通过逐渐增加不透明度在两者之间进行插值。`FadeOut` 的工作方式则相反:它从完全不透明插值到完全透明。再例如,`Rotate` 以作为参数传入的 mobject 对象开始,并以旋转一定量后的同一对象结束,这次插值的是 mobject 对象的角度而不是其不透明度。

动画化方法

mobject 对象的任何可更改属性都可以动画化。实际上,任何改变 mobject 对象属性的方法都可以通过使用 `animate()` 来作为动画使用。

示例:Animate 示例

from manim import *

class AnimateExample(Scene):
    def construct(self):
        square = Square().set_fill(RED, opacity=1.0)
        self.add(square)

        # animate the change of color
        self.play(square.animate.set_fill(WHITE))
        self.wait(1)

        # animate the change of position and the rotation at the same time
        self.play(square.animate.shift(UP).rotate(PI / 3))
        self.wait(1)
class AnimateExample(Scene):
    def construct(self):
        square = Square().set_fill(RED, opacity=1.0)
        self.add(square)

        # animate the change of color
        self.play(square.animate.set_fill(WHITE))
        self.wait(1)

        # animate the change of position and the rotation at the same time
        self.play(square.animate.shift(UP).rotate(PI / 3))
        self.wait(1)

参考:`Animation`

`animate()` 是所有 mobject 对象的一个属性,它将后续方法动画化。例如,`square.set_fill(WHITE)` 设置正方形的填充颜色,而 `square.animate.set_fill(WHITE)` 则将此操作动画化。

动画运行时长

默认情况下,传递给 `play()` 的任何动画都恰好持续一秒。使用 `run_time` 参数来控制时长。

示例:运行时长

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)
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 开始并在整个动画过程中变化。因此,您只需根据其 `interpolate_mobject` 方法中的 `alpha` 值在 Animation 内部操作 `self.mobject`。然后,您将获得 `Animation` 的所有优点,例如以不同的运行时长播放或使用不同的速率函数。

假设您从一个数字开始,并希望创建一个 `Transform` 动画将其转换为目标数字。您可以使用 `FadeTransform` 来实现,它会淡出起始数字并淡入目标数字。但当我们考虑将一个数字转换为另一个数字时,一种直观的方法是平滑地递增或递减它。Manim 有一个功能,允许您通过定义自己的自定义动画来自定义此行为。

您可以从创建自己的 `Count` 类开始,该类继承自 `Animation`。该类可以有一个包含三个参数的构造函数:一个 `DecimalNumber` Mobject 对象,以及 `start`(起始值)和 `end`(结束值)。构造函数会将 `DecimalNumber` Mobject 对象传递给父构造函数(本例中为 `Animation` 构造函数),并设置 `start` 和 `end`。

您唯一需要做的就是定义动画每一步的外观。Manim 在 `interpolate_mobject()` 方法中为您提供了 `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` 对象,使用任何速率函数来播放它。

示例:计数场景

from manim import *

class Count(Animation):
    def __init__(self, number: DecimalNumber, start: float, end: float, **kwargs) -> None:
        # Pass number as the mobject of the animation
        super().__init__(number,  **kwargs)
        # Set start and end
        self.start = start
        self.end = end

    def interpolate_mobject(self, alpha: float) -> None:
        # Set value of DecimalNumber according to alpha
        value = self.start + (alpha * (self.end - self.start))
        self.mobject.set_value(value)


class CountingScene(Scene):
    def construct(self):
        # Create Decimal Number and add it to scene
        number = DecimalNumber().set_color(WHITE).scale(5)
        # Add an updater to keep the DecimalNumber centered as its value changes
        number.add_updater(lambda number: number.move_to(ORIGIN))

        self.add(number)

        self.wait()

        # Play the Count Animation to count from 0 to 100 in 4 seconds
        self.play(Count(number, 0, 100), run_time=4, rate_func=linear)

        self.wait()
class Count(Animation):
    def __init__(self, number: DecimalNumber, start: float, end: float, **kwargs) -> None:
        # Pass number as the mobject of the animation
        super().__init__(number,  **kwargs)
        # Set start and end
        self.start = start
        self.end = end

    def interpolate_mobject(self, alpha: float) -> None:
        # Set value of DecimalNumber according to alpha
        value = self.start + (alpha * (self.end - self.start))
        self.mobject.set_value(value)


class CountingScene(Scene):
    def construct(self):
        # Create Decimal Number and add it to scene
        number = DecimalNumber().set_color(WHITE).scale(5)
        # Add an updater to keep the DecimalNumber centered as its value changes
        number.add_updater(lambda number: number.move_to(ORIGIN))

        self.add(number)

        self.wait()

        # Play the Count Animation to count from 0 to 100 in 4 seconds
        self.play(Count(number, 0, 100), run_time=4, rate_func=linear)

        self.wait()

参考:`Animation` `DecimalNumber` `interpolate_mobject()` `play()`

使用 mobject 对象的坐标

Mobject 对象包含定义其边界的点。这些点可用于将其他 mobject 对象彼此相对添加,例如通过 `get_center()``get_top()``get_start()` 等方法。以下是一些重要坐标的示例

示例:Mobject 示例

../_images/MobjectExample-1.png
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)
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)

将 mobject 对象转换为其他 mobject 对象

也可以像这样将一个 mobject 对象转换为另一个 mobject 对象

示例:变换示例

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))
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 对象的点按顺时针排列而另一个按逆时针排列时。这时,使用 `flip` 函数并通过 numpy 的 `roll` 函数重新定位点可能会有所帮助。

示例:旋转示例

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)
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)

场景

`Scene` 类是 Manim 的连接组织。每个 mobject 对象都必须被 `添加` 到场景中才能显示,或者从场景中 `移除` 才能停止显示。每个动画都必须由场景 `播放`,并且没有动画发生的时间间隔由调用 `wait()` 决定。您的视频所有代码都必须包含在派生自 `Scene` 的类的 `construct()` 方法中。最后,如果需要同时渲染多个场景,单个文件可以包含多个 `Scene` 子类。