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 参数是一个从 0 到 1 的数字,通知我们 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 函数允许我们使用胡克定律在两个值之间进行插值。我们需要提供弹簧的描述以及 from 和 to 值。你可以把它想象成有一个处于静止位置的弹簧(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 附带了一些有用的预设,你可以使用,例如 PlopSpring 和 SmoothSpring。但可以定义你自己的弹簧:
const MySpring = {
mass: 0.04,
stiffness: 10.0,
damping: 0.7,
initialVelocity: 8.0,
};
mass- 描述弹簧的惯性。加速和减速它需要多少力。stiffness- 弹簧的系数。通常在胡克方程中表示为k。它描述弹簧的刚度。damping- 随着时间的推移,阻尼使弹簧失去能量并最终稳定在平衡状态。你可以将其设置为0以创建无限振荡的弹簧。initialVelocity- 弹簧的初始速度。你可以将from和to位置设置为相同的值,并给弹簧一些初始速度,使其在原位振荡。
Settle tolerance
请注意,在我们的弹簧示例中,我们为第一个弹簧提供了一个附加值:
yield * spring(PlopSpring, -400, 400, 1 /*...*/);
// here ^
这个可选参数称为 settleTolerance,用于定义弹簧应该达到的与 to 值的最小距离才能被认为是稳定的。由 spring 函数创建的生成器仅在弹簧稳定时完成。通过调整容差,我们可以根据需要使动画更快完成。在我们的示例中,我们为位置制作动画,因此 1 的容差意味着弹簧最多需要距离 to 值 1 像素。
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);
});