跳到主要内容

Tweening

Tweens 是动画的基本构建块之一。它们是一种特殊类型的生成器,在给定的时间内两个值之间进行动画处理。

tween function

创建 tween 的最简单方法是通过 tween 函数:

import {Circle, makeScene2D} from '@wangyaoshen/locus-2d';
import {createRef, map, tween} from '@wangyaoshen/locus-core';

export default makeScene2D(function* (view) {
const circle = createRef<Circle>();

view.add(
<Circle
ref={circle}
x={-300}
width={240}
height={240}
fill="#e13238"
/>,
);
yield* tween(2, value => {
circle().position.x(map(-300, 300, value));
});
});

在上面的示例中,我们在 2 秒的时间内将圆圈的 x 坐标从 -300 动画到 300

tween 函数接受两个参数。第一个指定 tween 的持续时间(以秒为单位)。第二个是一个回调函数,将在 tween 发生的每一帧调用。它接收的 value 参数是一个从 01 的数字,通知我们 tween 的进度。我们可以使用它来计算我们的 tween 动画的值。对于我们的圆圈,我们使用 map 函数将 value 范围从 [0, 1] 映射到 [-300, 300] 并将其设置为 x 坐标:

Timing functions

目前,我们的动画感觉有点不自然。value 参数更改的速度是恒定的,这反过来使圆圈以恒定速度移动。然而,在现实生活中,物体具有惯性 - 它们需要时间来加速和减速。我们可以使用 timing functions 来模拟这种行为。

Timing function 接受 [0, 1] 范围内的数字,并返回相同范围内的另一个数字,但具有变化的速率。Motion Canvas 提供所有最流行的 timing functions(有时称为 easing functions),但由于它是一个普通的 JavaScript 函数,你可以创建自己的。

让我们使用 easeInOutCubic 函数来修复我们的动画:

yield *
tween(2, value => {
circle().position.x(map(-300, 300, easeInOutCubic(value)));
});

easeInOut 意味着对象将在开始时加速(in),在结束时减速(Out)。Cubic 表示使用的数学函数 - 在这种情况下,它是三次方程。知道这一点,一个名为 easeOutQuad 的函数将使对象以全速开始,然后在结束时使用二次方程减速。

通过在时间以恒定速率变化时为对象的 y 坐标制作动画,可以可视化特定的 easing 函数的效果,例如使其弹入和弹出。

因为使用 timing functions 来映射值范围是一个非常常见的模式,所以可以完全跳过 map 并将范围传递给 timing function 本身:

// This:
map(-300, 300, easeInOutCubic(value));
// Can be simplified to:
easeInOutCubic(value, -300, 300);

Interpolation functions

到目前为止,我们只制作了一个单一数值的动画。map 函数可用于在两个数字之间进行插值,但要为更复杂的类型制作动画,我们需要使用插值函数。考虑以下示例:

// import { Color } from "@motion-canvas/core";
yield *
tween(2, value => {
circle().fill(
Color.lerp(
new Color('#e6a700'),
new Color('#e13238'),
easeInOutCubic(value),
),
);
});

Color.lerp 是一个在两种颜色之间进行插值的静态函数:

Motion Canvas 中的所有复杂类型都提供一个名为 lerp 的静态方法,用于在该类型的两个实例之间进行插值。

除了默认的线性插值之外,某些类型提供更高级的函数,例如 Vector2.arcLerp。它使对象沿着弯曲的路径从点 a 移动到 b:

yield *
tween(2, value => {
circle().position(
Vector2.arcLerp(
new Vector2(-300, 200),
new Vector2(300, -200),
easeInOutCubic(value),
),
);
});

Tweening properties

当我们需要编排复杂动画时,tween 函数很有用。但是,有一种更好的方法来为单个属性制作 tween。你可能还记得 quickstart 部分中的以下 tween:

yield *
tween(2, value => {
circle().color(
Color.lerp(
new Color('#e6a700'),
new Color('#e13238'),
easeInOutCubic(value),
),
);
});

可以写成:

yield * circle().color('#e13238', 2);

在这里,我们使用一个类似于 setter 的 SignalTween 签名,除了它接受转换持续时间作为第二个参数。在底层,这也会创建一个 tween - 一个以当前值开始并以新提供的值结束的 tween。

我们可以通过在返回的对象上调用 to() 方法来链接多个 tweens:

yield * circle().color('#e13238', 2).to('#e6a700', 2);

默认情况下,属性 tweens 使用 easeInOutCubic 作为 timing function。我们可以通过提供第三个参数来覆盖它:

yield *
circle().color(
'#e13238',
2,
easeOutQuad,
);

同样,我们可以将自定义插值函数作为第四个参数传递:

yield *
circle().position(
new Vector2(300, -200),
2,
easeInOutCubic,
Vector2.arcLerp,
);

spring function

spring 函数允许我们使用胡克定律在两个值之间进行插值。我们需要提供弹簧的描述以及 fromto 值。你可以把它想象成有一个处于静止位置的弹簧(to 值),将其一直拉伸到起始位置(from 值),然后松开它。弹簧试图达到平衡时的运动就是我们可以用来驱动动画的东西。

在下面的示例中,我们使用弹簧来为圆圈的位置制作动画,但这种方法可以用于更多事情,而不仅仅是位置。

import {Circle, makeScene2D} from '@wangyaoshen/locus-2d';
import {
PlopSpring,
SmoothSpring,
createRef,
spring,
} from '@wangyaoshen/locus-core';

export default makeScene2D(function* (view) {
const circle = createRef<Circle>();

view.add(
<Circle
ref={circle}
x={-400}
size={240}
fill={'#e13238'}
/>,
);

yield* spring(PlopSpring, -400, 400, 1, value => {
circle().position.x(value);
});
yield* spring(SmoothSpring, 400, -400, value => {
circle().position.x(value);
});
});

Spring description

spring 函数的第一个参数期望一个描述弹簧物理属性的对象。Motion Canvas 附带了一些有用的预设,你可以使用,例如 PlopSpringSmoothSpring。但可以定义你自己的弹簧:

const MySpring = {
mass: 0.04,
stiffness: 10.0,
damping: 0.7,
initialVelocity: 8.0,
};
  • mass - 描述弹簧的惯性。加速和减速它需要多少力。
  • stiffness - 弹簧的系数。通常在胡克方程中表示为 k。它描述弹簧的刚度。
  • damping - 随着时间的推移,阻尼使弹簧失去能量并最终稳定在平衡状态。你可以将其设置为 0 以创建无限振荡的弹簧。
  • initialVelocity - 弹簧的初始速度。你可以将 fromto 位置设置为相同的值,并给弹簧一些初始速度,使其在原位振荡。

Settle tolerance

请注意,在我们的弹簧示例中,我们为第一个弹簧提供了一个附加值:

yield * spring(PlopSpring, -400, 400, 1 /*...*/);
// here ^

这个可选参数称为 settleTolerance,用于定义弹簧应该达到的与 to 值的最小距离才能被认为是稳定的。由 spring 函数创建的生成器仅在弹簧稳定时完成。通过调整容差,我们可以根据需要使动画更快完成。在我们的示例中,我们为位置制作动画,因此 1 的容差意味着弹簧最多需要距离 to1 像素。

Saving and restoring states

所有节点都提供一个 save 方法,允许我们保存节点当前状态的快照。然后我们可以在动画的稍后点使用 restore 方法将节点恢复到之前保存的状态。

circle().save();
yield * circle().position(new Vector2(300, -200), 2);
yield * circle().restore(1);

也可以为 restore 方法提供自定义 timing function

yield * circle().restore(1, linear);

节点状态存储在堆栈上。这使得可以通过多次调用 save 方法来保存多个状态。调用 restore 时,节点将通过弹出状态堆栈中的顶部条目恢复到最近保存的状态。如果没有保存的状态,则此方法不执行任何操作。

下面的示例显示了如何在动画中存储和恢复多个状态的更完整示例。

import {Circle, makeScene2D} from '@wangyaoshen/locus-2d';
import {all, createRef} from '@wangyaoshen/locus-core';

export default makeScene2D(function* (view) {
const circle = createRef<Circle>();

view.add(
<Circle
ref={circle}
size={150}
position={[-300, -300]}
fill={'#e13238'}
/>,
);

circle().save();
yield* all(circle().position.x(0, 1), circle().scale(1.5, 1));

circle().save();
yield* all(circle().position.y(0, 1), circle().scale(0.5, 1));

circle().save();
yield* all(circle().position.x(300, 1), circle().scale(1, 1));

yield* circle().restore(1);
yield* circle().restore(1);
yield* circle().restore(1);
});