manim是一个基于python的数学动画制作擎,如果还不会配置环境的话,可以去3b1b看看,环境配置完成后,关于manim工程结构与程序执行流程级的讲解在我的另一篇文章中Argparse在manim中的应用
关于manim的入门,我已经在我的另一篇文章中介绍了(manim入门),下面的主要内容是和视频具体创作过程相关的。这篇文章的内容主要涉及到几何图形、文本、公式的创建、布局及动画展示
。下面不会有具体的动画展示内容,更多的是一种直接的功能说明。我将会介绍在这些基本对象的创建与展示的过程中用到的方法,同时会对这些方法有一个创作流程级(每一个行业,每一个民族,每一个个人,都有它们的一条流程,行业中叫做行业规范,民族中叫做民风民俗,个人叫做个性特征,同样manim也有它的创作所遵循的基本流程与思想
)的总结。如果你是想照着文章内容写程序,也许可以成功,也许不行。成功当然OK,如果失败了,那么就去多查一些其它的资料解决它(最好是官方推荐的),毕竟找问题并解决它是学习的最好方法
。
我们从一个小例子说起
class Shapes(Scene):
#A few simple shapes
def construct(self):
circle = Circle()
square = Square()
line=Line(np.array([3,0,0]),np.array([5,0,0]))
triangle=Polygon(np.array([0,0,0]),np.array([1,1,0]),np.array([1,-1,0]))
self.add(line)
self.play(ShowCreation(circle))
self.play(FadeOut(circle))
self.play(GrowFromCenter(square))
self.play(Transform(square,triangle))
这个例子虽然小,但它其实已经包含了创建一个manim动画所需要的一切基本组件,我们看到Shapes这个类继承自Scene,我们就把这个类叫做Shapes Scene,也可以说Shapes是一个Scene,这个Scene将会告诉manim在哪里,如何,放置哪些对象。据说,3b1b上的动画并不是一体化成型的,而是由一个个这样的Scene通过视频剪辑软件拼接起来的,每一个Scene中都有一个construct函数,在执行脚本的时候,它会被默认调用。在这个函数里,你需要实例化所有的对象,控制对象所需的代码,放置对象在屏幕上所需的代码以及与对象相关的动画代码。
在这个Scene中,我在construct函数里创建了一个圆、一个矩形,一条线,和一个多边形。下面是一个空行,然后就是这些对象相关的动画,其中的.play()方法是manim中最重要的方法之一
,它是用来具体执行与对象相关的各种动画
的方法。可以被执行的变换有很多,这里有ShowCreation(circle)、FadeOut(circle)、GrowFromCenter(square)、Transform(square,triangle),这些变换方法都可以在相关的文件中找到,具体是哪些文件,我们后面会具体展示。
先展示一个例子
class MoreShapes(Scene):
def construct(self):
circle = Circle(color=PURPLE_A)
square = Square(fill_color=GOLD_B, fill_opacity=1, color=GOLD_A)
square.move_to(UP+LEFT)
circle.surround(square)
rectangle = Rectangle(height=2, width=3)
ellipse=Ellipse(width=3, height=1, color=RED)
ellipse.shift(2*DOWN+2*RIGHT)
pointer = CurvedArrow(2*RIGHT,5*RIGHT,color=MAROON_C)
arrow = Arrow(LEFT,UP)
arrow.next_to(circle,DOWN+LEFT)
rectangle.next_to(arrow,DOWN+LEFT)
ring=Annulus(inner_radius=.5, outer_radius=1, color=BLUE)
ring.next_to(ellipse, RIGHT)
self.add(pointer)
self.play(FadeIn(square))
self.play(Rotating(square),FadeIn(circle))
self.play(GrowArrow(arrow))
self.play(GrowFromCenter(rectangle), GrowFromCenter(ellipse), GrowFromCenter(ring))
我们可以以各种各样的方式
创建各种各样的几何图形
,在geometry.py这个文件中,我们可以找到各种几何图形的类(圆弧Arc,两点圆弧ArcBetweenPoints,曲线箭头CurvedArrow,曲线双箭头CurvedDoubleArrow,圆Circle,点Dot,小点SmallDot,椭圆Ellipse,环扇AnnularSector,扇形Sector,环Annulus,线Line,多边形Polygon,三角形Triangle,矩形Rectangle等等
),基于这些类我们还可以创建属于自己的新的几何图形。这些几何图形有多种参数供我们设置,我们以上面的例子来说明。我这里仅仅介绍如何改一些基本的参数,具体改动这些参数之后有什么效果,可以自己去实践观察。
先看圆,我们给了它一个color=PURPLE_A参数,这是一个关键字参数,它会被传递给Circle这个类的一个实例变量,具体细节可以去看看源码,其实我也只是看了个大概,我这里要介绍的是在哪里可以知道能够设置哪些变量。我们在进入一个类的时候,都会看到里面有一个CONFIG字典,这个字典里面包含了这个类的一些属性,我们可以一层一层地向上追溯这些类的父类爷爷类等,找到所有的CONFIG,这些CONFIG里面的内容基本上便可以满足我们的修改需求了。
有了上面的基本认识,再看Square里面的参数便不难理解了,那些参数都是在它的类或者更上层的类中找到的。代码中的其它一些需要位置参数的图形需要根据具体的参数意义去进行参数设定。
关于布局,任何的布局,我们首先要了解的是坐标系统,下面的代码是是constants.py里面的内容
FRAME_HEIGHT = 8.0
FRAME_WIDTH = FRAME_HEIGHT * DEFAULT_PIXEL_WIDTH / DEFAULT_PIXEL_HEIGHT
FRAME_Y_RADIUS = FRAME_HEIGHT / 2
FRAME_X_RADIUS = FRAME_WIDTH / 2
我们可以看到,高度默认被分成了8份(通过更改这个值,可以实现坐标系统精度的控制,也就是屏幕被切割成多少个小方块),宽度按照与高度的比例被分成相应的份数(如果宽高比是2,那么,宽度将被分成16份),最终我们应该在脑海中有一个由小方块组成的坐标系统,坐标系统原点在视频中心。
有了manim坐标系统的知识之后,还是以上面的小例子来说明几何图形对象的布局
class MoreShapes(Scene):
def construct(self):
circle = Circle(color=PURPLE_A)
square = Square(fill_color=GOLD_B, fill_opacity=1, color=GOLD_A)
square.move_to(UP+LEFT)
circle.surround(square)
rectangle = Rectangle(height=2, width=3)
ellipse=Ellipse(width=3, height=1, color=RED)
ellipse.shift(2*DOWN+2*RIGHT)
pointer = CurvedArrow(2*RIGHT,5*RIGHT,color=MAROON_C)
arrow = Arrow(LEFT,UP)
arrow.next_to(circle,DOWN+LEFT)
rectangle.next_to(arrow,DOWN+LEFT)
ring=Annulus(inner_radius=.5, outer_radius=1, color=BLUE)
ring.next_to(ellipse, RIGHT)
self.add(pointer)
self.play(FadeIn(square))
self.play(Rotating(square),FadeIn(circle))
self.play(GrowArrow(arrow))
self.play(GrowFromCenter(rectangle), GrowFromCenter(ellipse), GrowFromCenter(ring))
几何图形布局主要分为两大种方法:基于自身方法,基于外界方法
,在这个例子中主要是基于自身方法的布局,基于外界方法的布局后面介绍,从这个例子中,我们看到了.move_to、.shift、.surround、.next_to等方法,这些都是Mobject这个类所提供的方法,由于大多数几何图形都是基于这个类的,所以大多数几何图形都能够使用这些方法来对自身进行布局,下面我们来说说一些常用布局的含义,以及使用方法。
.shift()
:通过观察其函数定义,我们发现,它可以接收无数个元组或者np.array类型的参数,在这里,我们给它的是2DOWN+2RIGHT,DOWN和RIGHT都是np.array类型的对象,它们由三个数组成,每一个代表坐标系统的一维(manim坐标系统的三维指的是:上下、左右、内外),这些np.array对象可以进行组合以达到几何图形的布局功能(关于DOWN这些常量,我们可以在constants.py这个文件中找到,可以自己去观察一下,相信大有裨益
)
.move_to()
:通过观察其函数定义,我们发现,我们可以传递一个np.array数组,也可以传递一个对象
,传递np.array数组,我们将直接得到传送目的地坐标,传递对象,我们将在函数内部通过相应的操作得到这个对象的位置,然后把这个位置当作传送目的地坐标
.surround():通过观察函数的定义,它必须接受一个被环绕对象,创建完之后,调用这个方法的对象就会在布局上围绕着这个被环绕对象
.next_to():通过观察函数定义,我们可以发现,它必须接收一个几何对象或者一个np.array数组,同时它可以通过direction这个关键字参数设定靠近方向,还有很多可以调整的参数。
.to_corner():用来将对象放置到角落,这个角落可以设置
.to_edge():用来将对象放置到视频边缘
这些方法mobject.py文件中。
对于这种操作,.play()这个函数十分应该是最重要的,通过观察函数的定义,我发现,.play可以接收动画对象,这些动画对象主要由animation这个文件夹中的.py文件生成,我们来看一些由哪些文件。
我们来一个一个文件看
。
功能目前不清楚
动画出现方式类
,将想要显示出来的几何对象传送给对应的类从而实现一个动画实例对象,将这个动画实例对象传送给.play()便可以实现几何图像出现时的动画控制。功能目前不清楚
功能目前不清楚
还是从一个小例子说起
class AddingText(Scene):
#Adding text on the screen
def construct(self):
my_first_text=TextMobject("Writing with manim is fun")
second_line=TextMobject("and easy to do!")
second_line.next_to(my_first_text,DOWN)
third_line=TextMobject("for me and you!")
third_line.next_to(my_first_text,DOWN)
self.add(my_first_text, second_line)
self.wait(2)
self.play(Transform(second_line,third_line))
self.wait(2)
second_line.shift(3*DOWN)
self.play(ApplyMethod(my_first_text.shift,3*UP))
我们使用TextMobject这个类来实例化一个图形化文本对象(关于这个类的内部源码探究,后期再看),这个类接收一个字符串,我们可以控制这些字体的大小、颜色、对齐等属性。我们还可以单独控制某个单词的颜色,这些控制方法都可以通过查看看它的相关类
来知晓。字符串的创建方法就是这样。
布局方法和上面的几何图形类似
大部分的变换操作与几何图形类似,但这里要讲解前面的基于自身方法,基于外界方法
中的基于外界方法
,我们这里用到了一个方法ApplyMethod(),在上面小例子中的最后一行。下面来仔细研究一下这个类
class ApplyMethod(Transform):
def __init__(self, method, *args, **kwargs):
"""
method is a method of Mobject, *args are arguments for
that method. Key word arguments should be passed in
as the last arg, as a dict, since **kwargs is for
configuration of the transform itslef
Relies on the fact that mobject methods return the mobject
"""
self.check_validity_of_input(method)
self.method = method
self.method_args = args
super().__init__(method.__self__, **kwargs)
def check_validity_of_input(self, method):
if not inspect.ismethod(method):
raise Exception(
"Whoops, looks like you accidentally invoked "
"the method you want to animate"
)
assert(isinstance(method.__self__, Mobject))
这是这个类的构造函数,作者给这个构造函数做了一些文档字符串
用以说明这个类的使用方法。从文档字符串中我们可以知道一些信息:method是一个Mobject对象的方法(注意一定要是某个对象的方法,如circle.shift),*args是用来提供给circle.shift的参数,后面是一些关键字参数。
在后面的代码中,首先检查了method参数的有效性,然后将method赋值给这个self.method,将args赋值给self.method_args,紧接着是调用父类的构造函数,它的父类是Transform
class Transform(Animation):
CONFIG = {
"path_arc": 0,
"path_arc_axis": OUT,
"path_func": None,
"replace_mobject_with_target_in_scene": False,
}
def __init__(self, mobject, target_mobject=None, **kwargs):
super().__init__(mobject, **kwargs)
self.target_mobject = target_mobject
self.init_path_func()
由此我们得到一个等价逻辑,ApplyMethod(circle0.shift, UP) 等价于使用Transform将当前的cricle0
变换成一个由circle0移动一个UP后的新的对象
。
下面再介绍一些相关的操作(不限于文本使用)
下面是一个小例子
class BasicEquations(Scene):
#A short script showing how to use Latex commands
def construct(self):
eq1=TextMobject("$\\vec{X}_0 \\cdot \\vec{Y}_1 = 3$")
eq1.shift(2*UP)
eq2=TexMobject(r"\vec{F}_{net} = \sum_i \vec{F}_i")
eq2.shift(2*DOWN)
self.play(Write(eq1))
self.play(Write(eq2))
第一种方法是使用TextMobject(),这种方法不好。我们使用TexMobject方法,这种方法要求我们提供原始latex字符串(就是在字符串前面加个r
)。可能有的朋友对latex不太熟悉,那没办法,只能自己去学习一下了(不用害怕,很简单
)。
下面是一个小例子
class UsingBraces(Scene):
#Using braces to group text together
def construct(self):
eq1A = TextMobject("4x + 3y")
eq1B = TextMobject("=")
eq1C = TextMobject("0")
eq2A = TextMobject("5x -2y")
eq2B = TextMobject("=")
eq2C = TextMobject("3")
eq1B.next_to(eq1A,RIGHT)
eq1C.next_to(eq1B,RIGHT)
eq2A.shift(DOWN)
eq2B.shift(DOWN)
eq2C.shift(DOWN)
eq2A.align_to(eq1A,LEFT)
eq2B.align_to(eq1B,LEFT)
eq2C.align_to(eq1C,LEFT)
eq_group=VGroup(eq1A,eq2A)
braces=Brace(eq_group,LEFT)
eq_text = braces.get_text("A pair of equations")
self.add(eq1A, eq1B, eq1C)
self.add(eq2A, eq2B, eq2C)
self.play(GrowFromCenter(braces),Write(eq_text))
如果前面理解得不错的话,那么一直到braces = Brace(eq_group,left)应该都够流畅地理解(如果不能也没关系,要么重新看看本篇文章,要么去找别的资料看看,可能我的的思路或者表达风格不适合你
)。Brace是大括号的意思,我们在eq_group左边创建了一个大括号,同时这个大括号跟着一些文本,在后面的动画显示中,我们会把这个大括号和文本显示出来。
有没有觉得上面的方法过于繁琐,我第一眼见到这样的操作就感觉这实在是过于繁琐,一定有优雅一点的方法实现这些效果,下面是一个小例子
class UsingBracesConcise(Scene):
#A more concise block of code with all columns aligned
def construct(self):
eq1_text=["4","x","+","3","y","=","0"]
eq2_text=["5","x","-","2","y","=","3"]
eq1_mob=TexMobject(*eq1_text)
eq2_mob=TexMobject(*eq2_text)
eq1_mob.set_color_by_tex_to_color_map({
"x":RED_B,
"y":GREEN_C
})
eq2_mob.set_color_by_tex_to_color_map({
"x":RED_B,
"y":GREEN_C
})
for i,item in enumerate(eq2_mob):
item.align_to(eq1_mob[i],LEFT)
eq1=VGroup(*eq1_mob)
eq2=VGroup(*eq2_mob)
eq2.shift(DOWN)
eq_group=VGroup(eq1,eq2)
braces=Brace(eq_group,LEFT)
eq_text = braces.get_text("A pair of equations")
self.play(Write(eq1),Write(eq2))
self.play(GrowFromCenter(braces),Write(eq_text))
这里用到了python中的解包操作与enumerate()函数以及.set_color_by_tex_to_color_map()成员函数,理解这几个函数其余的应该就没问题了
这个和前面的几何图形对象与文本对象的展示是一样的方法,如果有特殊的操作,我后面会补充
我在这里对上面manim做一个创作流程级的总结。
manim的基本创作流程是创建对象、布局对象、动画展示对象,其中创建对象有的人喜欢用到什么创建什么,有的人喜欢一次性创建完,这要看个人喜好。布局对象,根据我们上面的总结,我们可以发现大概有两种方法:基于自身方法(如.next_to),基于外界方法(如VGroup),动画展示对象中有两个最重要的方法.add()和.play(),所有创建的动画都要通过.play()展示出来。