Introduction
15/10/2024 update:
- I got fed up with Manim and switched to Motion Canvas and I think you should too 🙂
27/12/2023 update:
- fixed to be up-to-date with Manim v0.18.0
- 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 , 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
.
from manim import *
class Intro(Scene):
def construct(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 screen
self.play(Write(square), Write(circle))
# fading them from the scene
self.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 tom
edium (others beingl
ow,h
igh 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 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:
# this
square = Square(color=RED)
square.shift(LEFT * 2)
# is the same as this
square = 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:
from manim import *
class Animate(Scene):
def construct(self):
square = Square(color=RED).shift(LEFT * 2)
circle = Circle(color=BLUE).shift(RIGHT * 2)
self.play(Write(square), Write(circle))
# moving objects
self.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 color
self.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:
from manim import *
class NextTo(Scene):
def construct(self):
c1, c2, c3, c4 = [Circle(radius=0.5, color=WHITE)
for _ in range(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) for o in [c1, c2, c3, c4, rectangle]])
# move the circles such that they surround the rectangle
self.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:
from manim import *
class MoveTo(Scene):
def construct(self):
s1, s2, s3 = [Square() for _ in range(3)]
self.play(*[Write(o) for o in [s1, s2, s3]])
# align squares next to one another
self.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 below
t1, t2, t3 = [Tex(f"${i}$").scale(3) for i in range(3)]
# move the numbers on top of the squares
t1.move_to(s1)
t2.move_to(s2)
t3.move_to(s3)
self.play(*[Write(o) for o in [t1, t2, t3]])
align_to
For moving one object on the “same level” as another, we’ll use the align_to
function:
from manim import *
class AlignTo(Scene):
def construct(self):
c1, c2, c3 = [Circle(radius=1.5 - i / 3, color=WHITE)
for i in range(3)]
self.play(*[Write(o) for o in [c1, c2, c3]])
# align such that c1 < c2 < c3
self.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 c2
self.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 point
self.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 (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 , you can use one of many online editors (such as this one).
from manim import *
class TextAndMath(Scene):
def construct(self):
text = Tex("Hello Manim!").shift(LEFT * 2.5)
# note that we're using Python's r-strings for cleaner code
formula = 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 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.
from manim import *
class ShuffleExample(Scene):
def construct(self):
c11 = Circle(color=WHITE).shift(UP * 1.5 + LEFT * 2)
c12 = Circle(color=WHITE).shift(UP * 1.5 + RIGHT * 2)
c21 = Circle(color=WHITE).shift(DOWN * 1.5 + LEFT * 2)
c22 = Circle(color=WHITE).shift(DOWN * 1.5 + RIGHT * 2)
self.play(Write(c11), Write(c12), Write(c21), Write(c22))
self.play(Swap(c11, c21))
self.play(Swap(c12, c22, path_arc=160 * DEGREES))
Author's Solution
from manim import *
from random import *
class Shuffle(Scene):
def construct(self):
seed(0xDEADBEEF)
# number of values to shuffle
n = 5
circles = [
Circle(color=WHITE, fill_opacity=0.8, fill_color=WHITE).scale(0.6)
for _ in range(n)
]
# spacing between the circles
spacing = 2
for i, circle in enumerate(circles):
circle.shift(RIGHT * (i - (len(circles) - 1) / 2) * spacing)
self.play(*[Write(circle) for circle in circles])
# selected circle
selected = randint(0, n - 1)
self.play(circles[selected].animate.set_color(RED))
self.play(circles[selected].animate.set_color(WHITE))
# slowly increase speed when swapping
swaps = 13
speed_start = 1
speed_end = 0.2
for i in range(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 angle
self.play(
Swap(circles[a], circles[b]), run_time=speed, path_arc=135 * DEGREES
)
# highlight the initial circle again
self.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.
from manim import *
class StretchToFitHeightExample(Scene):
def construct(self):
s1 = Square().shift(LEFT * 2.5)
s2 = Square().shift(RIGHT * 2.5)
self.play(Write(s1), Write(s2))
self.play(
s1.animate.stretch_to_fit_height(3.5),
s2.animate.set_height(3.5),
)
Author's Solution
from manim import *
from random import *
class Sort(Scene):
def construct(self):
seed(0xDEADBEEF)
n = 20
value_min, value_max = 1, 20
values = [randint(value_min, value_max) for _ in range(n)]
# width of rectangles and the height of a single unit
rectangle_width = 0.2
unit_height = 0.2
rectangle_spacing = 2.5
rectangles = [
Rectangle(
width=rectangle_width,
height=unit_height * v,
fill_color=WHITE,
fill_opacity=1,
)
for v in values
]
# calculate the point at which to align all of the rectangles (so they're all centered)
alignment_point = None
max_value = 0
for i, v in enumerate(values):
if max_value < v:
max_value = v
alignment_point = Point().shift(DOWN * rectangles[i].height / 2)
for i, rect in enumerate(rectangles):
rect.shift(
RIGHT
* (i - (len(rectangles) - 1) / 2)
* rectangle_width
* rectangle_spacing
).align_to(alignment_point, DOWN)
self.play(*[Write(r) for r in rectangles])
def animate_at(a, b, duration):
"""Animate that we're looking at the positions a and b."""
self.play(
*[
r.animate.set_color(WHITE if i not in (a, b) else YELLOW)
for i, r in enumerate(rectangles)
],
run_time=duration,
)
def animate_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 slower
speed_slow = 0.6
speed_fast = 0.07
for i in range(n):
speed = speed_slow if i == 0 else speed_fast
swapped = False
for j in range(n - i - 1):
animate_at(j, j + 1, speed)
if values[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, stop
if not swapped:
break
self.play(*[FadeOut(r) for r in rectangles])
Search
Create an animation of binary searching a random sorted sequence.
The Arrow
object is very useful for creating the position indicators.
from manim import *
class ArrowExample(Scene):
def construct(self):
a1 = Arrow(start=UP, end=DOWN).shift(LEFT * 2)
a2 = Arrow(start=DOWN, end=UP).shift(RIGHT * 2)
self.play(Write(a1), Write(a2))
Author's Solution
from manim import *
from random import *
class Search(Scene):
def construct(self):
seed(0xDEADBEEF1) # prettier input
n = 10
value_min, value_max = 1, n
values = sorted([randint(value_min, value_max) for _ in range(n)])
square_side_length = 0.75
square_spacing = 1.3
squares = [Square(side_length=square_side_length) for v in values]
numbers = [Tex(f"${v}$") for v in values]
# move rectangles such that they are centered
for i, rect in enumerate(squares):
rect.shift(
RIGHT
* (i - (len(squares) - 1) / 2)
* square_side_length
* square_spacing
)
# label positions
for i, number in enumerate(numbers):
number.move_to(squares[i])
pointer_length = 0.4
l_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) for s in squares], *[Write(n) for n in numbers])
# print the number we're looking for
target = 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) - 1
def color_in_range(objects, color, range):
"""Return the animation of coloring the objects in the sequence."""
return [
o.animate.set_color(color) for i, o in enumerate(objects) if i in range
]
while lo < hi:
avg = (lo + hi) // 2
current_arrow = (
Arrow(start=DOWN * pointer_length, end=UP)
.next_to(squares[avg], DOWN)
.set_color(ORANGE)
)
self.play(Write(current_arrow))
if values[avg] < target:
# move left pointer
self.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 + 1
elif values[avg] >= target:
# move right pointer
self.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 found
if values[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),
)
break
self.play(*[FadeOut(r) for r in numbers + squares + [r_pointer, text]])