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.animUsing
get_value(), you can retrieve the valueImportant
In an Updater, you need to use
current()to get theValueTrackerinstance in the current animation state, i.e. viatr.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 valueUse
get()to get the data valueUse
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.