Skip to content

Examples of Scene and Study Notes based on Manim Community Edition 📹

Notifications You must be signed in to change notification settings

leekunhwee/manimce

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

欢迎来到 ManimCE 中文教程


目录 Table of Contents:


中文版 Jupyter Notebook 演示文件在本库的:First Steps with Manim.ipynb,需要将其下载下来,放到 Jupyter Notebook 的默认运行路径下,运行 Jupyter Notebook,然后在自动跳转的网页中打开。

其他相关资源: Documentation, Discord, Reddit

编程环境配置

需要安装两个软件:

Anaconda:一个开源的 Python 和 R 语言的发行版本,常用于计算科学,其致力于简化软件包管理系统和部署。

MikTeX:一款免费的跨平台文字处理系统,包含了 TeX 及其相关程序。

点击软件名即可跳转下载链接,下载后直接按照提示步骤默认安装即可,由于安装过程简单,此处不加赘述。

创建虚拟环境

利用 Anaconda 新建开发环境(虚拟环境),这样可以进行多版本的灵活切换,避免很多版本冲突问题。

Anaconda Navigator 中的 Environments 里,点击 base(root) 的右边箭头,Open Terminal 打开终端,可发现上图的路径前有一个 (base),这说明当前运行环境为主系统环境。

输入 conda create -n <虚拟环境名>,回车。这时在 cmd 中就可以激活并进入该新建的环境了。

cmd 中激活该环境可以用 conda activate <虚拟环境名> ,取消激活可以用 conda deactivate

:此步骤中使用 开始 -> Anaconda -> Anaconda Prompt 也可以实现同 cmd 一样的效果。

安装依赖文件

在该虚拟环境下,安装一个 3.9 版本的 Python(当前 manimCE 版支持 3.7-3.9 版本的 Python),输入:conda install python=3.9,回车。

再安装提供影视串流功能的 ffmpeg,输入:conda install ffmpeg,回车。

安装 manim

manim Community Edition 可以使用 pip 工具直接安装,非常方便。直接输入:pip install manim,回车。

安装配置 Jupyter

在命令行运行生成视频时代码与结果分离,不易于对照与比较,如果使用 Jupyter Notebook 则可以达到很好的视觉效果,也方便记录编程的过程。安装完 Anaconda 之后,系统会默认在主系统环境中安装 Jupyter Notebook,为了在虚拟环境中使用 Jupyter Notebook,需要再在虚拟环境中安装一次。

在虚拟环境中安装 Jupyter

在激活新环境的基础上(可以看到命令行开头处的括号内显示新的环境名),输入 conda install jupyter notebook,安装 Jupyter Notebook

为了在虚拟环境中运行 Jupyter Notebook,一方面需要在虚拟环境中安装 Jupyter Notebook,另一方面,要安装一个插件 conda install nb_conda,之后再打开 Jupyter Notebook,在 new 按钮下将多出不同环境的选项。

注意,关于 Jupter Notebook 的安装与配置,只有该步是在虚拟环境中进行的,下面的安装、配置均在主系统环境(base)中进行。

更改默认路径

在 Anaconda prompt 的主系统环境(base)中,输入:jupyter notebook --generate-config ,可以生成一个路径配置文件,用以配置 Jupyter 每次打开时的默认路径。

该新生成的文件路径一般在 C:\Users\<User_name>\.jupyter\jupyter_notebook_config.py 中。

打开该文件,找到 ## The directory to use for notebooks and kernels. 这一行。将其下 c.NotebookApp.notebook_dir = 一行开头的 # 删除并添加自己的路径(注意:路径的分隔符要由 \ 改为 \\,或者,在路径前加 r),然后保存。

那么下一次从 Anaconda Navigator 打开 Jupyter Notebook 的时候,将跳转到自定义的路径。(如果还是不行,可能是因为 Jupyter 快捷方式的属性中,路径一行的末尾存在 %USERPROFILE%,进而导致设置无效,将 %USERPROFILE% 删去即可。)

优化编程界面

点击打开 Jupyter themes 找到优化方法。 由于 Anaconda 中不包含这个包,所以这里需要用 pip 进行安装。

在 Anaconda prompt 的主系统环境(base)中,输入: pip install jupyterthemes 进行安装,后面可以通过 pip install --upgrade jupyterthemes 进行升级。

可参考输入配置参数:jt -t oceans16 -f fira -fs 17 -cellw 90% -ofs 14 -dfs 14 -T 进行优化界面配置。

代码自动补全

在 Anaconda prompt 的主系统环境(base)中,输入:

pip install jupyter_contrib_nbextensions && jupyter contrib nbextension install

然后运行:

jupyter contrib nbextension install --user --skip-running-check

Anaconda Navigator 打开 Jupyter Notebook,会发现多了一个 Nbextes 选项,点击并勾选里面的 HinterlandTable of Contents

配置完毕!

快捷键

  • Enter :进入编辑模式
  • Esc :进入命令模式
  • Y :单元转入代码状态
  • M :单元转入 markdown 状态
  • R :单元转入 raw 状态
  • Ctrl+Enter :运行
  • O :展开折叠输出
  • H :显示快捷键
  • A :在上方插入代码块
  • B :在下方插入代码块
  • D,D:删除代码块

导入 manim 库

打开 Jupyter Notebook,在虚拟环境中新建文件,或打开已有文件后通过 Kernel -> Change Kernel -> Python [conda env:<虚拟环境名>]

在代码块中输入 import * 来导入 manim 中的所有库。点击 Run 按钮或 Shift+EnterCtrl+Enter 来运行。

第二行是用来控制输出视频的展示画面的宽度,可根据自己的需要进行调整。

from manim import *

config.media_width = "60%"
Manim Community v0.13.1

如果运行成功,将会输出已安装的 manim 的版本。

第一个 Scene

manim 通过实例化 Scenes 来生成视频。这些 Scenes 实际上是一系列特殊 的实例,它们都拥有一个用以描述动画的 construct 方法。(为了方便本教程的描述,这里没有顾及读者是否熟悉 Python 或诸如 方法 等面向对象语言的专业术语,不过如果读者想继续学习 manim,有必要先学习一下 Python 编程基础。)

运行下面的例子,查看输出的视频。

%%manim -v WARNING -qm CircleToSquare

class CircleToSquare(Scene):
    def construct(self):
        blue_circle = Circle(color=BLUE, fill_opacity=0.5)
        green_square = Square(color=GREEN, fill_opacity=0.8)
        self.play(Create(blue_circle))
        self.wait()
        
        self.play(Transform(blue_circle, green_square))
        self.wait()

Circle To Square

尽管这个例子中大部分代码看上去是不言自明是,这里仍需要一步步详细地进行说明。

首先,

%%manim -v WARNING -qm CircleToSquare

是一个 魔术指令 (magic command),该指令仅对 Jupyter notebook 有效。这条命令等价于在终端调用 manim 命令。

-v WARNING 将隐去所有暂时用不到的输出信息 (如果你想看这些信息,可以将命令中的这部分代码去掉)。标志 -qm 控制输出视频的画面质量,它是 --quality=m 的缩写,即中等质量。这意味着输出的视频将为 30帧 720p。(可以尝试将其改为 -qh-ql 分别代表 高 (high)低 (low) 质量。)

CircleToSquareScene 类实例化之后的名称。之后几句分别为:

class CircleToSquare(Scene):
    def construct(self):
        [...]

def 后面的这个整体定义了这个名为 CircleToSquare 的 manim 实例,该实例中的 construct 方法中精确描述了将展现怎样的动画内容及效果,其可被看做是一张描绘着该动画内容及效果的 蓝图

blue_circle = Circle(color=BLUE, fill_opacity=0.5)
green_square = Square(color=GREEN, fill_opacity=0.8)

前两行分别创建了名为 CircleSquare 的两个 Mobject 对象,它们分别拥有各自的颜色及填充透明度。 但是,它们尚未加入到动画场景中。为了展现,需要用到 self.add,或者 ...

self.play(Create(blue_circle))
self.wait()

... 将一个 manim 对象 (Mobject) 添加到场景中的过程以动画形式展现出来。在该方法中 self 代表的是当前场景,self.play(my_animation) 可以解释为:"该场景中需要演示我的动画" 。

Create 就是一个动画,不过这里还有很多其他类型的动画 (例如 FadeIn,或者 DrawBorderThenFill,可以自行尝试!)。调用 self.wait() 的目的就如你想的那样,让动画画面暂停一会儿 (默认是 1 秒)。如果改为 self.wait(2),将暂停两秒。

最后两行,

self.play(Transform(blue_circle, green_square))
self.wait()

用于展现从蓝色圆圈到绿色方块的变化过程 (并加上 1 秒的暂停)。

定位 Mobjects 并将其移动

新问题:创建一个场景,其中创建圆圈,同时在其下方写出一些文字。可以复用上面给出的蓝色圆圈,然后再加些新代码。

%%manim -v WARNING -qm HelloCircle

class HelloCircle(Scene):
    def construct(self):
        # blue_circle = Circle(color=BLUE, fill_opacity=0.5)
        # 此处也可以先创建一个“默认的”圆圈,再通过 set 方法添加自己想要的属性!
        circle = Circle()
        blue_circle = circle.set_color(BLUE).set_opacity(0.5)
        
        label = Text("A wild circle appears!")
        label.next_to(blue_circle, DOWN, buff=0.5)
        
        self.play(Create(blue_circle), Write(label))
        self.wait()

Hello Circle

显然,文字可以使用名为 Text 的 Mobject 对象来创建,其所处位置由下面一行代码实现:

label.next_to(blue_circle, DOWN, buff=0.5)

Mobject 对象有好几种方法实现定位,next_to 是其中之一 (shift, to_edge, to_corner, move_to 则是另外一些 – 具体可通过搜索框查阅 documentation )。对于 next_to 方法,第一个参数 (blue_circle) 代表 label 所附着的 Mobject 对象。第二个参数(DOWN),描述了附着的方向 (可以改为 LEFTUP,或 RIGHT 试试!)。最后,buff=0.5 控制了 blue_circlelabel 这两个 Mobject 间的距离,增大赋值将使得 label 向下方远离 blue_circle

另外需要注意的是,这里 self.play 的调用形式也发生了改变:可以将多个动画参数同时赋予 self.play 方法,那么这些动画将同步展现。如果想一个接着一个地展现,那么就将 self.play 方法分开来写为几行:

self.play(Create(blue_circle))
self.play(Write(label))

并看看有什么变化。

顺便说一下,Mobject 对象自带与定位无关的方法:例如,为了得到蓝色圆圈,也可以创建一个默认的圆圈,然后设置其颜色和不透明度:

circle = Circle()
blue_transparent_circle = circle.set_color(BLUE)
blue_circle = blue_transparent_circle.set_opacity(0.5)

也可将上面的代码简写为:

blue_circle = Circle().set_color(BLUE).set_opacity(0.5)

像这样,可以直接在名为 Circle 的圆圈上附加属性。

方法的动画

在上一个例子中,用到了 .next_to 方法,这是一种修改 Mobject 对象位置的方法。但如何去演示出这些方法对 Mobject 对象的作用过程,或者说方法 .shift.rotate 或者 .scale 到底是如何影响一个 Mobject 对象的?那么,.animate 语法就是该问题的答案,让我们看看这个例子。

%%manim -v WARNING -qm CircleAnnouncement

class CircleAnnouncement(Scene):
    def construct(self):
        blue_circle = Circle(color=BLUE, fill_opacity=0.5)
        announcement = Text("Let us draw a circle.")
        
        self.play(Write(announcement))
        self.wait()
        
        self.play(announcement.animate.next_to(blue_circle, UP, buff=0.5))
        self.play(Create(blue_circle))

Circle Announcement

在一般通过 announcement.next_to(blue_circle, UP, buff=0.5) 来实现定位的地方,前置调用一个 .animate 方法,将其后的方法转为动画,并通过 self.play 方法演示出了。下面的例子展现了几种方法的作用于 Mobject 对象的过程动画:

%%manim -v WARNING -qm AnimateSyntax

class AnimateSyntax(Scene):
    def construct(self):
        triangle = Triangle(color=RED, fill_opacity=1)
        self.play(DrawBorderThenFill(triangle))
        self.play(triangle.animate.shift(LEFT))
        self.play(triangle.animate.shift(RIGHT).scale(2))
        self.play(triangle.animate.rotate(PI/3))
        self.play(triangle.animate.set_color(GREEN))

Animate Syntax

第一个 .play 方法创建了一个三角形;第二个 .play 将该三角形往左移动一个单位;第三个 .play 将其向右移回去,并同时放大到原来的两倍;第四个 .play 将其逆时针旋转 $\pi/3$ (可以修改一些参数,并再次运行上面的代码,例如 set_color)。

当你仔细看上面那个例子时,将会发现,旋转过程并不是真正的 “旋转”,而是直接变换到了转动后的一个版本。但其三个角划过的并不是圆弧(尽管其本应绕着三角形的中心旋转),而是几条直线段。这个动画给人留下的印象是:该三角形先缩小然后又扩大。

这并非是一个 bug,纯属 .animate 语法作用的效果:动画是根据初始 (Mobject 对象 triangle) 和最终 (旋转后的 Mobject 对象 triangle.rotate(PI/3)) 的两个特定状态构建。manim 试图通过插值的方式将这两种状态联系起来,但却不知道如何平滑过渡。下面的例子将清晰地说明这一点:

%%manim -v WARNING -qm DifferentRotations

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

Different Rotations

排版打印数学公式

manim 支持 LaTeX,该标识语言常用于数学排版中。详情请见 $LaTeX$ 30 分钟教程.

下面是一些在 manim 中使用 $LaTeX$ 的实例:

%%manim -v WARNING -qm CauchyIntegralFormula

class CauchyIntegralFormula(Scene):
    def construct(self):
        formula = MathTex(r"[z^n]f(z) = \frac{1}{2\pi i}\oint_{\gamma} \frac{f(z)}{z^{n+1}}~dz")
        self.play(Write(formula), run_time=3)
        self.wait()

Cauchy Integral Formula

正如该例所示,MathTex 对象可以被赋予简单的 (数学模式) $LaTeX$ 字符串。如果想赋予“一般模式”的 $LaTex$,则使用 Tex

当然,manim 也可以实现排版公式转换的可视化。如下面的例子所示:

%%manim -v WARNING -qm TransformEquation

class TransformEquation(Scene):
    def construct(self):
        eq1 = MathTex("42 {{ a^2 }} + {{ b^2 }} = {{ c^2 }}")
        eq2 = MathTex("42 {{ a^2 }} = {{ c^2 }} - {{ b^2 }}")
        eq3 = MathTex(r"a^2 = \frac{c^2 - b^2}{42}")
        self.add(eq1)
        self.wait()
        self.play(TransformMatchingTex(eq1, eq2))
        self.wait()
        self.play(TransformMatchingShapes(eq2, eq3))
        self.wait()

Transform Equation

在上面的例子中,eq1eq2 有一些双花括号的位置,在通常情况下,$LaTeX$ 不会这样表达。这是一种特殊的 manim 符号,它以一种特殊的方式将生成的 Tex 对象' eq1eq2 分组。

这种特殊的符号在使用 TransformMatchingTex 方法时很有用:它会将具有同样 TeX 字符串的部分相互转换 (例如,a^2a^2),如果没有这种特殊的符号,这个等式就会被当作一个很长的 TeX 字符串。相比之下,TransformMatchingShapes 方法就不那么智能了:它只是简单地尝试将“看起来相同”的形状转换成彼此的形状,尽管如此,它仍然非常有用。

如果您已经了解到这里,您应该对库的基本用法有了初步的印象。您可以在下面的例子中找到一些更高级的示例,它们说明了一些更专业的概念。继续,试着尝试并修改它们,就像你对上面的那些做的那样!请浏览文档 documentation,了解已经实现的内容,如果您想自己构建一些更复杂的对象,请查看源代码。

社区 community 非常欢迎问题咨询,并且我们希望您能够分享自己编写的令人称奇的项目。Happy manimating!

一些更专业的例子

在深入研究这些示例之前:请注意,它们用更加专业的概念让您了解如何设置和编写更为复杂的场景。这些例子没有附带额外的解释,它们不打算作为(入门级)学习资源

%%manim -v WARNING -qm FormulaEmphasis

class FormulaEmphasis(Scene):
    def construct(self):
        product_formula = MathTex(
            r"\frac{d}{dx} f(x)g(x) =",
            r"f(x) \frac{d}{dx} g(x)",
            r"+",
            r"g(x) \frac{d}{dx} f(x)"
        )
        self.play(Write(product_formula))
        box1 = SurroundingRectangle(product_formula[1], buff=0.1)
        box2 = SurroundingRectangle(product_formula[3], buff=0.1)
        self.play(Create(box1))
        self.wait()
        self.play(Transform(box1, box2))
        self.wait()

Formula Emphasis

%%manim -v WARNING -qm PlotExample

class PlotExample(Scene):
    def construct(self):
        plot_axes = Axes(
            x_range=[0, 1, 0.05],
            y_range=[0, 1, 0.05],
            x_length=9,
            y_length=5.5,
            axis_config={
                "numbers_to_include": np.arange(0, 1 + 0.1, 0.1),
                "font_size": 24,
            },
            tips=False,
        )

        y_label = plot_axes.get_y_axis_label("y", edge=LEFT, direction=LEFT, buff=0.4)
        x_label = plot_axes.get_x_axis_label("x")
        plot_labels = VGroup(x_label, y_label)

        plots = VGroup()
        for n in np.arange(1, 20 + 0.5, 0.5):
            plots += plot_axes.plot(lambda x: x**n, color=WHITE)
            plots += plot_axes.plot(
                lambda x: x**(1 / n), color=WHITE, use_smoothing=False
            )

        extras = VGroup()
        extras += plot_axes.get_horizontal_line(plot_axes.c2p(1, 1, 0), color=BLUE)
        extras += plot_axes.get_vertical_line(plot_axes.c2p(1, 1, 0), color=BLUE)
        extras += Dot(point=plot_axes.c2p(1, 1, 0), color=YELLOW)
        title = Title(
            r"Graphs of $y=x^{\frac{1}{n}}$ and $y=x^n (n=1, 1.5, 2, 2.5, 3, \dots, 20)$",
            include_underline=False,
            font_size=40,
        )
        
        self.play(Write(title))
        self.play(Create(plot_axes), Create(plot_labels), Create(extras))
        self.play(AnimationGroup(*[Create(plot) for plot in plots], lag_ratio=0.05))

Plot Example

%%manim -v WARNING -qm ErdosRenyiGraph

import networkx as nx

nxgraph = nx.erdos_renyi_graph(14, 0.5)

class ErdosRenyiGraph(Scene):
    def construct(self):
        G = Graph.from_networkx(nxgraph, layout="spring", layout_scale=3.5)
        self.play(Create(G))
        self.play(*[G[v].animate.move_to(5*RIGHT*np.cos(ind/7 * PI) +
                                         3*UP*np.sin(ind/7 * PI))
                    for ind, v in enumerate(G.vertices)])
        self.play(Uncreate(G))

Erdos Renyi Graph

%%manim -v WARNING -qm CodeFromString

class CodeFromString(Scene):
    def construct(self):
        code = '''from manim import Scene, Square

class FadeInSquare(Scene):
    def construct(self):
        s = Square()
        self.play(FadeIn(s))
        self.play(s.animate.scale(2))
        self.wait()
'''
        rendered_code = Code(code=code, tab_width=4, background="window",
                            language="Python", font="Monospace")
        self.play(Write(rendered_code))
        self.wait(2)

Code From String

%%manim -qm -v WARNING OpeningManim

class OpeningManim(Scene):
    def construct(self):
        title = Tex(r"This is some \LaTeX")
        basel = MathTex(r"\sum_{n=1}^\infty \frac{1}{n^2} = \frac{\pi^2}{6}")
        VGroup(title, basel).arrange(DOWN)
        self.play(
            Write(title),
            FadeIn(basel, shift=UP),
        )
        self.wait()

        transform_title = Tex("That was a transform")
        transform_title.to_corner(UP + LEFT)
        self.play(
            Transform(title, transform_title),
            LaggedStart(*[FadeOut(obj, shift=DOWN) for obj in basel]),
        )
        self.wait()

        grid = NumberPlane(x_range=(-10, 10, 1), y_range=(-6.0, 6.0, 1))
        grid_title = Tex("This is a grid")
        grid_title.scale(1.5)
        grid_title.move_to(transform_title)

        self.add(grid, grid_title)
        self.play(
            FadeOut(title),
            FadeIn(grid_title, shift=DOWN),
            Create(grid, run_time=3, lag_ratio=0.1),
        )
        self.wait()

        grid_transform_title = Tex(
            r"That was a non-linear function \\ applied to the grid"
        )
        grid_transform_title.move_to(grid_title, UL)
        grid.prepare_for_nonlinear_transform()
        self.play(
            grid.animate.apply_function(
                lambda p: p + np.array([np.sin(p[1]), np.sin(p[0]), 0])
            ),
            run_time=3,
        )
        self.wait()
        self.play(Transform(grid_title, grid_transform_title))
        self.wait()

Opening Manim

About

Examples of Scene and Study Notes based on Manim Community Edition 📹

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published