快速上手 Manim#

传统上,制作技术概念的动画相当繁琐,因为要使动画足够精确以准确传达这些概念可能很困难。Manim 利用 Python 的简洁性,通过编程生成动画,使得精确指定每个动画的运行方式变得方便。可以查看示例图库,获取一些灵感,了解如何使用 Manim 创建精美的图像和视频。

备注

在继续之前,请按照 安装 部分的步骤安装 Manim,并确保其运行正常。有关在 JupyterLab 或 Jupyter Notebook 中使用 Manim 的信息,请参阅 IPython 魔法命令 %%manim 的文档

本快速入门指南将引导你使用 Manim 创建示例项目:用于精确程序化动画的动画引擎。

首先,你将使用命令行界面创建 Scene(场景),这是 Manim 生成视频的类。在场景中,你将制作一个圆的动画。然后,你将添加另一个场景,展示一个正方形变形为圆的过程。这将是你初步了解 Manim 的动画能力。之后,你将学习如何定位多个数学对象(Mobjects)。最后,你将掌握 .animate 语法,这是强大的功能,可以动画化你用于修改 Mobjects 的方法。

新建项目#

首先,创建新文件夹。在本指南中,将其命名为 project

from pathlib import Path

temp_dir = Path("./.temp")
(temp_dir/"project").mkdir(exist_ok=True)
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[1], line 4
      1 from pathlib import Path
      3 temp_dir = Path("./.temp")
----> 4 (temp_dir/"project").mkdir(exist_ok=True)

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/pathlib.py:1311, in Path.mkdir(self, mode, parents, exist_ok)
   1307 """
   1308 Create a new directory at this given path.
   1309 """
   1310 try:
-> 1311     os.mkdir(self, mode)
   1312 except FileNotFoundError:
   1313     if not parents or self.parent == self:

FileNotFoundError: [Errno 2] No such file or directory: '.temp/project'

这个文件夹是你项目的 ./.temp 目录。它包含 Manim 运行所需的所有文件,以及项目生成的任何输出。

制作圆的动画#

编写如下代码:

%%file {temp_dir}/project/scene.py
from manim import * # 这是使用 Manim 的推荐方式,因为脚本通常会使用 Manim 命名空间中的多个名称。
# 导入了并使用了 ``Scene``、``Circle``、``PINK``和``Create``等类。

class CreateCircle(Scene):
    def construct(self):
        circle = Circle()  # create a circle
        circle.set_fill(PINK, opacity=0.5)  # set the color and transparency
        self.play(Create(circle))  # show the circle on screen
Writing .temp/project/scene.py
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[2], line 1
----> 1 get_ipython().run_cell_magic('file', '{temp_dir}/project/scene.py', 'from manim import * # 这是使用 Manim 的推荐方式,因为脚本通常会使用 Manim 命名空间中的多个名称。\n# 导入了并使用了 ``Scene``、``Circle``、``PINK``和``Create``等类。\n\nclass CreateCircle(Scene):\n    def construct(self):\n        circle = Circle()  # create a circle\n        circle.set_fill(PINK, opacity=0.5)  # set the color and transparency\n        self.play(Create(circle))  # show the circle on screen\n')

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/interactiveshell.py:2541, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
   2539 with self.builtin_trap:
   2540     args = (magic_arg_s, cell)
-> 2541     result = fn(*args, **kwargs)
   2543 # The code below prevents the output from being displayed
   2544 # when using magics with decorator @output_can_be_silenced
   2545 # when the last Python token in the expression is a ';'.
   2546 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/magic.py:757, in MagicAlias.__call__(self, *args, **kwargs)
    755         args_list[0] = self.magic_params + " " + args[0]
    756         args = tuple(args_list)
--> 757     return fn(*args, **kwargs)
    758 finally:
    759     self._in_call = False

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/magics/osm.py:854, in OSMagics.writefile(self, line, cell)
    851     print("Writing %s" % filename)
    853 mode = 'a' if args.append else 'w'
--> 854 with io.open(filename, mode, encoding='utf-8') as f:
    855     f.write(cell)

FileNotFoundError: [Errno 2] No such file or directory: '.temp/project/scene.py'

打开命令行,导航到你的项目文件夹,然后执行以下命令:

manim -pql scene.py CreateCircle

Manim 将输出渲染信息,然后创建 MP4 文件。你的默认视频播放器将播放该 MP4 文件,显示以下动画。

在 Sphinx 文档中嵌入#

在 Sphinx 项目的 conf.py 文件中,添加如下插件:

extensions = [
    "manim.utils.docbuild.manim_directive",
    "manim.utils.docbuild.autocolor_directive",
    "manim.utils.docbuild.autoaliasattr_directive",
]

可以使用 .. manim:: 指令来嵌入 Manim 场景。以下是基本示例:

.. manim:: MyScene

    class MyScene(Scene):
        def construct(self):
            circle = Circle(color=BLUE, fill_opacity=0.5)
            self.play(Create(circle))
            self.wait()

manim_directive 支持多种选项,用于控制视频的渲染和显示方式。以下是一些常用的选项:

  • hide_source: 如果设置为 True,则不会在文档中显示源代码。

  • no_autoplay: 如果设置为 True,则视频不会自动播放。

  • quality: 控制视频的渲染质量,可选值为 'low', 'medium', 'high', 'fourk'

  • save_as_gif: 如果设置为 True,则渲染为 GIF 文件。

  • save_last_frame: 如果设置为 True,则只渲染最后一帧的图像。

示例:

.. manim:: MyScene
    :hide_source:
    :quality: high
    :save_as_gif:

    class MyScene(Scene):
        def construct(self):
            circle = Circle(color=BLUE, fill_opacity=0.5)
            self.play(Create(circle))
            self.wait()

小技巧

  • 确保你的 Manim 场景类名在文档中是唯一的,以避免冲突。

  • 如果你在 Sphinx 文档中嵌入多个场景,确保每个场景的类名不同,以避免渲染错误。

在 Jupyter Notebook 中使用#

如果你在 Jupyter Notebook 中使用 Manim,可以使用魔法命令来渲染场景。

行模式:

%manim [CLI options] MyAwesomeScene

单元格模式:

%%manim [CLI options] MyAwesomeScene

class MyAweseomeScene(Scene):
    def construct(self):
        ...

备注

在笔记本中显示的渲染视频的最大宽度可以通过 media_width 配置选项进行配置。默认设置为 25vw,即当前视口宽度的 25%。为了让输出尽可能大,可以将 config.media_width 设置为 "100%"

media_embed 选项会将图像/视频输出嵌入到笔记本中。这通常是不希望的,因为它会使笔记本变得非常大,但在某些平台上是必需的(特别是 Google 的 CoLab,除非通过 config.embed = False 抑制,否则它会自动启用),并且在笔记本(或转换后的 HTML 文件)相对于视频位置移动的情况下也是需要的。使用场景包括使用 Sphinx 和 JupyterBook 构建文档。另请参阅 Sphinx 的 manim 指令。

首先,确保在一个单元格中输入 import manimfrom manim import * 并执行它。

典型的用于 Manim 的 Jupyter 笔记本单元格可能如下所示:

%%manim -v WARNING --disable_caching -qm BannerExample

config.media_width = "75%"
config.media_embed = True

class BannerExample(Scene):
    def construct(self):
        self.camera.background_color = "#ece6e2"
        banner_large = ManimBanner(dark_theme=False).scale(0.7)
        self.play(banner_large.create())
        self.play(banner_large.expand())

执行此单元格将渲染并显示在单元格主体中定义的 BannerExample 场景。

小技巧

如果你想隐藏包含输出进度条的红色框,可以将 progress_bar 配置选项设置为 None。这也可以通过传递 --progress_bar None 作为命令行标志来实现。

将正方形转换为圆#

在完成圆的动画之后,继续进行一些稍微复杂的内容。

from manim import *
%%manim -v WARNING --disable_caching -ql SquareToCircle

config.media_width = "25%"
config.media_embed = True

class SquareToCircle(Scene):
    def construct(self):
        circle = Circle()  # 创建圆
        circle.set_fill(PINK, opacity=0.5)  # 设置颜色和透明度

        square = Square()  # 创建正方形
        square.rotate(PI / 4)  # 旋转一定角度

        self.play(Create(square))  # 动画显示正方形的创建
        self.play(Transform(square, circle))  # 将正方形插值转换为圆
        self.play(FadeOut(square))  # 淡出动画
Manim Community v0.18.1

Animation 0: Create(Square):   0%|          | 0/15 [00:00<?, ?it/s]
Animation 1: Transform(Square):   0%|          | 0/15 [00:00<?, ?it/s]
Animation 2: FadeOut(Square):   0%|          | 0/15 [00:00<?, ?it/s]

这个例子展示了Manim的主要功能之一:只需几行代码即可实现复杂且数学密集型的动画(例如在两个几何形状之间进行干净插值)。

定位 Mobject#

接下来,将介绍一些定位 Mobject 的基本技巧。

%%manim -v WARNING --disable_caching -ql SquareAndCircle

config.media_embed = True
config.media_width = "25%"

class SquareAndCircle(Scene):
    def construct(self):
        circle = Circle()  # 创建一个圆
        circle.set_fill(PINK, opacity=0.5)  # 设置颜色和透明度

        square = Square()  # 创建一个正方形
        square.set_fill(BLUE, opacity=0.5)  # 设置颜色和透明度

        square.next_to(circle, RIGHT, buff=0.5)  # 设置位置
        self.play(Create(circle), Create(square))  # 在屏幕上显示形状
Manim Community v0.18.1

Animation 0: Create(Circle), etc.:   0%|          | 0/15 [00:00<?, ?it/s]

next_to 是用于定位 MobjectMobject 方法。

我们首先通过将 circle 作为方法的第一个参数,将粉色圆指定为正方形的参考点。 第二个参数用于指定 Mobject 相对于参考点的放置方向。 在本例中,我们将方向设置为 RIGHT,告诉 Manim 将正方形放置在圆的右侧。 最后,buff=0.5 在两个对象之间应用了一个小的距离缓冲。

尝试将 RIGHT 更改为 LEFTUPDOWN,看看这如何改变正方形的位置。

使用定位方法,您可以渲染包含多个 Mobject 的场景, 通过坐标设置它们在场景中的位置,或将它们相对于彼此定位。

使用 .animate 语法来动画化方法#

使用 .animate 用于动画化你对 Mobject 所做的更改。当你在任何修改 Mobject 的方法调用前加上 .animate 时,该方法就会变成可以被 self.play 播放的动画。回到 SquareToCircle,看看在创建 Mobject 时使用方法与使用 .animate 动画化这些方法调用之间的区别。

%%manim -v WARNING --disable_caching -ql AnimatedSquareToCircle

config.media_width = "25%"
config.media_embed = True

class AnimatedSquareToCircle(Scene):
    def construct(self):
        circle = Circle()  # create a circle
        square = Square()  # create a square

        self.play(Create(square))  # show the square on screen
        self.play(square.animate.rotate(PI / 4))  # rotate the square
        self.play(Transform(square, circle))  # transform the square into a circle
        self.play(
            square.animate.set_fill(PINK, opacity=0.5)
        )  # color the circle on screen
Manim Community v0.18.1

Animation 0: Create(Square):   0%|          | 0/15 [00:00<?, ?it/s]
Animation 1: _MethodAnimation(Square):   0%|          | 0/15 [00:00<?, ?it/s]
Animation 2: Transform(Square):   0%|          | 0/15 [00:00<?, ?it/s]
Animation 3: _MethodAnimation(Square):   0%|          | 0/15 [00:00<?, ?it/s]

第一个 self.play 创建了正方形。第二个动画化地将其旋转45度。第三个将正方形变形为圆形,最后一个将圆形着色为粉色。尽管最终结果与 SquareToCircle 相同,但 .animate 展示了 rotateset_fill 方法动态地应用于 Mobject,而不是在创建时就已经应用了这些变化。

尝试其他方法,比如 flipshift,看看会发生什么。

%%manim -v WARNING --disable_caching -ql DifferentRotations

config.media_width = "25%"
config.media_embed = True

class DifferentRotations(Scene):
    def construct(self):
        left_square = Square(color=BLUE, fill_opacity=0.7).shift(2 * LEFT)
        right_square = Square(color=GREEN, fill_opacity=0.7).shift(2 * RIGHT)
        self.play(
            left_square.animate.rotate(PI), Rotate(right_square, angle=PI), run_time=2
        )
        self.wait()
Manim Community v0.18.1

Animation 0: _MethodAnimation(Square), etc.:   0%|          | 0/30 [00:00<?, ?it/s]
Animation 0: _MethodAnimation(Square), etc.:  57%|█████▋    | 17/30 [00:00<00:00, 168.14it/s]

这个 Scene 展示了 .animate 的一些特性。当使用 .animate 时,Manim 实际上会获取一个 Mobject 的初始状态和最终状态,并在两者之间进行插值。在 AnimatedSquareToCircle 类中,你可以观察到这一点,当正方形旋转时:正方形的角在移动到变形所需的第一个正方形到第二个正方形的位置时,似乎会稍微收缩。

DifferentRotations 中,.animate 对旋转的解释与 Rotate 方法之间的差异更加明显。一个旋转180度的 Mobject 的初始状态和最终状态是相同的,因此 .animate 试图对两个相同的对象进行插值,结果就是左边的正方形。如果你发现你自己的 .animate 使用导致了类似的不良行为,可以考虑使用传统的动画方法,比如右边的正方形,它使用了 Rotate

TransformReplacementTransform#

TransformReplacementTransform 之间的区别在于,Transform(mob1, mob2) 会将 mob1 的点(以及颜色等其他属性)转换为 mob2 的点/属性。

ReplacementTransform(mob1, mob2) 则是直接将场景中的 mob1 替换为 mob2

使用 ReplacementTransform 还是 Transform 主要取决于个人偏好。它们都可以用来实现相同的效果,如下所示。

%%manim -v WARNING --disable_caching -ql TwoTransforms

config.media_width = "25%"
config.media_embed = True

class TwoTransforms(Scene):
    def transform(self):
        a = Circle()
        b = Square()
        c = Triangle()
        self.play(Transform(a, b))
        self.play(Transform(a, c))
        self.play(FadeOut(a))

    def replacement_transform(self):
        a = Circle()
        b = Square()
        c = Triangle()
        self.play(ReplacementTransform(a, b))
        self.play(ReplacementTransform(b, c))
        self.play(FadeOut(c))

    def construct(self):
        self.transform()
        self.wait(0.5)  # wait for 0.5 seconds
        self.replacement_transform()
Manim Community v0.18.1

Animation 0: Transform(Circle):   0%|          | 0/15 [00:00<?, ?it/s]
Animation 1: Transform(Circle):   0%|          | 0/15 [00:00<?, ?it/s]
Animation 2: FadeOut(Circle):   0%|          | 0/15 [00:00<?, ?it/s]
Animation 4: ReplacementTransform(Circle):   0%|          | 0/15 [00:00<?, ?it/s]
Animation 5: ReplacementTransform(Square):   0%|          | 0/15 [00:00<?, ?it/s]
Animation 6: FadeOut(Triangle):   0%|          | 0/15 [00:00<?, ?it/s]

然而,在某些情况下,使用 Transform 更为有利,例如当你需要依次转换多个 mobject 时。下面的代码避免了必须保留对上一个被转换的 mobject 的引用的麻烦。

%%manim -v WARNING --disable_caching -ql TransformCycle
config.media_embed = True
config.media_width = "25%"
class TransformCycle(Scene):
    def construct(self):
        a = Circle()
        t1 = Square()
        t2 = Triangle()
        self.add(a)
        self.wait()
        for t in [t1,t2]:
            self.play(Transform(a,t))
Manim Community v0.18.1

Animation 1: Transform(Circle):   0%|          | 0/15 [00:00<?, ?it/s]
Animation 2: Transform(Circle):   0%|          | 0/15 [00:00<?, ?it/s]