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:#
To create an animation you must create a class that inherits from
Animation
or a subclass ofAnimation
.Every animation is tied to an Mobject, this mobject will be assigned as an attribute to the animation
self.mobject
.Every animation has
run_time
(duration),rate_func
(behavior),remover
, etc. If we don’t define them then the defaultAnimation
values will be used.Let’s note that there is an attribute called
starting_mobject
that we haven’t defined.We are overwriting the
interpolate_mobject
method.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:
An attribute called
mobject
is created, this is the mobject that will be manipulated, and therefore will change, throughout the animation.An attribute called
starting_mobject
is also created, which is a copy of the mobject at the start of the animation.The
begin
method that all classes that inherit fromAnimation
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.
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 thealpha
parameter and here it is indicated how each step of the animation will be. You can make use ofAnimation.mobject
orAnimation.starting_mobject
, or any other value you’ve defined in or beforebegin
.interpolate_submobject
is used to have greater control of each submobject of the object (in case it is aVGroup
or similar), this method receives three parameters, the submob to be modified, thestarting_submob
, which is a copy of the submob at the beginning of the animation, andalpha
.REMARK: It is recommended to define the line
alpha = self.rate_func(alpha)
at the beginning of these methods so that the animation respects therate_func
that we define in theScene.play
method.
The
finish
method is called which renders the last frame.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 ifremover
isTrue
.
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