ValueTracker 与自定义数据

基础用法

在 JAnim 中,可以使用 ValueTracker 来变化和跟踪自定义数据的值。

例如,下面这个例子我们已经在 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()

在这个例子中,我们可以注意到 ValueTracker 的两个主要功能:

  • 通过 set_value() 方法,可以设置数据的新值,并且这个变化可以通过 .anim 创建为动画

  • 通过 get_value() 方法,可以获取数据的值

    重要

    在 Updater 中,需要使用 current() 方法来获取当前的动画状态中的 ValueTracker 实例,即通过

    tr.current().get_value()
    

    来获取当前动画中的值。

    关于 current() 的介绍,详见 current() 的使用

ValueTracker 不仅支持这种简单数值的变化与跟踪,还支持各种复杂结构,例如列表,字典,numpy 数组等:

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

在这个例子中,我们使用了一个包含多个字段的字典作为 ValueTracker 的值,并在 Updater 中根据这些字段来更新点云的位置,旋转,颜色和半径。

相比于 set_value() 需要提供完整字段,我们也可以用 update_value() 方法来只更新部分字段的值,而不影响其他字段。

注册自定义类型

如果有一个类没有定义 SupportsTracking 所需求的三大件 copy()not_changed() 以及 interpolate() , 那么我们可以通过 register_funcs() 来注册这些方法,从而让这个类可以作为 ValueTracker 的值类型。具体使用方法可参考内置类型的注册。

另外,我们可以通过 register_update_func() 来注册供 update_value() 使用的更新方法。具体使用方法可参考内置的 dict 类型的注册。

添加自定义的物件数据

ValueTracker 的特性完全基于 Cmpt_Data 组件实现, 你完全可以将其组件添加到任意物件中,从而让你的自定义物件能够灵活地跟踪额外的数据变化,并依据你地需求灵活使用

为了添加这种组件,使用 CustomData 即可,就像这样:

from janim.imports import *

class PhysicalBlock(Square):
    physic = CustomData()

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

        self.physic.set({
            'speed': ORIGIN,    # 默认静止
            'accel': ORIGIN,    # 并且没有加速度
        })

    def do_physic(self, dt: float) -> Self:
        # 根据 `speed` 与 `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):
        # 将 `do_physic` 包装为 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)

        # 实时显示物块的运动向量
        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))

        # 物块运动以及参数变更
        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)

其中的 physic = CustomData() 就是我们添加的自定义数据组件,它的用法和 ValueTracker 十分相似:

  • 通过 set() 方法设置数据的值

  • 通过 get() 方法获取数据的值

  • 通过 update() 方法更新数据的部分字段

可以像这样完善其类型注解:

from typing import TypedDict

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

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

    ...

备注

类型注解中的 Self 是为了让组件的 .r 正常运作


高级用法

除了基本的数值和结构, ValueTracker 还支持沿用组件类型,以及注册自定义类型的处理。

备注

说实话,高级用法的应用场景非常罕见,除非你在开发新的组件或者需要跟踪非常复杂的数据,否则大多数情况下使用基础用法就足够了。

因此以下两个小标题 沿用组件类型注册自定义类型 的内容仅作简单介绍。

沿用组件类型

首先我们需要知道,所有 Component 的子类都可以直接作为 ValueTracker 的值类型,例如:

虽然说我们可以将组件类型 Cmpt_Points 作为值类型直接使用,但是对于实际来讲,还是直接操作 Points 物件或者含义更具体的物件,并在需要时 current() 会更方便。