.. _updaters: Updater 的使用 ===================== ``Updater`` 系列的动画类是 JAnim 中一套强大的功能,包括 :class:`~.DataUpdater` :class:`~.GroupUpdater` :class:`~.ItemUpdater` :class:`~.StepUpdater`, 我们将逐一介绍,并介绍若干重要的特性。 学懂 :class:`~.DataUpdater` :class:`~.GroupUpdater` 以及 :class:`~.ItemUpdater` 后,你就可以从基础教程“毕业”了! .. warning:: JAnim 中的 ``Updater`` 系列动画类与 Manim 中的 ``updater`` 在概念上存在较大差异,若套用 Manim 中的概念可能导致理解偏差。 ``DataUpdater`` 的使用 -------------------------------------------------------- :class:`~.DataUpdater` 是最基础也是应用范围最广的一种 ``Updater``,它的作用是以时间为参数对物件进行修改: .. janim-example:: BasicDataUpdater :media: _static/tutorial/BasicDataUpdater.mp4 :hide_name: square = Square() self.play( DataUpdater( square, lambda data, p: data.points.rotate(p.alpha * PI) ), duration=3 ) 在这里,我们给 :class:`~.DataUpdater` 传入了一个函数,这个函数的作用是:将正方形的顶点进行旋转,以产生动画效果。 那么这个函数是如何起作用的呢?形式上来说,:class:`~.DataUpdater` 的格式是: .. code-block:: python DataUpdater(物件, lambda 物件初始状态, 时间信息: <根据时间信息改变初始状态>) .. tip:: 你可以根据需要选择使用 ``lambda`` 函数还是 ``def`` 函数,这取决于具体的使用情景。 在功能简单的情况下,使用 ``lambda`` 函数会更加方便。 其中时间信息有 ``p.alpha`` 表示当前动画的进度, ``p.global_t`` 表示当前的全局时刻等。 所以,对于 ``lambda data, p: data.points.rotate(p.alpha * PI)`` 而言,这个函数的作用是: **将正方形从动画的初始状态,根据动画的进度进行旋转,动画向前推进得越多,那么旋转量就会越大** 从而产生矩形物件旋转的动画效果。 .. note:: 其实上面这个示例正是 :class:`~.Rotate` 和 :class:`~.Rotating` 动画的实现方式 .. hint:: 你可以使用 ``p.elapsed`` 得知,到当前时刻动画持续了多久,这是对 ``p.global_t - p.range.at`` 的简写。 需要注意的是,如果传递给 :class:`~.DataUpdater` 的物件有子物件, 在默认情况下 ``root_only=True`` 只对根物件自身进行操作, 若传入 ``root_only=False``,则会对其所有后代物件都分别应用 ``Updater`` 的效果,但并不会将他们作为一个整体进行操作。 为了将物件及其后代物件作为一个整体进行操作,我们就需要引出 :class:`~.GroupUpdater`,我们马上在下一个小节介绍。 .. warning:: 原则上来说,传入 :class:`~.DataUpdater` 以及 :class:`~.GroupUpdater` 等 ``Updater`` 的函数不应产生“副作用”,也就是只能改变 ``data`` 的状态,应避免产生对函数之外其它变量的影响。 ``GroupUpdater`` 的使用 -------------------------------------------------------- :class:`~.GroupUpdater` 在用法上和 :class:`~.DataUpdater` 一致,都是以时间为参数对物件进行修改。 但正如在上一小节对 :class:`~.DataUpdater` 的介绍中提到的,:class:`~.GroupUpdater` 侧重于将传入物件及其后代物件作为一个整体操作, 这在处理如“整体旋转”和“整体对齐”等操作时比较实用。 以下示例展示了使用 :class:`~.DataUpdater` 和 :class:`~.GroupUpdater` 进行旋转的区别: .. janim-example:: DataUpdaterVsGroupUpdater :media: _static/tutorial/DataUpdaterVsGroupUpdater.mp4 :hide_name: 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:: 在能得到相同效果(如平移而非旋转)时, :class:`~.DataUpdater` 的性能会优于 :class:`~.GroupUpdater`。 .. _current_usage: ``current()`` 的使用 ----------------------------------- 对于传入 ``Updater`` 的函数而言,在动画过程中如果需要访问 **其它正在进行动画的物件** 的当前状态,可以在对应物件后面加上 ``.current()`` 来获取。 .. warning:: 如果不加 :meth:`~.Item.current`,只会得到 ``construct`` 函数中对应物件的最终状态,而非动画过程中的状态。 .. janim-example:: ArrowPointingExample :extract-from-example: :no-construct: :media: _static/videos/ArrowPointingExample.mp4 .. hint:: ``dot2.update.points.rotate(TAU, about_point=RIGHT * 2)`` 相当于 .. code-block:: python DataUpdater( dot2, lambda data, p: data.points.rotate(TAU * p.alpha, about_point=RIGHT * 2) ) 这是一种简化写法,但并不是所有方法都可以这样简化。 在这个示例中,我们首先将 ``dot2`` 围绕一个圆周进行运动。 然后在 ``arrow`` 的 ``Updater`` 函数中, 使用 ``.current()`` 便可以得到 ``dot2`` 当前运动到的位置,从而让箭头始终指向 ``dot2``。 动画复合 --------------------- JAnim 的各个 ``Updater`` 并非孤立,不仅可以使用 ``.current()`` 获知其它物件的当前动画状态,还可以在一个物件上 **叠加多个** ``Updater``,依次应用动画效果。 在下面这个例子中,我们每两秒加入一个新的 ``Updater``,以演示“动画复合”的作用: .. janim-example:: CombineUpdatersExample :extract-from-example: :no-construct: :media: _static/videos/CombineUpdatersExample.mp4 .. tip:: 可以给 ``Updater`` 传入 ``become_at_end=False`` 使物件在动画后回到最初的状态。 但是 ``.anim`` 没有这种参数,所以这里每次都有 ``square.points.to_border(LEFT)``。 .. warning:: ``.anim`` 所创建的动画具有覆盖性,当其参与“动画复合”时,应将其放在最开始使用。 这里另外再给出一个“动画复合”的示例: .. janim-example:: RotatingPieExample :extract-from-example: :no-construct: :media: _static/videos/RotatingPieExample.mp4 :ref: :class:`~.Sector` :func:`~.rotate_vector` .. _item_updater_usage: ``ItemUpdater`` 的使用 ------------------------------------------ :class:`~.ItemUpdater` 和前面介绍的两个 ``Updater`` 存在很大的差异,传入前面两个 ``Updater`` 的函数都会收到两个参数 ``data, p``, 但是 :class:`~.ItemUpdater` 只会提供一个参数 ``p``,并且 **将函数返回的物件直接渲染到画面上**。 :class:`~.ItemUpdater` 的使用场景是在动画过程中动态创建物件以显示,例如数值持续变化的文字: .. janim-example:: DynamicNumber :media: _static/tutorial/DynamicNumber.mp4 :hide_name: 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() .. janim-example:: UpdaterExample :extract-from-example: :no-construct: :media: _static/videos/UpdaterExample.mp4 :ref: :class:`~.Brace` .. note:: 从原理上来讲,传入 :class:`~.ItemUpdater` 的物件与动画过程其实没有任何关系。 :class:`~.ItemUpdater` 所干的,在默认情况下其实就是: - 在动画开始时,把传入的物件隐藏 - 在动画过程中,渲染函数所返回的物件 - 在动画结束后,把传入的物件显示,并调用 :meth:`~.Item.become` 方法将传入物件改变成动画最后一刻的样子 所以 :class:`~.ItemUpdater` 可以不传入物件,传入 ``None`` 也是可以的。 ``duration=FOREVER`` 的使用 ------------------------------------------ 我们可以使用 ``duration=FOREVER`` 来创建一个持续进行的 ``Updater``,例如: .. janim-example:: ForeverUpdater :media: _static/tutorial/ForeverUpdater.mp4 :hide_name: 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) ``StepUpdater`` 的使用 ------------------------------------ 按步更新物件,适合用于 **“需要基于上一刻的状态更新下一刻状态”** 的情景,例如物理模拟或是微分方程数值演示等。 以下是一个最简单(但也是最没必要使用 :class:`~.StepUpdater`)的一个示例: .. janim-example:: SimplestStepUpdater :media: _static/tutorial/SimplestStepUpdater.mp4 :hide_name: 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() 在这个示例中,:class:`~.StepUpdater` 的函数会每次将圆形向右移动 1/50 个单位, 由于 :class:`~.StepUpdater` 默认情况下每秒钟会执行 50 次,所以圆形每秒会向右移动 1 个单位,经过两秒则时间则向右移动了 2 个单位。 --------------------- 下面是一个更复杂的例子, 我们结合 :class:`~.CustomData` 给物件附加了“速度”与“加速度”这两个物理属性, 在这个例子中演示了物件依赖这些属性的动态变化 .. janim-example:: UpdatingPhysicalBlock :extract-from-test-mark: :media: _static/tutorial/UpdatingPhysicalBlock.mp4 :hide_name: 关于 :class:`~.CustomData` 以及这个例子的具体介绍,可以参考教程页面 :ref:`add_custom_data` ,这里暂时略过。 --------------------- .. note:: 文档有待完善