ValueTracker and Custom Data

Basic Usage

In JAnim, you can use ValueTracker to change and track custom data values.

For example, the following example was already mentioned in Usage of ItemUpdater:

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

In this example, we can note two main features of ValueTracker:

  • Using set_value(), you can set a new value, and the change can be animated with .anim

  • Using get_value(), you can retrieve the value

    Important

    In an Updater, you need to use current() to get the ValueTracker instance in the current animation state, i.e. via

    tr.current().get_value()
    

    to get the current animated value.

    For current(), see Usage of current().

ValueTracker not only supports simple numeric changes and tracking, but also complex structures such as lists, dicts, and numpy arrays:

tr = ValueTracker({
    'position': ORIGIN,
    'rotate': 0,
    'color': [1.0, 0.5, 0.0],
    'radius': 0.2
})
dots = DotCloud(
    *(
        [x, y, 0]
        for x in range(-1, 2)
        for y in range(-1, 2)
    )
).show()

def dots_updater(data: DotCloud, p=None) -> None:
    value = tr.current().get_value()
    data.points.move_to(value['position']).rotate(value['rotate'])
    data.color.set(value['color'])
    data.radius.set(value['radius'])

dots_updater(dots)

self.forward()
self.play(
    Succession(
        tr.anim.set_value({
            'position': UP * 1.5,
            'rotate': PI / 4,
            'color': [0.0, 0.5, 1.0],
            'radius': 0.5
        }),
        tr.anim.set_value({
            'position': LEFT * 1.5,
            'rotate': -PI / 4,
            'color': [1.0, 0.0, 0.5],
            'radius': 0.1
        }),
        tr.anim.set_value({
            'position': ORIGIN,
            'rotate': 0,
            'color': [1.0, 0.5, 0.0],
            'radius': 0.2
        }),
        tr.anim.update_value({
            'rotate': TAU,
            'color': [1.0, 1.0, 1.0]
        })
    ),
    DataUpdater(dots, dots_updater, duration=4)
)
self.forward()

In this example, we use a dictionary with multiple fields as the ValueTracker value, and in the Updater we update the point cloud’s position, rotation, color, and radius based on these fields.

Compared to set_value() which requires full fields, you can use update_value() to update only some fields without affecting the others.

Register Custom Types

If a class does not define the three methods required by SupportsTracking (copy(), not_changed(), and interpolate()), you can register them with register_funcs() so the class can be used as a value type for ValueTracker. See the registration of built-in types for usage.

Additionally, you can use register_update_func() to register an update method for update_value(). See the built-in dict registration for usage.

Add Custom Item Data

The features of ValueTracker are entirely based on the Cmpt_Data component. You can add this component to any item, allowing your custom item to flexibly track extra data changes and use them as needed.

To add this component, use CustomData like this:

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

The physic = CustomData() here is the custom data component we added, and its usage is very similar to ValueTracker:

  • Use set() to set the data value

  • Use get() to get the data value

  • Use update() to update parts of the data

You can refine its type annotations like this:

from typing import TypedDict

class PhysicData(TypedDict):
    speed: np.ndarray
    accel: np.ndarray

class PhysicalBlock(Square):
    physic = CustomData[Self, PhysicData]()

    ...

Note

The Self in the type annotation is to make the component’s .r work properly


Advanced Usage

Besides basic values and structures, ValueTracker also supports reusing component types and registering custom types.

Note

To be honest, advanced usage is rare; unless you are developing new components or tracking very complex data, basic usage is sufficient in most cases.

Therefore the following sections Reusing Component Types and Register Custom Types are only briefly introduced.

Reusing Component Types

First, note that all subclasses of Component can be used directly as value types for ValueTracker, for example:

Although you can use a component type like Cmpt_Points as the value type directly, in practice it is more convenient to operate on Points items or more specific items, and use current() when needed.