Updater Usage

The Updater family of animation classes is a powerful feature in JAnim, including:

We will introduce them one by one, along with several important features.

Once you understand Updaters, you can graduate from the basic tutorials!

Warning

The Updater series of animation classes in JAnim differs significantly in concept from updater in Manim. Applying Manim concepts may lead to misunderstanding.

Overview

When using built-in animations, we can only play a specific animation effect. With Updaters, we can use code to control the state of items more flexibly on every frame.

To understand the differences between these Updaters, we first need to focus on what state the animation is based on:

The descriptions above may seem unfamiliar for now. We will introduce each of them in more detail, so don’t worry.

However, you can already see from the descriptions above that DataUpdater and GroupUpdater belong to the same category, while StepUpdater and GroupStepUpdater belong to another.

This allows you to simplify the task from understanding 5 types of Updaters to understanding 3, which is what we will discuss next.

Using DataUpdater and GroupUpdater

Let’s start with DataUpdater. It is the most basic and widely applicable type of Updater, and many built-in JAnim animations are implemented using it.

It modifies an item based on its state at the starting moment, using time as a parameter:

square = Square()

self.play(
    DataUpdater(
        square,
        lambda data, p: data.points.rotate(p.alpha * PI)
    ),
    duration=3
)

As mentioned earlier, the core function of an Updater is to “control the state of an item on every frame using code”. Here, data.points.rotate(p.alpha * PI) is the code we use to control the item, causing the vertices of the square to rotate counterclockwise.

We need to understand what lambda data, p: data.points.rotate(p.alpha * PI) means and how it makes the item rotate:

  • Here, data and p represent the “initial state of the item” and the “current time information”, respectively. These two arguments are provided by JAnim.

  • What comes after the colon : is the part we need to implement. Here, data.points.rotate(p.alpha * PI) means “rotate by p.alpha * PI degrees based on the initial state”.

Note

Here, p.alpha represents the animation progress, gradually increasing from 0 to 1 as the animation proceeds. Other commonly used attributes include:

  • p.global_t represents the current global time.

  • p.elapsed represents how long the animation has lasted up to the current moment. It is shorthand for p.global_t - p.at.

Putting the code together, we can interpret it as follows:

The square rotates from its initial state data according to the animation progress p.alpha.

The further the animation progresses, the greater the rotation, resulting in a gradual rotation effect.

Hint

In fact, the example above is exactly how the built-in Rotate and Rotating animations are implemented.


By design, DataUpdater is intended to modify the state of a single item. Even if you pass root_only=False, it only applies the same effect to each descendant item individually, rather than treating them as a whole.

If we need to animate a group of items as a whole, we can use GroupUpdater:

squares = Square() * 2  # Roughly equivalent to squares = Group(Square(), Square())
squares.points.arrange()

self.play(
    GroupUpdater(
        squares,
        lambda group, p: group.points.rotate(p.alpha * PI)
    ),
    duration=3
)

As you can see, GroupUpdater is used in much the same way as DataUpdater. The only difference is that we rotate squares as a whole.

Warning

In principle, functions passed to Updater such as DataUpdater and GroupUpdater should not produce “side effects”, meaning they should only change the state of data and avoid affecting other variables outside the function.

Finally, here is a comparison between DataUpdater with root_only=False and GroupUpdater.

Click to expand

The former applies the rotation effect independently to each descendant item, while the latter applies it to the group as a whole.

squares1 = Square() * 2
squares1.points.arrange()

squares2 = squares1.copy()

group = Group(
    Text('DataUpdater'), Text('GroupUpdater'),
    squares1, squares2
).show()
group.points.arrange_in_grid(buff=LARGE_BUFF)

self.play(
    DataUpdater(
        squares1,
        lambda data, p: data.points.rotate(p.alpha * PI),
        root_only=False
    ),
    GroupUpdater(
        squares2,
        lambda data, p: data.points.rotate(p.alpha * PI)
    ),
    duration=4
)

Tip

When the same effect can be achieved (such as translation rather than rotation), DataUpdater will perform better than GroupUpdater.

Some Useful Updater Operations

We have just introduced DataUpdater and GroupUpdater. Before introducing the remaining Updaters, let’s first learn some useful operations related to Updaters.

The Function of current()

For functions passed to Updater, if you need to access the current state of other items that are animating during the animation process, you can add .current() after the corresponding item to get it.

Warning

If current() is not added, you will only get the final state of the corresponding item in the construct function, not the state during the animation process.

ArrowPointingExample
dot1 = Dot(LEFT * 3)
dot2 = Dot()

arrow = Arrow(dot1, dot2, color=YELLOW)

self.show(dot1, dot2, arrow)
self.play(
    dot2.update.points.rotate(TAU, about_point=RIGHT * 2),
    GroupUpdater(
        arrow,
        lambda data, p:
            data.set_start_and_end(
                dot1.points.box.center,
                dot2.current().points.box.center
            )
    ),
    duration=4
)

Hint

dot2.update.points.rotate(TAU, about_point=RIGHT * 2) is equivalent to

GroupUpdater(
    dot2,
    lambda group, p: group.points.rotate(TAU * p.alpha, about_point=RIGHT * 2)
)

This is a simplified way of writing, but not all methods can be simplified this way. You can try it out yourself.

In this example, we first make dot2 move around a circle.

Then, in the Updater function of arrow, using .current() allows us to get the current position of dot2, so that the arrow always points to dot2.

Animation Combination

JAnim’s various Updater are not isolated. Not only can you use .current() to know the current animation state of other items, but you can also stack multiple Updater on one item, applying animation effects sequentially.

In the following example, we add a new Updater every two seconds to demonstrate the effect of “animation combination”:

CombineUpdatersExample
square = Square()
square.points.to_border(LEFT)

# Here, a new Updater is accumulated for every `play`
# to show the effect of animation combination

self.play(
    square.anim.points.to_border(RIGHT),
    duration=2
)

###############################

square.points.to_border(LEFT)
self.play(
    square.anim.points.to_border(RIGHT),
    DataUpdater(
        square,
        lambda data, p: data.points.shift(UP * math.sin(p.alpha * 4 * PI)),
        become_at_end=False
    ),
    duration=2
)

###############################

square.points.to_border(LEFT)
self.play(
    square.anim.points.to_border(RIGHT),
    DataUpdater(
        square,
        lambda data, p: data.points.shift(UP * math.sin(p.alpha * 4 * PI)),
        become_at_end=False
    ),
    square.update(become_at_end=False).color.set(BLUE).r.points.rotate(-TAU),
    duration=2
)

Tip

You can pass become_at_end=False to Updater to make the item return to its initial state after the animation.

But .anim does not have this parameter, so here we have square.points.to_border(LEFT) each time.

Warning

Animations created by .anim are overriding. When participating in “animation combination”, they should be placed at the beginning.

Here is another example of “animation combination”:

RotatingPieExample
pie = Group(*[
    Sector(start_angle=i * TAU / 4, angle=TAU / 4, radius=1.5, color=color, fill_alpha=1, stroke_alpha=0)
        .points.shift(rotate_vector(UR * 0.05, i * TAU / 4))
        .r
    for i, color in enumerate([RED, PURPLE, MAROON, GOLD])
])

self.play(
    GroupUpdater(
        pie,
        lambda data, p: data.points.rotate(p.alpha * TAU, about_point=ORIGIN),
        duration=5
    ),
    DataUpdater(
        pie[0],
        lambda data, p: data.points.shift(normalize(data.mark.get()) * p.alpha),
        rate_func=there_and_back,
        become_at_end=False,
        at=2,
        duration=2
    )
)

The Function of duration=FOREVER

We can use duration=FOREVER to create a continuously running Updater , for example:

square = Square().show()

self.forward()

self.prepare(
    DataUpdater(
        square,
        lambda data, p: data.points.rotate(p.elapsed * 60 * DEGREES),
        duration=FOREVER
    )
)

self.prepare(
    DataUpdater(
        square,
        lambda data, p: data.points.set_x(2 * math.sin(p.alpha * TAU)),
        become_at_end=False
    ),
    at=2,
)

self.forward(5)

Using StepUpdater and GroupStepUpdater

The relationship between StepUpdater and GroupStepUpdater is similar to that between DataUpdater and GroupUpdater: one operates on an individual item, while the other operates on an entire group of items as a whole.

Let’s first introduce StepUpdater. It updates an item step by step and is suitable for scenarios “where the next state needs to be updated based on the previous state”, such as physics simulations or numerical demonstrations of differential equations.

Note

We call each state update performed by StepUpdater a “step”. When constructing it, the step parameter specifies how many updates are performed per second, independently of the frame rate.

The following is the simplest example (but also the least necessary use of StepUpdater):

NumberPlane(faded_line_ratio=1).show()

circle = Circle(0.5, color=YELLOW, fill_alpha=0.6).show()

self.forward()
self.play(
    StepUpdater(
        circle,
        lambda data, p: data.points.shift(RIGHT / 50)
    ),
    duration=2
)
self.forward()

In this example, the function of StepUpdater moves the circle 1/50 unit to the right each time. Since StepUpdater executes 50 times per second by default, after two seconds the circle will have moved a total of 2 units to the right.

Note

If you have experience using Manim, its updater is more similar to the logic of StepUpdater.

Below is a more complex example. We combine CustomData to attach two physical properties, “speed” and “acceleration”, to the item. This example demonstrates the dynamic changes of items that depend on these properties.

Click to expand

class PhysicalBlock(Square):
    physic = CustomData()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.physic.set({
            'speed': ORIGIN,    # stationary by default
            'accel': ORIGIN,    # and has no acceleration
        })

    def do_physic(self, dt: float) -> Self:
        # Update item position based on `speed` and `accel`
        value = self.physic.get()

        avg_speed = value['speed'] + 0.5 * value['accel'] * dt
        shift = avg_speed * dt

        self.physic.update({ 'speed': value['speed'] + value['accel'] * dt })
        self.points.shift(shift)

        return self

    def do_physic_updater(self):
        # Wrap `do_physic` as an Updater
        return StepUpdater(self, lambda data, p: data.do_physic(p.dt))


class UpdatingPhysicalBlock(Timeline):
    def construct(self):
        block = PhysicalBlock()
        block.points.to_border(DL)

        # Display block's motion vectors in real time
        def vectors_updater(p):
            cur = block.current()
            pos = cur.points.box.center
            value = cur.physic.get()

            vec_speed = Vector(value['speed'] * 0.5, color=BLUE)
            vec_speed.points.shift(pos)
            vec_accel = Vector(value['accel'] * 0.5, color=RED)
            vec_accel.points.shift(pos)

            return Group(vec_speed, vec_accel)

        self.prepare(ItemUpdater(None, vectors_updater, duration=FOREVER))

        # Block motion and parameter changes
        self.play(block.do_physic_updater())
        block.physic.set({ 'speed': np.array([4, 6, 0]), 'accel': DOWN * 4 })
        self.play(block.do_physic_updater(), duration=2)
        block.physic.update({ 'accel': LEFT * 6 })
        self.play(block.do_physic_updater(), duration=2)

For detailed information about CustomData and this example, please refer to the tutorial page Add Custom Item Data. We will skip it for now.


As for GroupStepUpdater, which belongs to the same family as StepUpdater, its functionality is the same. The only difference is whether the group of items is treated as a whole, making it convenient for scenarios such as handling collisions between a large number of balls:

BallsCollisionExample
from janim.imports import *

class Ball(Dot):
    speed = CustomData()

    def __init__(self, radius: float):
        super().__init__(radius=radius, color=BLUE)
        self.speed.set(ORIGIN)


class BallsCollisionExample(Timeline):
    def construct(self):
        # Configuration
        left = -4
        right = 4
        bottom = -3
        top = 3

        radius = 0.25
        ball_count = 25

        # Container boundary
        Polygon([left, top, 0], [left, bottom, 0], [right, bottom, 0], [right, top, 0], fill_alpha=0.2).show()
        # Balls inside the container
        balls = Ball(radius) * ball_count

        # Generate non-overlapping initial positions
        positions = []
        rng = np.random.default_rng(1234)
        for ball in balls:
            # Initial position
            ...

            # Initial velocity
            ...

        def updater(group: Group[Ball], p) -> None:
            dt = p.dt

            # 1. Move according to velocity
            for ball in group:
                ball.points.shift(ball.speed.get() * dt)

            # 2. Collision with container boundaries
            ...

            # 3. Perfectly elastic collisions between balls
            ...

        self.play(
            GroupStepUpdater(balls, updater),
            duration=4,
        )

        ball_follow = balls[6]

        self.forward(0.5)
        self.play(
            self.camera.anim.points.scale(0.5).move_to(ball_follow),
            ball_follow.anim.set(color=YELLOW),
        )
        self.forward(0.5)
        self.play(
            GroupStepUpdater(balls, updater),
            Follow(self.camera, ball_follow, ORIGIN),
            duration=6,
        )

Click to view the complete code

from janim.imports import *

class Ball(Dot):
    speed = CustomData()

    def __init__(self, radius: float):
        super().__init__(radius=radius, color=BLUE)
        self.speed.set(ORIGIN)

class BallsCollisionExample(Timeline):
    def construct(self):
        # Configuration
        left = -4
        right = 4
        bottom = -3
        top = 3

        radius = 0.25
        ball_count = 25

        # Container boundary
        Polygon([left, top, 0], [left, bottom, 0], [right, bottom, 0], [right, top, 0], fill_alpha=0.2).show()
        # Balls inside the container
        balls = Ball(radius) * ball_count

        # Generate non-overlapping initial positions
        positions = []
        rng = np.random.default_rng(1234)
        for ball in balls:
            # Initial position
            while True:
                pos = np.array([
                    rng.uniform(left + radius, right - radius),
                    rng.uniform(bottom + radius, top - radius),
                    0,
                ])
                if all(np.linalg.norm(pos - other) >= 2 * radius for other in positions):
                    break
            ball.points.move_to(pos)
            positions.append(pos)

            # Initial velocity
            ball.speed.set(
                np.array([
                    rng.uniform(-3, 3),
                    rng.uniform(-3, 3),
                    0,
                ])
            )

        def updater(group: Group[Ball], p) -> None:
            dt = p.dt

            # 1. Move according to velocity
            for ball in group:
                ball.points.shift(ball.speed.get() * dt)

            # 2. Collision with container boundaries
            for ball in group:
                pos = ball.points.box.center
                speed = ball.speed.get().copy()

                if pos[0] - radius < left:
                    ball.points.set_x(left + radius)
                    speed[0] = abs(speed[0])

                elif pos[0] + radius > right:
                    ball.points.set_x(right - radius)
                    speed[0] = -abs(speed[0])

                if pos[1] - radius < bottom:
                    ball.points.set_y(bottom + radius)
                    speed[1] = abs(speed[1])

                elif pos[1] + radius > top:
                    ball.points.set_y(top - radius)
                    speed[1] = -abs(speed[1])

                ball.speed.set(speed)

            # 3. Perfectly elastic collisions between balls
            for i in range(len(group)):
                for j in range(i + 1, len(group)):
                    ball1 = group[i]
                    ball2 = group[j]

                    p1 = ball1.points.box.center
                    p2 = ball2.points.box.center

                    delta = p2 - p1
                    dist = np.linalg.norm(delta)

                    min_dist = 2 * radius

                    if dist >= min_dist:
                        continue

                    # Determine the collision direction when the balls overlap
                    if dist < 1e-8:
                        normal = np.array([1.0, 0.0, 0.0])
                        dist = 0.0
                    else:
                        normal = delta / dist

                    v1 = ball1.speed.get()
                    v2 = ball2.speed.get()

                    # Relative velocity
                    relative_velocity = v2 - v1
                    velocity_along_normal = np.dot(relative_velocity, normal)
                    # Only handle collisions when the balls are approaching each other
                    if velocity_along_normal < 0:
                        # Perfectly elastic collision between balls of equal mass
                        impulse = velocity_along_normal * normal
                        ball1.speed.set(v1 + impulse)
                        ball2.speed.set(v2 - impulse)

                    # Resolve the overlap between the two balls
                    overlap = min_dist - dist
                    if overlap > 0:
                        correction = normal * (overlap / 2)
                        ball1.points.shift(-correction)
                        ball2.points.shift(correction)

        self.play(
            GroupStepUpdater(balls, updater),
            duration=4,
        )

        ball_follow = balls[6]

        self.forward(0.5)
        self.play(
            self.camera.anim.points.scale(0.5).move_to(ball_follow),
            ball_follow.anim.set(color=YELLOW),
        )
        self.forward(0.5)
        self.play(
            GroupStepUpdater(balls, updater),
            Follow(self.camera, ball_follow, ORIGIN),
            duration=6,
        )

For a detailed introduction to CustomData, please refer to the tutorial page Add Custom Item Data.

Using ItemUpdater

ItemUpdater differs significantly from the two types of Updater introduced earlier. Functions passed to the previous two types of Updater receive two parameters, data, p or group, p, whereas ItemUpdater provides only one parameter, p, and directly renders the item returned by the function onto the screen.

The use case of ItemUpdater is to dynamically create items during animation for display, such as text with continuously changing values:

tr = ValueTracker(0)
txt = Text('0.00', font_size=40).show()

self.forward()
self.play(
    Succession(
        tr.anim.set_value(4),
        tr.anim.set_value(2.5),
        tr.anim.set_value(10)
    ),
    ItemUpdater(
        txt,
        lambda p: Text(f'{tr.current().get_value():.2f}', font_size=40),
        duration=3
    )
)
self.forward()
UpdaterExample
square = Square(fill_color=BLUE_E, fill_alpha=1).show()
brace = Brace(square, UP).show()

def text_updater(p: UpdaterParams):
    cmpt = brace.current().points
    return cmpt.create_text(f'Width = {cmpt.brace_length:.2f}')

self.prepare(
    DataUpdater(
        brace,
        lambda data, p: data.points.match(square.current())
    ),
    ItemUpdater(None, text_updater),
    duration=10
)
self.forward()
self.play(square.anim.points.scale(2))
self.play(square.anim.points.scale(0.5))
self.play(square.anim.points.set_width(5, stretch=True))

w0 = square.points.box.width

self.play(
    DataUpdater(
        square,
        lambda data, p: data.points.set_width(
            w0 + 0.5 * w0 * math.sin(p.alpha * p.range.duration)
        )
    ),
    duration=5
)
self.forward()

See also:

Brace

Note

In principle, since ItemUpdater does not depend on any state and is only used to display the item returned by the function, the item passed as the first argument to ItemUpdater is actually unrelated to the animation process.

What ItemUpdater does, by default, is:

  • At the start of the animation, hide the passed item

  • During the animation, render the item returned by the function

  • After the animation ends, show the passed item and call the become() method to change the passed item to the state at the last moment of the animation

So ItemUpdater can be used without passing an item, passing None is acceptable.