Updater Usage

The Updater series of animation classes is a powerful feature in JAnim, including DataUpdater GroupUpdater ItemUpdater StepUpdater. We will introduce them one by one and cover several important features.

After learning DataUpdater, GroupUpdater, and ItemUpdater, 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.

Usage of DataUpdater

DataUpdater is the most basic and widely applicable type of Updater. Its function is to modify items using time as a parameter:

square = Square()

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

Here, we pass a function to DataUpdater. This function rotates the vertices of the square to create an animation effect.

So how does this function work? Formally speaking, the format of DataUpdater is:

DataUpdater(<item>, lambda <initial_state>, <time_info>: <change initial state based on time info>)

Tip

You can choose to use either lambda functions or def functions depending on the specific use case.

For simple functionality, using lambda functions is more convenient.

Time information includes p.alpha representing the current animation progress, p.global_t representing the current global time, etc.

So, for lambda data, p: data.points.rotate(p.alpha * PI), the function’s purpose is:

Rotate the square from its initial animation state according to the animation progress. The more the animation progresses, the greater the rotation amount will be

Thus creating a rotating animation effect for the rectangle item.

Note

In fact, the above example is exactly how Rotate and Rotating animations are implemented

Hint

You can use p.elapsed to know how long the animation has lasted until the current moment. This is shorthand for p.global_t - p.range.at.

Note that if the item passed to DataUpdater has child items, by default root_only=True only operates on the root item itself. If root_only=False is passed, the Updater effect will be applied to all its descendant items separately, but they will not be operated on as a whole.

To operate on an item and its descendant items as a whole, we need to introduce GroupUpdater, which we will cover in the next section.

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.

Usage of GroupUpdater

GroupUpdater is used in the same way as DataUpdater, both modifying items using time as a parameter.

But as mentioned in the previous section about DataUpdater, GroupUpdater focuses on operating on the passed item and its descendant items as a whole, which is practical when handling operations like “overall rotation” and “overall alignment”.

The following example demonstrates the difference between using DataUpdater and GroupUpdater for rotation:

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.

Usage 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

DataUpdater(
    dot2,
    lambda data, p: data.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.

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
    )
)

Usage of ItemUpdater

ItemUpdater differs significantly from the two Updater introduced earlier. Functions passed to the previous two Updater receive two parameters data, p, but ItemUpdater only provides one parameter p, and directly renders the item returned by the function onto the frame.

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, the item passed to ItemUpdater has no relationship with 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.

Usage 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)

Usage of StepUpdater

Update items step by step, suitable for scenarios “requiring updating the next state based on the previous state”, such as physics simulations or numerical demonstrations of differential equations.

The following is the simplest example (but also the least necessary to use 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 to the right by 1/50 unit each time. Since StepUpdater executes 50 times per second by default, the circle moves 1 unit to the right per second, and after two seconds it moves 2 units to the right.

Note

Documentation needs to be improved