the code snippets can be selected by triple clicking on the code window
Over the course of this year (2021/2022), I created a well-received “Introduction to Manim” series for KSP (Czech CS-oriented correspondence seminar), so it made sense to make it more accessible by translating it to English and publish it here.
Animations are better than pictures, be it when presenting interesting ideas or visualizing algorithms.
That is why it’s useful to know how to create them, ideally programmatically.
This is the main motivation behind learning Manim – a Python library created by Grant Sanderson (3b1b), which this multipart series aims to cover.
Preface
To follow the series, basic knowledge of Python is required.
It is also useful to know the basics of TEX, which we’ll use to typeset math and text, but it is not required.
You will also need to install Manim if you wish to try the example code yourself.
Each part of the series contain a number of tasks for the reader to implement, which use the covered concepts.
The author’s solutions are always provided (you are, however, highly encouraged to try to implement them first).
The classes/methods discussed in the series are accompanied by the links to their pages on the official Manim documentation, which contains a more comprehensive description and the source code.
First animations
The basic building block of Manim are scenes, which are Python classes inheriting the Scene
class.
Each of the scenes must implement the construct
method, which contains information about how the scene looks like (creating shapes, moving them, changing, color and size, etc.).
Here is an example of a simple scene that creates a red Square
and then a blue Circle
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
frommanimimport*classIntro(Scene):defconstruct(self):# create square and circle objects (and move them)square=Square(color=RED).shift(LEFT*2)circle=Circle(color=BLUE).shift(RIGHT*2)# animate writing them on screenself.play(Write(square),Write(circle))# fading them from the sceneself.play(FadeOut(square),FadeOut(circle),run_time=2)
To render the video, we will use the manim <file_name> -pqm command, where
-p is the preview flag, telling Manim that we want to immediately view the result and
-qm sets the quality to medium (others being low, high and 4k),
yielding the following video:
The method self.play
always expects a non-zero number of animations that it plays at the same time.
We’re calling it with the Write
and FadeOut
animations above, which create and hide objects passed to them.
The optional parameter run_time sets the animation duration (in seconds), defaulting to 1 if nothing is passed.
There are also a number of builtin constants used (LEFT, RIGHT, RED, BLUE).
These are constants that Manim uses to make the code more readable.
For a more comprehensive list, here are the colors
and here are other
constants.
After both of the objects are created, the shift
method is used to move them in the given direction, also returning them.
This is how the vast majority of functions on Manim objects are implemented, mainly to avoid having to use the following (valid but verbose) syntax:
1
2
3
4
5
6
# thissquare=Square(color=RED)square.shift(LEFT*2)# is the same as thissquare=Square(color=RED).shift(LEFT*2)
Since the direction constants are NumPy arrays, they can be added and multiplied by constants with ease – to move to the side and slightly up, we can simply do obj.shift(LEFT + UP * 1.5).
animate syntax
Besides creating and hiding objects, we’d like to animate their attributes like color, position and orientation.
The shift
function does move the object, but it doesn’t do so visually.
This can be done in a number of ways, arguably the most elegant being the animate
syntax, which can be used via the magic animate word after the object:
frommanimimport*classAnimate(Scene):defconstruct(self):square=Square(color=RED).shift(LEFT*2)circle=Circle(color=BLUE).shift(RIGHT*2)self.play(Write(square),Write(circle))# moving objectsself.play(square.animate.shift(UP*0.5),circle.animate.shift(DOWN*0.5))# rotating and filling the square (opacity 80%)# scaling and filling the circle (opacity 80%)self.play(square.animate.rotate(PI/2).set_fill(RED,0.8),circle.animate.scale(2).set_fill(BLUE,0.8),)# change colorself.play(square.animate.set_color(GREEN),circle.animate.set_color(ORANGE),)self.play(FadeOut(square),FadeOut(circle))
The example shows a number of properties that each object has (position, orientation, color) and that can be animated.
As you can see from the code, the animations can be chained to change a number of them at once – just make sure that they are not conflicting (moving both up and down at the same time, for example).
Aligning objects
We’ve seen that the shift
function moves an object in the given direction.
Sometimes, however, it might be more convenient to move it in relation to other objects in the scene.
next_to
For moving one object next to another, we’ll use the next_to
function:
frommanimimport*classNextTo(Scene):defconstruct(self):c1,c2,c3,c4=[Circle(radius=0.5,color=WHITE)for_inrange(4)]rectangle=Rectangle(width=5,height=3)# use Python's * syntax to write the objects# does the following: f(*[1, 2, 3]) == f(1, 2, 3)self.play(*[Write(o)foroin[c1,c2,c3,c4,rectangle]])# move the circles such that they surround the rectangleself.play(c1.animate.next_to(rectangle,LEFT),c2.animate.next_to(rectangle,UP),c3.animate.next_to(rectangle,RIGHT),c4.animate.next_to(rectangle,DOWN),)
move_to
For moving one object on top of another, we’ll use the move_to
function:
frommanimimport*classMoveTo(Scene):defconstruct(self):s1,s2,s3=[Square()for_inrange(3)]self.play(*[Write(o)foroin[s1,s2,s3]])# align squares next to one anotherself.play(s1.animate.next_to(s2,LEFT),s3.animate.next_to(s2,RIGHT),)# create numbers for each of them# the Tex class will be discussed belowt1,t2,t3=[Tex(f"${i}$").scale(3)foriinrange(3)]# move the numbers on top of the squarest1.move_to(s1)t2.move_to(s2)t3.move_to(s3)self.play(*[Write(o)foroin[t1,t2,t3]])
align_to
For moving one object on the “same level” as another, we’ll use the align_to
function:
frommanimimport*classAlignTo(Scene):defconstruct(self):c1,c2,c3=[Circle(radius=1.5-i/3,color=WHITE)foriinrange(3)]self.play(*[Write(o)foroin[c1,c2,c3]])# align such that c1 < c2 < c3self.play(c1.animate.next_to(c2,LEFT),c3.animate.next_to(c2,RIGHT),)# align c1 and c2 such that their bottoms are the same as c2self.play(c1.animate.align_to(c2,DOWN),c3.animate.align_to(c2,DOWN),)point=[0,2.5,0]# align all circles such that their top touches a line going through the pointself.play(c1.animate.align_to(point,UP),c2.animate.align_to(point,UP),c3.animate.align_to(point,UP),)
Typesetting text and math
Manim supports setting in TEX (including math).
To do this, we’ll be using the Tex
for setting text and MathTex
for setting math.
If you’re not familiar with setting math in TEX, you can use one of many online editors (such as this one).
1
2
3
4
5
6
7
8
9
10
11
12
13
frommanimimport*classTextAndMath(Scene):defconstruct(self):text=Tex("Hello Manim!").shift(LEFT*2.5)# note that we're using Python's r-strings for cleaner codeformula=MathTex(r"\sum_{i = 0}^\infty \frac{1}{2^i} = 2").shift(RIGHT*2.5)self.play(Write(formula),Write(text))self.play(FadeOut(formula),FadeOut(text))
An alternative to using the Tex
class is the Text
class, which internally doesn’t use TEX and thus renders a(n arguably) worse-looking text, but is easier to work with when dealing with non-english characters.
Tasks
Shuffle
Create an animation of random shuffling.
The Swap
animation may be used for swapping the position of two objects.
Its optional parameter path_arc, which determines the angle under which they are swapped, might also be useful.
frommanimimport*fromrandomimport*classShuffle(Scene):defconstruct(self):seed(0xDEADBEEF)# number of values to shufflen=5circles=[Circle(color=WHITE,fill_opacity=0.8,fill_color=WHITE).scale(0.6)for_inrange(n)]# spacing between the circlesspacing=2fori,circleinenumerate(circles):circle.shift(RIGHT*(i-(len(circles)-1)/2)*spacing)self.play(*[Write(circle)forcircleincircles])# selected circleselected=randint(0,n-1)self.play(circles[selected].animate.set_color(RED))self.play(circles[selected].animate.set_color(WHITE))# slowly increase speed when swappingswaps=13speed_start=1speed_end=0.2foriinrange(swaps):speed=speed_start-abs(speed_start-speed_end)/swaps*i# pick two random circles (ensuring a != b)a,b=sample(range(n),2)# swap with a slightly larger arc angleself.play(Swap(circles[a],circles[b]),run_time=speed,path_arc=135*DEGREES)# highlight the initial circle againself.play(circles[selected].animate.set_color(RED))self.play(circles[selected].animate.set_color(WHITE))
Sort
Create an animation of a sequence being sorted.
The stretch_to_fit_height
function (combined with the animate syntax) may be useful for changing the height of the object, without scaling it proportionally.
frommanimimport*fromrandomimport*classSort(Scene):defconstruct(self):seed(0xDEADBEEF)n=20value_min,value_max=1,20values=[randint(value_min,value_max)for_inrange(n)]# width of rectangles and the height of a single unitrectangle_width=0.2unit_height=0.2rectangle_spacing=2.5rectangles=[Rectangle(width=rectangle_width,height=unit_height*v,fill_color=WHITE,fill_opacity=1,)forvinvalues]# calculate the point at which to align all of the rectangles (so they're all centered)alignment_point=Nonemax_value=0fori,vinenumerate(values):ifmax_value<v:max_value=valignment_point=Point().shift(DOWN*rectangles[i].height/2)fori,rectinenumerate(rectangles):rect.shift(RIGHT*(i-(len(rectangles)-1)/2)*rectangle_width*rectangle_spacing).align_to(alignment_point,DOWN)self.play(*[Write(r)forrinrectangles])defanimate_at(a,b,duration):"""Animate that we're looking at the positions a and b."""self.play(*[r.animate.set_color(WHITEifinotin(a,b)elseYELLOW)fori,rinenumerate(rectangles)],run_time=duration,)defanimate_swap(a,b,duration):"""Animate the swap of positions a and b."""self.play(rectangles[a].animate.stretch_to_fit_height(values[a]*unit_height).align_to(alignment_point,DOWN),rectangles[b].animate.stretch_to_fit_height(values[b]*unit_height).align_to(alignment_point,DOWN),run_time=duration,)# the first pass is slowerspeed_slow=0.6speed_fast=0.07foriinrange(n):speed=speed_slowifi==0elsespeed_fastswapped=Falseforjinrange(n-i-1):animate_at(j,j+1,speed)ifvalues[j]>values[j+1]:values[j],values[j+1]=values[j+1],values[j]animate_swap(j,j+1,speed)swapped=True# if the sequence is sorted, stopifnotswapped:breakself.play(*[FadeOut(r)forrinrectangles])
Search
Create an animation of binary searching a random sorted sequence.
The Arrow
object is very useful for creating the position indicators.
frommanimimport*fromrandomimport*classSearch(Scene):defconstruct(self):seed(0xDEADBEEF1)# prettier inputn=10value_min,value_max=1,nvalues=sorted([randint(value_min,value_max)for_inrange(n)])square_side_length=0.75square_spacing=1.3squares=[Square(side_length=square_side_length)forvinvalues]numbers=[Tex(f"${v}$")forvinvalues]# move rectangles such that they are centeredfori,rectinenumerate(squares):rect.shift(RIGHT*(i-(len(squares)-1)/2)*square_side_length*square_spacing)# label positionsfori,numberinenumerate(numbers):number.move_to(squares[i])pointer_length=0.4l_pointer=Arrow(start=DOWN*pointer_length,end=UP).next_to(squares[0],DOWN)r_pointer=Arrow(start=DOWN*pointer_length,end=UP).next_to(squares[-1],DOWN)self.play(*[Write(s)forsinsquares],*[Write(n)forninnumbers])# print the number we're looking fortarget=randint(value_min,value_max)text=Tex(f"Target: ${target}$").shift(UP*1.5)self.play(Write(text))self.play(Write(l_pointer),Write(r_pointer))lo,hi=0,len(values)-1defcolor_in_range(objects,color,range):"""Return the animation of coloring the objects in the sequence."""return[o.animate.set_color(color)fori,oinenumerate(objects)ifiinrange]whilelo<hi:avg=(lo+hi)//2current_arrow=(Arrow(start=DOWN*pointer_length,end=UP).next_to(squares[avg],DOWN).set_color(ORANGE))self.play(Write(current_arrow))ifvalues[avg]<target:# move left pointerself.play(FadeOut(current_arrow),l_pointer.animate.next_to(squares[avg+1],DOWN),*color_in_range(squares,DARK_GRAY,range(lo,avg+1)),*color_in_range(numbers,DARK_GRAY,range(lo,avg+1)),)lo=avg+1elifvalues[avg]>=target:# move right pointerself.play(FadeOut(current_arrow),r_pointer.animate.next_to(squares[avg],DOWN),*color_in_range(squares,DARK_GRAY,range(avg+1,hi+1)),*color_in_range(numbers,DARK_GRAY,range(avg+1,hi+1)),)hi=avg# the desired value has been foundifvalues[hi]==target:self.play(*color_in_range(squares,DARK_GRAY,range(hi)),*color_in_range(squares,DARK_GRAY,range(hi+1,n)),*color_in_range(numbers,DARK_GRAY,range(hi)),*color_in_range(numbers,DARK_GRAY,range(hi+1,n)),numbers[hi].animate.set_color(GREEN),squares[hi].animate.set_color(GREEN),FadeOut(l_pointer),)breakself.play(*[FadeOut(r)forrinnumbers+squares+[r_pointer,text]])