Skip to article frontmatterSkip to article content
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 share kind 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 different names.
    • Line 31 calls Pet.__str__ implicitly to return a string containing pets’ specific kind in addition to its name.

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 and WHITE imported from manim.
  • It is then placed to to the scene using self.add inherited from Scene.

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