跳转至

§4.3 自定义动画

字数 761 个   代码 63 行   阅读时间 3 分钟   访问量

一、动画基类

1.1 参数及方法

关于动画基类 Animation,在 §4.1 基础动画 中提过一嘴,实现动画的核心就在于这个类。这里说明一下它的相关参数。

  • ms: 动画时长,单位是毫秒
  • callback: 回调函数,每帧都将调用这个函数,传入当前时间占比
  • controller: 控制函数
  • end: 终止函数,动画一轮结束时会调用这个函数
  • repeat: 重复次数,小于零表示无限次,默认为 0
  • fps: 动画帧率,默认为 30
  • derivation: 表示是否对控制函数求导,默认为 False

此外,动画基类还有两个方法,即 start 和 stop。但是注意,调用 stop 之后这个动画就被彻底终止了,并不是暂停,如果重新调用 start 也只是从头开始运行动画。

下面有一个简单的示例,展示了相关参数的用法:

import time

import maliang
import maliang.animation as animation

root = maliang.Tk()

callback = lambda x: print(x, time.time() - t)
end = lambda: print("Done!")

an = animation.Animation(1000, callback, controller=animation.linear, end=end)

t = time.time()
an.start()
root.mainloop()

运行上述示例,可以看见每帧都输出了当前时间占比和实际时长,结束时调用 end 函数。

关于最后一个参数,稍微有些复杂,将在下一部分进行说明。

1.2 控制动画的速度

众所周知,位移函数求导之后就是速度函数,正如前面章节 §4.2 控制函数,有时候位置并不好直接修改,但速度比较好控制,这个时候就需要将 derivation 设为 True 了。此时每帧传给回调函数的参数将不在每帧的时间占比了,而是这一帧的时间占比,所有帧的总和为 1(1),也就是函数图像与横纵轴截出来的面积为 1。

  1. 所以并不是直接的求导,而是求导之后再除以总帧数

那这有什么用呢?当你的回调函数并非直接操控位置时可以尝试使用这个,典型的就是 Canvas 的 move 方法和 moveto 方法。move 使用的是相对坐标,对于单帧而言相当于速度,moveto 使用的是绝对坐标,对于单帧而言就是位置。当我们无法使用 moveto 方法的时候,move 方法配合 derivation 参数就起作用了(如 MoveWidget)。

二、自定义动画类

所谓自定义动画类,实际上就是通过继承动画基类并具化为自己需要的类。可以把内置的那些基础动画类作为参考,实现属于自己需要的动画类。当然,若是需求比较简单,那还是直接调用动画基类就好。

下面看一下内置基础动画类 MoveWidget 的源代码:

class MoveWidget(Animation):
    """Animation of moving `virtual.Widget`."""

    def __init__(
        self,
        widget: virtual.Widget,
        offset: tuple[float, float],
        duration: int,
        *,
        controller: collections.abc.Callable[[float], float] = controllers.linear,
        end: collections.abc.Callable[[], typing.Any] | None = None,
        fps: int = 30,
        repeat: int = 0,
        repeat_delay: int = 0,
    ) -> None:
        """
        * `widget`: the `virtual.Widget` to be moved
        * `offset`: relative offset of the coordinates
        * `duration`: duration of the animation, in milliseconds
        * `controller`: a function that controls the animation process
        * `end`: end function, which is called once at the end of the animation
        * `fps`: frame rate of the animation
        * `repeat`: number of repetitions of the animation
        * `repeat_delay`: length of the delay before the animation repeats
        """
        Animation.__init__(
            self, duration, lambda p: widget.move(offset[0]*p, offset[1]*p),
            controller=controller, end=end, fps=fps, repeat=repeat,
            repeat_delay=repeat_delay, derivation=True,
        )

看起来很多,实际核心内容就一点点。当然,我们自己定义属于自己的动画类时并不需要像上面这样写得如此繁琐。

比如,我们写一个这样的动画类:

import math

import maliang
import maliang.animation as animation


class MyAnimation(animation.Animation):
    """My customized animation class"""

    def __init__(self, ms: int, window: maliang.Tk, *args, **kwargs) -> None:
        """Let the window gradually change from transparent to clear"""
        controller = animation.generate(math.sin, 0, math.pi, map_y=False)
        super().__init__(ms, window.alpha, controller=controller, *args, **kwargs)


root = maliang.Tk()
MyAnimation(2000, root, repeat=-1, fps=60).start(delay=1000)
root.mainloop()

运行上述代码,你窗口不停的从透明到清晰,再清晰到透明地变化。