Custom animations#

[1]:
from manim import *
config.media_embed = True; config.media_width = "100%"
_RV = "-v WARNING -qm --progress_bar None --disable_caching Example"
_RI = "-v WARNING -s --progress_bar None --disable_caching Example"
Manim Community v0.15.1

Structure of an animation.#

This class is a simplification and extension of this tutorial by Benjamin Hackl (a member of the ManimCE development team).

Let’s study the example of the Rotating animation to understand the skeleton of an animation:

class Rotating(Animation):
    def __init__(
        self,
        mobject: Mobject,
        axis: np.ndarray = OUT,
        radians: np.ndarray = TAU,
        about_point: np.ndarray | None = None,
        about_edge: np.ndarray | None = None,
        run_time: float = 5,
        rate_func: Callable[[float], float] = linear,
        **kwargs,
    ) -> None:
        self.axis = axis
        self.radians = radians
        self.about_point = about_point
        self.about_edge = about_edge
        super().__init__(mobject, run_time=run_time, rate_func=rate_func, **kwargs)

    def interpolate_mobject(self, alpha: float) -> None:
        self.mobject.become(self.starting_mobject)
        self.mobject.rotate(
            self.rate_func(alpha) * self.radians,
            axis=self.axis,
            about_point=self.about_point,
            about_edge=self.about_edge,
        )

Points to highlight:#

  1. To create an animation you must create a class that inherits from Animation or a subclass of Animation.

  2. Every animation is tied to an Mobject, this mobject will be assigned as an attribute to the animation self.mobject.

  3. Every animation has run_time (duration), rate_func (behavior), remover, etc. If we don’t define them then the default Animation values will be used.

  4. Let’s note that there is an attribute called starting_mobject that we haven’t defined.

  5. We are overwriting the interpolate_mobject method.

  6. We are using the rate_func method.

Explanation:#

Let’s analyze the following code:

[2]:
class Example(Scene):
    def construct(self):
        sq = Square().scale(2)
        self.play(Rotating(sq,radians=PI/2,run_time=2))
        self.wait()

%manim $_RV

When the Python interpreter reaches the play method, it will then execute a series of procedures for each animation inside of it.

The procedure is the next:

  1. An attribute called mobject is created, this is the mobject that will be manipulated, and therefore will change, throughout the animation.

  2. An attribute called starting_mobject is also created, which is a copy of the mobject at the start of the animation.

  3. The begin method that all classes that inherit from Animation have will be executed:

    • In case this mobject has an active updater, it will be suspended.

    • Finally, begin calls the interpolate method with the value 0. This method receives an alpha value (ranging from 0 to 1), and its behavior depends on other methods that will be explained below.

    • The begin method is only executed once, at the beginning of the animation.

  4. The way in which the progress of the animation is going to be indicated can be defined in two ways, with interpolate_mobject and with interpolate_submobject.

    • interpolate_mobject receives the alpha parameter and here it is indicated how each step of the animation will be. You can make use of Animation.mobject or Animation.starting_mobject, or any other value you’ve defined in or before begin.

    • interpolate_submobject is used to have greater control of each submobject of the object (in case it is a VGroup or similar), this method receives three parameters, the submob to be modified, the starting_submob, which is a copy of the submob at the beginning of the animation, and alpha.

    • REMARK: It is recommended to define the line alpha = self.rate_func(alpha) at the beginning of these methods so that the animation respects the rate_func that we define in the Scene.play method.

  5. The finish method is called which renders the last frame.

  6. The clean_up_from_scene method is called, which is mainly used to remove objects that have been created throughout the animation, this is the method that removes the object if remover is True.

Examples with interpolate_mobject.#

Rotate and change color#

[3]:
class RotateAndColor(Animation):
    def __init__(
        self,
        mobject,
        angle = TAU,
        target_color = RED,
        run_time = 3,
        rate_func = linear,
        **kwargs,
    ):
        # set attrs
        self._angle = angle
        self._init_color = mobject.get_color()
        self._target_color = target_color
        super().__init__(mobject, run_time=run_time, rate_func=rate_func, **kwargs)

    def interpolate_mobject(self, alpha):
        # don't forget this line
        alpha = self.rate_func(alpha)
        # similar to mob.restore() (see previus chapter)
        self.mobject.become(self.starting_mobject)
        # rotate
        self.mobject.rotate(alpha * self._angle)
        # set color
        self.mobject.set_color(interpolate_color(
            self._init_color, self._target_color, alpha
        ))
[4]:
class Example(Scene):
    def construct(self):
        sq = Square().scale(2)
        self.play(RotateAndColor(sq, PI, ORANGE))
        self.wait()

%manim $_RV

Highlight animation.#

Let’s create the following animation:

First without fading the object:

[5]:
class Remark(Animation):
    # define args and kwargs
    def __init__(self, mob, scale=1.3, color=RED, **kwargs):
        # set attrs
        self._scale = scale
        self._color = color
        super().__init__(mob, **kwargs)

    def begin(self):
        # create growing mob
        self.remark_mob = self.mobject.copy()
        self.remark_mob.set_color(self._color)
        # save state
        self.remark_mob.save_state()
        # add new mob to self.mobject, THIS IS IMPORTANT
        # if we don't do this, remark mob won't appear in the animation
        self.mobject.add(self.remark_mob)
        super().begin()

    def interpolate_mobject(self, alpha):
        alpha = self.rate_func(alpha)
        # restore initial state
        self.remark_mob.restore()
        # We can't do:
        # self.remark_mob.scale(self._scale * alpha)
        # because alpha goes from 0 a 1
        # try it and see what happens
        self.remark_mob.scale(interpolate(1,self._scale,alpha))

class Example(Scene):
    def construct(self):
        sq = Square().scale(2)
        self.play(Remark(sq, 1.7))
        self.play(sq.animate.shift(LEFT))
        self.wait()

%manim $_RV

We see that the animation is not as expected, on the one hand, because we have not removed the remark_mob object from the scene, we can fix this if we add the clean_up_from_scene method.

[6]:
class Remark(Remark):
    def clean_up_from_scene(self, scene):
        # remove extra mobs from self.mobject
        self.mobject.remove(self.remark_mob)
        # remove it also from screen
        scene.remove(self.remark_mob)
        # call super
        super().clean_up_from_scene(scene)

class Example(Scene):
    def construct(self):
        sq = Square().scale(2)
        self.play(Remark(sq, 1.7))
        self.play(sq.animate.shift(LEFT))
        self.wait()

%manim $_RV

And to finish, we add the fade-out to the object:

[7]:
class Remark(Remark):
    def interpolate_mobject(self, alpha):
        alpha = self.rate_func(alpha)
        self.remark_mob.restore()
        self.remark_mob.scale(interpolate(1,self._scale,alpha))
        # here is the fade-out
        self.remark_mob.fade(alpha)

class Example(Scene):
    def construct(self):
        sq = Square().scale(2)
        self.play(Remark(sq, 1.7))
        self.play(sq.animate.shift(LEFT))
        self.wait()

%manim $_RV

Example with interpolate_submobject.#

Let’s supose that we have several objects in a vgrp and we want to rotate them all about their geometric center.

The simplest way would be to do it like this (with list comprehension):

[8]:
class Example(Scene):
    def construct(self):
        vg = VGroup(Square(),Triangle(),Star())\
            .arrange(RIGHT)
        vg.set(width=config.frame_width-3)
        self.add(vg)
        self.play(*[
                Rotate(mob, 2*PI, about_point=mob.get_center_of_mass())
                for mob in vg
            ],
            run_time=4
        )
        self.wait()

%manim $_RV

But suppose we’re going to do this several times in our scene, it’s not very smart to repeat the code, so the best idea is to create an animation.

We can do it with interpolate_mobject or with interpolate_submobject, we leave as homework how it would be with interpolate_mobject, we will do it with interpolate_submobject.

[9]:
class RotateEveryMob(Animation):
    def __init__(self, vg, angle=PI/2, rate_func=linear, **kwargs):
        self._angle = angle
        super().__init__(vg, rate_func=rate_func,**kwargs)

    def begin(self):
        # In case we need to save information
        # about each submob, we can do so by
        # saving it as an attribute.
        for mob in self.mobject:
            mob._center = mob.get_center_of_mass()
        super().begin()

    def interpolate_submobject(self, sub, s_sub, alpha):
        # sub = submobject
        # s_sub = starting_submobject
        alpha = self.rate_func(alpha)
        sub.become(s_sub) # is like sub.restore()
        sub.rotate(self._angle * alpha, about_point=sub._center)

class Example(Scene):
    def construct(self):
        vg = VGroup(Square(),Triangle(),Star())\
            .arrange(RIGHT)
        vg.set(width=config.frame_width-3)
        self.add(vg)
        self.play(RotateEveryMob(vg,2*PI,run_time=4))
        self.wait()

%manim $_RV

Homework#

Adds the option for each submoject to change to a specific color from a list.

SOLUTION

class RotateEveryMob(Animation):
    def __init__(self, vg, angle=PI/2, rate_func=linear, colors=None,**kwargs):
        self._angle = angle
        if colors is not None:
            assert( len(vg) == len(colors) ) ,"len(vg) not equal to len(colors)"
        self._colors = colors
        super().__init__(vg, rate_func=rate_func,**kwargs)

    def begin(self):
        for i,mob in enumerate(self.mobject):
            mob._center = mob.get_center_of_mass()
            mob._init_color = mob.get_color()
            if self._colors is not None:
                mob._color = self._colors[i]
        super().begin()

    def interpolate_submobject(self, sub, s_sub, alpha):
        alpha = self.rate_func(alpha)
        sub.become(s_sub) # is like sub.restore()
        sub.rotate(self._angle * alpha, about_point=sub._center)
        sub.set_color(interpolate_color(
            sub._init_color, sub._color, alpha
        ))

class Example(Scene):
    def construct(self):
        vg = VGroup(Square(),Triangle(),Star())\
            .arrange(RIGHT)
        vg.set(width=config.frame_width-3)
        self.add(vg)
        self.play(RotateEveryMob(vg,2*PI,colors=[RED,PINK,ORANGE],run_time=4))
        self.wait()

%manim $_RV

Merge animations.#

Sometimes you will need to merge or modify the behavior of an animation just once, so creating a custom animation would be a waste of time.

In these cases we can do the following:

[10]:
class Example(Scene):
    def construct(self):
        sq = Square().scale(2)

        def merge_anim(mob,color=RED):
            # define anim
            anim = Write(mob, rate_func=linear) # change by there_and_back
            center = mob.get_center_of_mass()
            init_color = mob.get_color()
            # execute begin
            anim.begin()
            def update(mob, alpha):
                anim.interpolate(alpha)
                mob.rotate(PI/2*alpha,about_point=center)
                mob.set_color(interpolate_color(init_color,color,alpha))
            return update

        self.add(sq)
        self.play(UpdateFromAlphaFunc(sq, merge_anim(sq), run_time=3, rate_func=smooth))
        self.wait()

%manim $_RV

In case you want to merge several existing animations you can do it in the following way:

[11]:
class Example(Scene):
    def construct(self):
        sq = Square().scale(2)
        sq.save_state()
        sq._center = sq.get_center_of_mass()

        def merge_anim(mob,alpha):
            mob.restore()
            write_anim = Write(mob, rate_func=linear)
            write_anim.begin()
            write_anim.interpolate(alpha)

            ftc = FadeToColor(mob, RED, rate_func=there_and_back)
            ftc.begin()
            ftc.interpolate(alpha)

            rotate_anim = Rotate(mob, PI/2, about_point=mob._center,rate_func=smooth)
            rotate_anim.begin()
            rotate_anim.interpolate(alpha)

        self.add(sq)
        self.play(UpdateFromAlphaFunc(sq, merge_anim, run_time=3, rate_func=linear))
        self.wait()

%manim $_RV

Generalizing, we can define a function that does the same thing:

[12]:
def merge_anims(*anims):
    def update(mob, alpha):
        mob.restore()
        for anim_class, anim_kwargs in anims:
            an = anim_class(mob, **anim_kwargs)
            an.begin()
            an.interpolate(alpha)
    return update

It is important to note that the order in which the animations are defined matters, you must take this into account.

[13]:
class Example(Scene):
    def construct(self):
        sq = Square().scale(2)
        sq.save_state() # <- Don't forget this
        sq._center = sq.get_center_of_mass()

        merge_anim = merge_anims(
            (Write, {"rate_func": linear}),
            (FadeToColor, {"color": RED, "rate_func": there_and_back}),
            (Rotating, {"radians": PI/2, "about_point": sq._center, "rate_func": smooth}),
        )

        self.add(sq)
        self.play(UpdateFromAlphaFunc(sq, merge_anim, run_time=3, rate_func=linear))
        self.wait()

%manim $_RV

With a custom animation:

[14]:
class MergeAnims(Animation):
    def __init__(self, mob, anims, rate_func=linear, **kwargs):
        self.anims = anims
        super().__init__(mob, rate_func=rate_func, **kwargs)

    def interpolate_mobject(self, alpha):
        self.mobject.become(self.starting_mobject)
        for anim_class, anim_kwargs in self.anims:
            an = anim_class(self.mobject, **anim_kwargs)
            an.begin()
            an.interpolate(self.rate_func(alpha))
[15]:
class Example(Scene):
    def construct(self):
        sq = Square().scale(2)
        sq.save_state()
        sq._center = sq.get_center_of_mass()

        self.add(sq)
        self.play(
            MergeAnims(sq, (
                (Write, {"rate_func": linear}),
                (FadeToColor, {"color": RED, "rate_func": there_and_back}),
                (Rotating, {"radians": PI/2, "about_point": sq._center,  "rate_func": linear}),
            )),
            run_time=4
        )
        self.wait()

%manim $_RV