AnimationGroup, LaggedStart and Succession#

[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

Theory#

Let’s analyze the following animation:

[2]:
class Example(Scene):
    def construct(self):
        text = Text("Animation").scale(2)
        self.play(Write(text),run_time=4)
        self.wait()

%manim $_RV

We see that each letter is displayed on the screen with a lag from left to right, if we wanted all the letters to be displayed at the same time then we would have to add the parameter lag_ratio=0.

[3]:
class Example(Scene):
    def construct(self):
        text = Text("Animation").scale(2)
        self.play(Write(text,lag_ratio=0),run_time=4)
        self.wait()

%manim $_RV

AnimationGroup#

How would you make the following animation? (I’m using the Indicate animation)

Surely you would start by doing the following:

[5]:
class Example(Scene):
    def construct(self):
        dots = VGroup(*[Dot() for _ in range(20)])\
            .scale(2).arrange(RIGHT,buff=0.3)

        self.add(dots)
        self.play(*[
                Indicate(d, scale_factor=2)
                for d in dots
            ],
            run_time=4
        )
        self.wait()

%manim $_RV

To add the lag between the animations we have to group the animations, for that there is AnimationGroup, it is like a Group and VGroup but for animations.

Let’s see what happens when we group all the animations into an AnimationGroup:

[6]:
class Example(Scene):
    def construct(self):
        dots = VGroup(*[Dot() for _ in range(20)])\
            .scale(2).arrange(RIGHT,buff=0.3)

        self.add(dots)
        self.play(
            AnimationGroup(*[
                Indicate(d, scale_factor=2)
                for d in dots
            ]),
            run_time=4
        )
        self.wait()

%manim $_RV

The same thing still happens, now if we add a \(\,{\tt lag\_ratio} > 0\,\) then this changes:

[8]:
class Example(Scene):
    def construct(self):
        dots = VGroup(*[Dot() for _ in range(20)])\
            .scale(2).arrange(RIGHT,buff=0.3)

        self.add(dots)
        self.play(
            AnimationGroup(*[
                    Indicate(d, scale_factor=2)
                    for d in dots
                ],
                lag_ratio=0.1 # <- lag
            ),
        )
        self.wait()

%manim $_RV

We see that now it looks a little more like the target.

The lag_ratio indicates the ratio that determines the pause between animations.

  • lag_ratio = 0: All animations start at the same time:

  • lag_ratio = 0.1: The next animation starts when the previous one goes to 10%.

  • lag_ratio = 0.5: The next animation starts when the previous one goes to 50%.

  • lag_ratio = 1: The next animation starts just as the previous one ends.

  • lag_ratio = 2: The next animation starts when the previous one ends and there is a pause with the duration of the previous animation.

  • lag_ratio = 3: The next animation starts when the previous one ends and there is a pause with twice the duration of the previous animation.

Play around with the lag_ratio values to understand how it works, just remember that lag_ratio must be greater than zero, and values between 0 and 1 are generally used, although values greater than zero are also accepted.

Graphic explanation:

One detail to note is when we define the run_time within each AnimationGroup animation, and when we define the run_time within the AnimationGroup itself.

This is best explained with a graphical representation:

If we define the run_time in the animations then the AnimationGroup's run_time will be adjusted to the sum of the animations run_times (calculated with the lag_ratio).

[9]:
class Example(Scene):
    def construct(self):
        dots = VGroup(*[Dot() for _ in range(20)])\
            .scale(2).arrange(RIGHT,buff=0.3)

        self.add(dots)
        self.play(
            AnimationGroup(*[
                    Indicate(d, scale_factor=2, run_time=3)
                    for d in dots
                ],
                lag_ratio=0.1 # <- lag
            ),
        )
        self.wait()

%manim $_RV

If we define the run_time in the AnimationGroup then the run_time of the animations will be calculated using the AnimationGroup as reference.

[10]:
class Example(Scene):
    def construct(self):
        dots = VGroup(*[Dot() for _ in range(20)])\
            .scale(2).arrange(RIGHT,buff=0.3)

        self.add(dots)
        self.play(
            AnimationGroup(*[
                    Indicate(d, scale_factor=2)
                    for d in dots
                ],
                lag_ratio=0.1, # <- lag
                run_time=3
            ),
        )
        self.wait()

%manim $_RV

Pauses#

Using this concept we can make one animation start in the middle of another using AnimationGroup concatenations.

Let’s analyze the following animation:

[11]:
class Example(Scene):
    def construct(self):
        nl = NumberLine(x_range=[0,1,0.5],length=8)
        d = Dot(nl.n2p(0))

        self.add(nl)
        self.play(
            MoveAlongPath(d, nl, rate_func=linear, run_time=5)
        )
        self.wait()

%manim $_RV

The objective is that when the point reaches the center of the line, then we are going to write a text.

Try to use the knowledge that you have acquired to achieve it, I will leave you the solution but I recommend that you try it.

SOLUTION 1: Surely you did#

[12]:
class Example(Scene):
    def construct(self):
        nl = NumberLine(x_range=[0,1,0.5],length=8)
        d = Dot(nl.n2p(0))
        t = Text("Text").scale(2).next_to(nl,DOWN)

        self.add(nl)
        self.play(
            AnimationGroup(
                MoveAlongPath(d, nl, rate_func=linear, run_time=5),
                Write(t),
                lag_ratio=0.5
            )
        )
        self.wait()

%manim $_RV

SOLUTION 2#

[13]:
class Example(Scene):
    def construct(self):
        nl = NumberLine(x_range=[0,1,0.5],length=8)
        d = Dot(nl.n2p(0))
        t = Text("Text").scale(2).next_to(nl,DOWN)

        self.add(nl)
        self.play(
            MoveAlongPath(d, nl, rate_func=linear, run_time=5),
            AnimationGroup(
                Animation(Mobject(), run_time=2.5),
                Write(t),
                lag_ratio=1
            )
        )
        self.wait()

%manim $_RV

We can create a custom animation to summarize the code from Solution 2 in the previous exercise.

[14]:
class BeginWithPause(AnimationGroup):
    def __init__(self, pause, *anims, **kwargs):
        super().__init__(
            Animation(Mobject(),run_time=pause),
            *anims,
            lag_ratio=1,
            **kwargs
        )
[15]:
class Example(Scene):
    def construct(self):
        nl = NumberLine(x_range=[0,1,0.5],length=8)
        d = Dot(nl.n2p(0))
        t = Text("Text").scale(2).next_to(nl,DOWN)

        self.add(nl)
        self.play(
            MoveAlongPath(d, nl, rate_func=linear, run_time=5),
            BeginWithPause(2.5, Write(t))
        )
        self.wait()

%manim $_RV

LaggedStart & LaggedStartMap#

LaggedStart is the same as AnimationGroup but has a default lag_ratio=0.05.

A shorter way to define LaggedStart is using LaggedStartMap, it is useful to use it only when there are not many args or kwargs to define:

[16]:
class Example(Scene):
    def construct(self):
        dots = VGroup(*[Dot() for _ in range(20)])\
            .scale(2).arrange(RIGHT,buff=0.3)

        self.add(dots)
        self.play(
            LaggedStart(*[Indicate(d) for d in dots]),
            run_time=4, rate_func=linear
        )
        # OR
        self.play(
            LaggedStartMap(Indicate, dots),
            run_time=4, rate_func=linear
        )
        self.wait()

%manim $_RV

In case the LaggedStartMap animation requires more args then we can do it in two different ways:

[17]:
class Example(Scene):
    def construct(self):
        dots = VGroup(*[Dot() for _ in range(20)])\
            .scale(2).arrange(RIGHT,buff=0.3)

        self.add(dots)
        self.play(
            #                        scale, color
            LaggedStart(*[Indicate(d,    2, RED) for d in dots]),
            run_time=4, rate_func=linear
        )
        # OR
        self.play(
            #                                           scale, color
            LaggedStartMap(Indicate, dots, lambda m: (m, 2,    TEAL)),
            run_time=4, rate_func=linear
        )
        # OR
        self.play(
            #                                    scale, color
            LaggedStartMap(lambda m: Indicate(m, 2    , PURPLE), dots),
            run_time=4, rate_func=linear
        )
        self.wait()

%manim $_RV

lag_ration with .animate attr.#

In case the Mobject (or Group) has submobjects inside then the lag_ratio will be executed.

[18]:
class Example(Scene):
    def construct(self):
        dots = VMobject().add(*[Dot() for _ in range(20)])\
            .scale(2).arrange(RIGHT,buff=0.3)

        self.add(dots)
        # self.wait()
        self.play(
            dots.animate(run_time=3,lag_ratio=1).shift(DOWN)
        )
        self.play(
            dots.animate(run_time=3,lag_ratio=0.05).shift(DOWN)
        )
        self.wait()

%manim $_RV

Succession#

AnimationGroup have a limitation, let’s analyze the following code:

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

        self.add(sq)
        self.wait()
        self.play(
            AnimationGroup(
                Transform(sq, Circle().scale(2)),
                Transform(sq, Triangle().scale(2)),
                Transform(sq, Star().scale(2)),
                Transform(sq, Ellipse().scale(2)),
                lag_ratio=1,
                run_time=6
            )
        )
        self.wait()

%manim $_RV

We see that the transformations do not happen, because each animation must have different objects.

In case we need to do the above we can use Succession (it already has lag_ratio=1 by default):

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

        self.add(sq)
        self.wait()
        self.play(
            Succession(
                Transform(sq, Circle().scale(2)),
                Transform(sq, Triangle().scale(2)),
                Transform(sq, Star().scale(2)),
                Transform(sq, Ellipse().scale(2)),
                run_time=6
            )
        )
        self.wait()

%manim $_RV