跳到主要内容

Signals

Signals 代表可能随时间变化的值。它们可用于定义动画状态之间的依赖关系。这样,当值更改时,所有依赖于它的其他值都会自动更新。

概述

原始类型的 Signals 使用 createSignal() 函数创建,其中第一个参数指定它们的初始值:

import {createSignal} from '@motion-canvas/core';

const signal = createSignal(0);

此外,每个复杂类型都有一个静态的 createSignal() 方法,可用于为该类型创建 signal:

import {Vector2} from '@motion-canvas/core';

const signal = Vector2.createSignal(Vector2.up);

每个节点的属性也由 signals 表示:

const circle = <Circle />;

const signal = circle.fill;

创建后,可以调用 signals 来执行三种可能的操作之一(根据参数的数量选择操作):

  1. 检索值:
    const value = signal();
  2. 更新值:
    signal(3);
  3. 为值创建 tween:
    yield * signal(2, 0.3);

除了实际值之外,还可以为 signal 提供一个动态计算值的函数。考虑以下示例:

const radius = createSignal(1);
const area = createSignal(() => Math.PI * radius() * radius());

console.log(area()); // 3.141592653589793
radius(2);
console.log(area()); // 12.566370614359172

在这里,area signal 使用 radius signal 来计算其值。

说明

为了更好地理解 signals 的工作原理,让我们修改前面的示例,以准确计算面积的时间:

const radius = createSignal(1);
const area = createSignal(() => {
console.log('area recalculated!');
return Math.PI * radius() * radius();
});

area(); // area recalculated!
area();
radius(2);
area(); // area recalculated!
radius(3);
radius(4);
area(); // area recalculated!

这展示了 signals 的三个重要方面:

懒惰性

仅在请求信号的值时才计算它们。只有在调用 area() 之后,才会将第一条 "area recalculated!" 消息记录到控制台。

缓存

一旦计算了信号,其值就会被保存,然后在后续调用 area() 时返回。这就是为什么在第二次调用期间没有向控制台记录任何内容。信号的这一方面使它们非常适合缓存计算量大的操作。事实上,Motion Canvas 在内部使用 signals 来缓存矩阵之类的东西。

依赖跟踪

area signal 跟踪它所依赖的其他 signals。当我们更改 radius signal 时,area signal 会收到通知。但它不会立即重新计算 - 懒惰性仍然在起作用。我们可以根据需要多次修改半径,但只有在通过调用 area() 再次请求其值时,才会重新计算 area

DEFAULT

Signals 跟踪在创建期间指定的初始值。我们可以随时通过将 DEFAULT 符号传递给它来将 signal 重置为其初始值:

import {DEFAULT, createSignal} from '@motion-canvas/core';

const signal = createSignal(3); // <- 初始值为 3
signal(2);
signal(); // <- 值现在为 2
signal(DEFAULT);
signal(); // <- 值被重置回 3

我们还可以将 DEFAULT 符号用于 tweening:

yield * signal(DEFAULT, 2);

重置为默认值对于节点属性特别有用。在下面的示例中,我们将 Txt 节点的 lineHeight 设置为 150%。这将覆盖其默认值,而默认值只是从其父节点继承的:

const text = createRef<Txt>();
view.add(
<Txt lineHeight={'150%'} ref={text}>
Hello world!
</Txt>,
);

如果我们要将 lineHeight 重置为默认的继承值,我们可以使用 DEFAULT 来实现:

text().lineHeight(DEFAULT);

复杂示例

我们可以利用节点的属性由 signals 表示这一事实来构建在数据更改时自动更新的场景。按照前面的示例,让我们创建一个圆面积的可视化:

下面您将找到用于创建此动画的代码。我们突出显示了所有使用 signals 的地方:

import {Circle, Line, Txt, makeScene2D} from '@wangyaoshen/locus-2d';
import {Vector2, createSignal, waitFor} from '@wangyaoshen/locus-core';

export default makeScene2D(function* (view) {
const radius = createSignal(3);
const area = createSignal(() => Math.PI * radius() * radius());

const scale = 100;
const textStyle = {
fontWeight: 700,
fontSize: 56,
offsetY: -1,
padding: 20,
cache: true,
};

view.add(
<>
<Circle
width={() => radius() * scale * 2}
height={() => radius() * scale * 2}
fill={'#e13238'}
/>
<Line
points={[
Vector2.zero,
() => Vector2.right.scale(radius() * scale),
]}
lineDash={[20, 20]}
startArrow
endArrow
endOffset={8}
lineWidth={8}
stroke={'#242424'}
/>
<Txt
text={() => `r = ${radius().toFixed(2)}`}
x={() => (radius() * scale) / 2}
fill={'#242424'}
{...textStyle}
/>
<Txt
text={() => `A = ${area().toFixed(2)}`}
y={() => radius() * scale}
fill={'#e13238'}
{...textStyle}
/>
</>,
);

yield* radius(4, 2).to(3, 2);
yield* waitFor(1);
});

通过此设置,我们需要做的就是为 radius signal 设置动画,场景的其余部分将相应调整:

yield * radius(4, 2).to(3, 2);