动画系统

到目前为止,你已经见到了两种动画创建方式:

  1. 通过 物件.anim 创建组件插值动画

  2. 通过 动画名(物件, 动画参数) 为物件应用内置的特殊动画效果

circle = Circle()
tri = Triangle()

self.forward()

self.play(Create(circle))
self.play(circle.anim.points.shift(LEFT * 3).scale(1.5))
self.play(circle.anim.set(color=RED, fill_alpha=0.5))

self.play(SpinInFromNothing(tri))
self.play(tri.anim.points.shift(RIGHT * 3).scale(1.5))
self.play(tri.anim.set(color=BLUE, fill_alpha=0.5))

self.forward()

基础动画参数

无论是哪种创建动画的方式,它们都有几个关键的参数

  • duration: 动画持续时间

    大多数动画的默认时长是 1 秒,如果你需要更长或者更短的时间可以额外设置

  • at: 动画开始的时间点

    意味着动画会在当前时刻的多少秒之后才开始,比如 at=1, duration=2 意味着动画会在当前时刻的 1 秒后开始,进行 2 秒

    这个参数在接下来将要讲的“动画组”的内容中,会更加实用

  • rate_func: 动画的缓动函数

    大多数动画默认以 smooth() 的方式进行插值,使得动画过程在开始和结束的时候较慢,中间过程较快,总体上表现为一个平滑的过渡

    其它一些常用的缓动函数还有 linear(),使得动画全程匀速进行; 以及 rush_into() rush_from() 等,具体请参考 rate_functions 中的介绍

    ../_images/rate_functions.png

上面这些参数可以调整动画的表现细节,例如 duration 可以调整动画快慢,控制节奏, at 可以控制动画的开始时机; 当你想要让物体快速进入并逐渐减速时,可以考虑使用 rush_into() 作为缓动函数;总之,可以多多地探索这些参数的使用,获得更好的动画效果。

以下是对上面的动画参数进行一些调整后的示例:

circle = Circle()
star = Star()

self.forward()

self.play(Create(circle, duration=0.8, rate_func=rush_from))
self.play(
    circle.anim(rate_func=ease_out_bounce)
        .points.shift(LEFT * 3).scale(1.5)
)
self.play(
    circle.anim(duration=2)
        .set(color=RED, fill_alpha=0.5)
)

self.play(SpinInFromNothing(star, duration=0.6, rate_func=rush_from))
self.play(
    star.anim(rate_func=ease_out_bounce)
        .points.shift(RIGHT * 3).scale(1.5)
)
self.play(
    star.anim(duration=2)
        .set(color=YELLOW, fill_alpha=0.5)
)

self.forward()
  1. CreateSpinInFromNothing 的缓动函数改为了 rush_from(),并缩短它们进入的时长

  2. 将移动动画的缓动函数改为了 ease_out_bounce(),使得物体在移动终点处有一个弹跳的效果

  3. 将变色动画的时长改为了 2 秒

小技巧

.anim 后紧跟括号填入参数即可改变其动画参数。

在合适的地方增加换行可以优化代码的可读性,特别是在动画调用较长的时候。

动画组

动画并不是只能像上面一样单独依次执行,我们还可以让多个动画一起执行,创建更加丰富的动画效果。

首先是最基础的, 放在同一个 self.play 函数中的动画会一起执行,你也可以给动画分别传入 at 参数来控制它们的开始时机:

circle = Circle()
circle.points.to_border(UL, buff=LARGE_BUFF)

square = Square()
square.points.to_border(DL, buff=LARGE_BUFF)

self.play(
    FadeIn(circle),
    FadeIn(square)
)
self.play(
    circle.anim
        .points.to_border(UR, buff=LARGE_BUFF),
    square.anim(at=0.2)
        .points.to_border(DR, buff=LARGE_BUFF)
)

你还可以使用 AnimGroup Succession 等方式来组合多个动画。

  • 其中 AnimGroup 只是单纯地将多个动画组合到一起,可以统一应用 atduration 等参数

    AnimGroup 会根据传入的 duration 参数将内部动画结构进行整体伸缩以匹配时长

  • Succession 则会将多个动画串联起来,前一个动画结束后再开始下一个动画

circle = Circle()
circle.points.to_border(UL, buff=LARGE_BUFF)

square = Square()
square.points.to_border(DL, buff=LARGE_BUFF)

self.play(
    FadeIn(circle),
    FadeIn(square)
)
self.play(
    Succession(
        circle.anim(rate_func=rush_into)
            .points.to_border(UR, buff=LARGE_BUFF),
        square.anim(rate_func=rush_from)
            .points.to_border(DR, buff=LARGE_BUFF),
        duration=3
    ),
    AnimGroup(
        ShowCreationThenDestructionAround(circle),
        ShowCreationThenDestructionAround(square),
        at=0.5,
        duration=2
    )
)
../_images/ComplexGroupedAnimation_TimelineScreenshot.png

提示

其实 self.play 函数本身就充当一个 AnimGroup 的角色, 所以你可以直接将多个动画放在 self.play 中,并应用 atduration 等参数。

备注

关于动画组的更多内容,可以参考 composition 中的介绍, 其中还提及了关于 lag_ratiooffset 参数的使用,这里不再展开叙述。

预先设置动画

当我们使用 self.play 播放一个长达 4 秒的动画之后,当前时刻便会跳转至 4 秒后, 但是我们就失去了在这 4 秒内创建其它动画的机会,因为只能往前走而不能倒退。

因此,JAnim 提供了一个实用的功能——预先设置动画,但不在时间上前进,可以调用 self.prepare 做到:

txt = Text('JAnim')
txt.points.shift(LEFT * 2)

self.prepare(
    CircleIndicate(txt),
    at=1,
    duration=2
)

self.play(txt.anim.points.shift(RIGHT * 4).scale(2), duration=2)
self.play(txt.anim.points.shift(LEFT * 4).scale(0.5), duration=2)
../_images/PrepareAnimation_TimelineScreenshot.png

在该示例中,我们使用 self.prepare 预先设置了一个 CircleIndicate 动画, 使得在文字在后续的移动动画中,能够在预先设置的时间段看到黄圈高亮的效果。

备注

从原理上来讲,其实 play 就是 prepare + forward 的组合。

动画序列控制

Succession 的动画序列中,我们可以插入一些额外的控制,例如:

  • 使用 Wait 插入等待时间

  • 使用 Do 在动画序列的特定时间执行指定操作

dot = Dot(RIGHT * 2).show()
txt = Text('just a dot').show()
txt.points.next_to(dot, DOWN)

star = Star(start_angle=0, outer_radius=2)
star.points.shift(dot.points.box.center - star.points.get()[0])

txt1 = Text('Rotating...', font_size=60, color=GREY_D, depth=1)
txt2 = Text('Drawing a star!', font_size=60, color=GREY_D, depth=1)

self.forward()
self.play(
    Aligned(
        Succession(
            Do(txt1.show),
            Rotate(dot, TAU, about_point=ORIGIN, duration=2),
            Do(txt1.hide),
            Wait(0.5),
            Do(txt2.show),
            AnimGroup(
                MoveAlongPath(dot, star),
                Create(star, auto_close_path=False),
                duration=2
            ),
            Do(txt2.hide)
        ),
        Follow(txt, dot, DOWN)
    )
)
self.forward()
../_images/CompositionControl_TimelineScreenshot.png

内置动画

关于更多可用的内置动画,可查阅以下列表中的内容:

JAnim 还有一个重要的特性是“动画复合”,我们将在 Updater 的使用 中详细介绍这一特性。

.r 的使用

在 JAnim 中,由于 物件-组件 的结构关系,导致在一个组件中进行完操作后, 需要使用 .r 来返回物件级别,从而再访问物件或是其它组件中的功能,例如:

item.points.shift(LEFT * 2).r.color.fade(0.5)

或是对于动画而言

self.play(
    item.anim.points.shift(LEFT * 2).r.color.fade(0.5)
)