dt updaters#

[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#

All the animations we’ve seen so far absolutely need to be inside the Scene.play parameter, but this isn’t always necessary.

As we said, the updaters are updated every frame, but only when the Scene.play method is executed, let’s see the following example.

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

        def update_sq(mob):
            # angle to increment
            angle = 3 * DEGREES
            # save info
            mob._angle += angle * 180 / PI
            mob.rotate(angle)

        sq.add_updater(update_sq)

        self.add(sq)
        self.wait(4)
        print("ANGLE = " + str(sq._angle) + " °")

%manim $_RV
ANGLE = 0 °

As we can see, nothing happens, and it is what we expected, we are not executing the Scene.play method.

If we want the updaters to always run we can change the Scene.always_update_mobjects attribute to True.

[3]:
class Example(Scene):
    def construct(self):
        self.always_update_mobjects = True
        sq = Square().scale(2)
        sq._angle = 0

        def update_sq(mob):
            # angle to increment
            angle = 3 * DEGREES
            # save info
            mob._angle += angle * 180 / PI
            mob.rotate(angle)

        sq.add_updater(update_sq)

        self.add(sq)
        self.wait(4)

        print("ANGLE = " + str(sq._angle) + " °")

%manim $_RV
ANGLE = 366.0 °

But this is not always convenient, since it will make the rendering slower since Manim will be aware of the change of all the Mobjects at all times, what we want is that only the selected mobjects are updated.

To make a single Mobject update all the time, add the dt parameter to the updater definition.

Remember to use --disable_caching to avoid problems:

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

        def update_sq(mob, dt): # <- Add dt
            angle = 3 * DEGREES
            mob._angle += angle * 180 / PI
            mob.rotate(angle)

        sq.add_updater(update_sq)

        self.add(sq)
        self.wait(4)
        print("ANGLE = " + str(sq._angle) + " °")

%manim $_RV
ANGLE = 366.0 °

But now we have a problem, since now the value of _angle depends on the value of the FPS.

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

        def update_sq(mob, dt): # <- Add dt
            angle = 3 * DEGREES
            mob._angle += angle * 180 / PI
            mob.rotate(angle)

        sq.add_updater(update_sq)

        self.add(sq)
        self.wait(4)
        print("ANGLE = " + str(sq._angle) + " °")

%manim -v WARNING -qm --progress_bar None --disable_caching --fps=10 Example
ANGLE = 126.0 °
[6]:
class Example(Scene):
    def construct(self):
        sq = Square().scale(2)
        sq._angle = 0

        def update_sq(mob, dt): # <- Add dt
            angle = 3 * DEGREES
            mob._angle += angle * 180 / PI
            mob.rotate(angle)

        sq.add_updater(update_sq)

        self.add(sq)
        self.wait(4)
        print("ANGLE = " + str(sq._angle) + " °")

%manim -v WARNING -qm --progress_bar None --disable_caching --fps=20 Example
ANGLE = 246.0 °

To solve this you have to use the same dt parameter, since by definition \({\tt dt} = 1 / {\rm FPS}\).

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

        def update_sq(mob, dt):
            angle = 3 * DEGREES * dt
            mob._angle += angle * 180 / PI
            mob.rotate(angle)

        sq.add_updater(update_sq)

        self.add(sq)
        self.wait(4)
        print("ANGLE = " + str(sq._angle) + " °")

%manim -v WARNING -qm --progress_bar None --disable_caching --fps=10 Example
ANGLE = 11.700000000000008 °
[8]:
class Example(Scene):
    def construct(self):
        sq = Square().scale(2)
        sq._angle = 0

        def update_sq(mob, dt):
            angle = 3 * DEGREES * dt
            mob._angle += angle * 180 / PI
            mob.rotate(angle)

        sq.add_updater(update_sq)

        self.add(sq)
        self.wait(4)
        print("ANGLE = " + str(sq._angle) + " °")

%manim -v WARNING -qm --progress_bar None --disable_caching --fps=30 Example
ANGLE = 11.900000000000015 °

We see that now the final value is more consistent, but not perfect.

The theoretical value of _angle is \(\,3° \times 4\, {\rm secs} = 12 °\).

The error is due to how the Scene class is defined, Manim’s philosophy is that the final project is rendered at 60 FPS, so when you finish your animation (and used dt updaters) it is recommended to render at 60 FPS so you don’t see lag.

The updaters run in the background along with the animations with Scene.play, so we can do things like this:

[9]:
class Example(Scene):
    def construct(self):
        ellipse = Ellipse().scale(2)
        dot = Dot(ellipse.point_from_proportion(0))
        dot.pfp = 0

        def update_dot(mob, dt):
            mob.pfp += dt * (1/4) # 1 cycle every 4 seconds
            position = ellipse.point_from_proportion(mob.pfp % 1)
            mob.move_to(position)

        dot.add_updater(update_dot)
        self.add(dot, ellipse)
        self.wait(4)
        dot.clear_updaters()
        self.wait()

%manim $_RV
[10]:
class Example(Scene):
    def construct(self):
        ellipse = Ellipse().scale(2)
        text = Text("dt updaters").scale(2).to_edge(UP)
        dot = Dot(ellipse.point_from_proportion(0))
        dot.pfp = 0

        def update_dot(mob, dt):
            mob.pfp += dt * (1/4) # 1 cycle every 4 seconds
            position = ellipse.point_from_proportion(mob.pfp % 1)
            mob.move_to(position)

        dot.add_updater(update_dot)
        self.add(dot, ellipse)
        self.play(Write(text,run_time=3)) # 3 secs
        self.wait() # 1 sec
        dot.clear_updaters()
        self.wait()

%manim $_RV

Simulations#

The dt updaters are good for simulations:

[11]:
class Example(Scene):
    def construct(self):
        THETA_MAX = 10 * DEGREES
        L = 3
        g = 15
        W = np.sqrt(g / L)
        T = 2 * PI / W

        # Line definition
        line = Line(LEFT*3,RIGHT*3)
        line.rotate(-PI/2)
        mass = Dot(color=RED)\
            .scale(4).move_to(line.get_end())
        line.add(mass)
        line.save_state()
        line._angle = T/4 # initial angle
        pivot = line.get_start()
        # roof
        roof = VGroup(*[
            Line(ORIGIN,UR).scale(0.4).copy()
            for _ in range(8)
        ]).arrange(RIGHT,buff=0.1)
        roof.add(Line(roof.get_corner(DL),roof.get_corner(DR)))
        roof.next_to(line.get_top(),UP,buff=0)

        def line_update(mob, dt):
            mob.restore()
            angle = THETA_MAX * np.sin(W * mob._angle)
            mob._angle += dt
            mob.rotate(angle, about_point=pivot)

        line.add_updater(line_update)
        self.add(line,roof)
        self.wait(10)
        line.clear_updaters()
        self.wait()

%manim $_RV

Class animations to dt updaters#

The last functionality of the updaters is to transform class animations to dt updaters, it is something really simple if you have already understood the previously explained theory.

[12]:
class Example(Scene):
    def construct(self):
        text = Text("Animation")
        anim = Write(text,run_time=3)
        turn_animation_into_updater(anim)

        self.add(text)
        self.wait(4)
        text.clear_updaters()
        self.wait()

%manim $_RV

We can even add extra updaters

[13]:
class Example(Scene):
    def construct(self):
        text = Text("Animation",color=WHITE).scale(3)
        text._color = WHITE
        text._alpha = 0
        anim = Write(text,run_time=3)
        turn_animation_into_updater(anim)
        def update_text(mob, dt):
            alpha = there_and_back(mob._alpha % 1)
            mob.set_color(
                interpolate_color(mob._color, RED, alpha)
            )
            mob._alpha += dt * 0.7
        text.add_updater(update_text)

        self.add(text)
        self.wait(7)
        text.clear_updaters()
        self.wait()

%manim $_RV

We can avoid having to define the update_text function by using anonymous functions:

[14]:
class Example(Scene):
    def construct(self):
        text = Text("Animation",color=WHITE).scale(3)
        text._color = WHITE
        text._alpha = 0
        anim = Write(text,run_time=3)
        turn_animation_into_updater(anim)
        text.add_updater(
            lambda mob, dt: mob.set_color(interpolate_color(
                mob._color, RED, there_and_back(mob.set(_alpha=dt * 0.7 + mob._alpha)._alpha % 1)
            ))
        )

        self.add(text)
        self.wait(7)
        text.clear_updaters()
        self.wait()

%manim $_RV

Unfortunately we cannot merge animations like in the previous chapter.