from __init__ import install_dependencies
await install_dependencies()
import math
from manim import *
%reload_ext divewidgets
Manim is a powerful animation engine for mathemetics developed by 3Blue1Brown. We will use manim to domenstrate how object-oriented programming uses classes and objects to create complex animation.
Creating a scene¶
How to create an animation with manim
?
In Jupyter notebook, we can use %%manim
cell magic:
%%manim -qm --progress_bar=none --disable_caching --flush_cache -v ERROR HelloWorld
class HelloWorld(Scene):
def construct(self):
self.play(Write(Text("Hello, World!")))
In the line starting with %%
:
-qm
is the alias for--quality=m
, which means the video is rendered in medium quality. Change it to-ql
(-qh
) for low (high) quality.--progress_bar=none --disable_caching --flush_cache -v ERROR
are additional configurations to turn off some features.HelloWorld
is the class to render, which is defined in the body:class HelloWorld(Scene): def construct(self): ...
How to define a class?
As a toy example, the following defines a class Pet
and its subclasses Cat
and Dog
:
%%optlite -l -h 900
class Pet:
kind = "Pet"
def __init__(self, name):
self.name = name
def make_sound(self):
pass
def __str__(self):
return f"{self.kind} {self.name}"
class Cat(Pet):
kind = "Cat"
def make_sound(self):
print("Meow")
class Dog(Pet):
kind = "Dog"
def make_sound(self):
print("Woof")
p1 = Pet("Alfie")
p2 = Dog("Bobbie")
p3, p4 = Cat("Bella"), Cat("Kelly")
print(p1, p2, p3, p4, sep=", ")
p1.make_sound()
p2.make_sound()
p3.make_sound()
- Each pet has its own
name
but they sharekind
from their class without duplicating it. - Subclasses can reuse (inherit) code from their superclass but produce different results:
- Line 28-30 calls
Pet.__init__
implicitly to create pets with differentname
s. - Line 31 calls
Pet.__str__
implicitly to return a string containing pets’ specifickind
in addition to itsname
.
- Line 28-30 calls
To properly encapsulate the attributes to allow more controlled access, some built-in decorators are commonly used:
from abc import ABC, abstractmethod
class Pet(ABC):
_kind = "Pet"
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@name.setter
def name(self, new_name):
self._name = new_name
@property
def kind(self):
return self._kind
@abstractmethod
def make_sound(self):
pass
def __str__(self):
return f"{self.kind} {self.name}"
class Cat(Pet):
_kind = "Cat"
def make_sound(self):
print("Meow")
class Dog(Pet):
_kind = "Dog"
def make_sound(self):
print("Woof")
By making Pet inherit from ABC (Abstract Base Class) and defining at least one abstract method, we ensure that Pet cannot be instantiated directly:
try:
p1 = Pet("Alfie") # This will raise an error because Pet is now abstract
except TypeError as e:
print(f"TypeError: {e}")
@abstractmethod
is used to declare methods in a class that must be implemented by any subclass that inherits from it. This defines a common interface for the subclasses, ensuring that they all provide specific implementations of certain methods.
class Fish(Pet):
_kind = "Fish"
try:
Fish("goldie")
except TypeError as e:
print(f"TypeError: {e}")
The name
attribute is equipped with both a getter (@property
) and a setter (@name.setter
), which allows for safe renaming of the pet’s name by implementing the function name(self, new_new)
.
# Create pets
p2 = Dog("Bobbie")
p3, p4 = Cat("Bella"), Cat("Kelly")
print(p2, p3, p4, sep=", ")
# Rename some pets
p2.name = "Max"
p3.name = "Rex"
print(p2, p3, p4, sep=", ")
On the other hand, the kind attribute is made read-only by providing only a getter method and omitting the setter. This design choice enforces the immutability of the kind attribute, ensuring that once a Pet instance (or its subclasses) is created, its type cannot be altered. This is crucial for maintaining the integrity of the object’s identity.
try:
p2.kind = "Cat"
except AttributeError as e:
print(f"TypeError: {e}")
Animating an Object¶
How to add objects to a Scene
?
We can create a square and add it to the scene as follows:
%%manim -qm --progress_bar=none --disable_caching --flush_cache -v ERROR BlueSquare1
class BlueSquare1(Scene):
def construct(self):
s = Square(fill_color=BLUE, color=WHITE)
self.add(s)
- The square object is create using
Square
,BLUE
andWHITE
imported frommanim
. - It is then placed to to the scene using
self.add
inherited fromScene
.
How to animate an object?
The following shows the creation of a square:
%%manim -qm --progress_bar=none --disable_caching --flush_cache -v ERROR BlueSquare2
class BlueSquare2(Scene):
def construct(self):
s = Square(color=WHITE, fill_color=BLUE, fill_opacity=0.8)
self.play(Create(s))
self.wait()
self.play
plays the animationCreate(s)
.self.wait()
creates a pause “animation”.
How to transform an object?
To scale, move, or rotate the shape:
%%manim -qm --progress_bar=none --disable_caching --flush_cache -v ERROR BlueSquare3
class BlueSquare3(Scene):
def construct(self):
s = Square(color=WHITE, fill_color=BLUE, fill_opacity=0.8)
self.play(Create(s))
self.play(s.animate.scale(1.5).rotate(PI / 4))
self.play(s.animate.move_to([-3, 0, 0]))
self.play(s.animate.move_to([3, 0, 0]))
self.play(s.animate.scale(0.5).move_to(ORIGIN).rotate(-PI / 4))
self.wait()
Animating multiple objects¶
Tessellation with regular polygons
Consider tiling a 12-by-6 plane using squares:
%%manim -qm --progress_bar=none --disable_caching --flush_cache -v ERROR SquareTiling1
class SquareTiling1(Scene):
WIDTH = 12
HEIGHT = 6
EDGE = 1
def construct(self):
plane = Rectangle(width=self.WIDTH, height=self.HEIGHT)
unit = Square(color=WHITE, fill_color=BLUE, fill_opacity=0.8).scale(
self.EDGE / 2
)
self.add(plane, unit)
The first line of squares can be animated as follows:
%%manim -qm --progress_bar=none --disable_caching --flush_cache -v ERROR SquareTiling2
class SquareTiling2(Scene):
WIDTH = 12
HEIGHT = 6
EDGE = 1
def construct(self):
plane = Rectangle(width=self.WIDTH, height=self.HEIGHT)
self.add(plane)
unit = Square(color=WHITE, fill_color=BLUE, fill_opacity=0.8).scale(
self.EDGE / 2
)
self.play(Create(unit))
self.play(
unit.animate.move_to(
[-self.WIDTH / 2 + self.EDGE / 2, self.HEIGHT / 2 - self.EDGE / 2, 0]
)
)
for i in range(1, self.WIDTH // self.EDGE):
self.play(unit.copy().animate.shift([i, 0, 0]), run_time=1 / i)
self.wait()
We can use VGroup
method to create a group of shapes.
%%manim -qm --progress_bar=none --disable_caching --flush_cache -v ERROR SquareTiling3
class SquareTiling3(Scene):
WIDTH = 12
HEIGHT = 6
EDGE = 1
def construct(self):
plane = Rectangle(width=self.WIDTH, height=self.HEIGHT)
self.add(plane)
unit = Square(color=WHITE, fill_color=BLUE, fill_opacity=0.8).scale(
self.EDGE / 2
)
self.play(Create(unit))
self.play(
unit.animate.move_to(
[-self.WIDTH / 2 + self.EDGE / 2, self.HEIGHT / 2 - self.EDGE / 2, 0]
)
)
for i in range(1, math.floor(self.WIDTH / self.EDGE)):
self.play(unit.copy().animate.shift([i * self.EDGE, 0, 0]), run_time=1 / i)
line = VGroup(
*[
unit.copy().shift([i * self.EDGE, 0, 0])
for i in range(math.floor(self.WIDTH / self.EDGE))
]
)
for i in range(1, math.floor(self.HEIGHT / self.EDGE)):
self.play(line.copy().animate.shift([0, -i * self.EDGE, 0]), run_time=1 / i)
self.wait()