References
通常,在创建节点时,我们希望存储对它的引用,以便稍后可以为其制作动画。一种方法是先将其分配给变量,然后将其添加到场景中:
const circle = <Circle />;
view.add(circle);
// 我们现在可以为我们的圆圈制作动画:
yield * circle.scale(2, 0.3);
如果你习惯了像 React 这样的库,上面的示例可能看起来很奇怪。在 Motion Canvas 中,JSX 组件立即创建并返回给定类的实例。将它存储为引用并在整个动画中使用它是完全有效的。
但是这种方法不能很好地扩展。我们添加的节点越多,就越难看到场景的整体结构。考虑以下示例:
const rectA = <Rect />;
const rectB = <Rect />;
const circle = <Circle>{rectA}</Circle>;
view.add(
<Layout>
{circle}
{rectB}
</Layout>,
);
现在将其与不存储任何引用的版本进行比较:
view.add(
<Layout>
<Circle>
<Rect />
</Circle>
<Rect />
</Layout>,
);
如果你发现后一个示例更具可读性,那么本指南适合你。
ref property
Motion Canvas 中的每个节点都有一个名为 ref 的属性,允许你创建对该节点的引用。它接受一个回调,该回调将在节点创建后立即调用,第一个参数是新创建的实例。
考虑到这一点,我们可以将初始示例重写为:
let circle: Circle;
view.add(
<Circle
ref={instance => {
circle = instance;
}}
/>,
);
yield * circle.scale(2, 0.3);
以这种方式使用 ref 属性并不实用,我们不推荐它。但理解它是如何工作至关重要,因为所有即将到来的方法都使用这个属性作为基础。
createRef() function
使用 ref 属性的首选方法是与 createRef() 函数结合使用。继续我们的示例,我们可以将其重写为:
import {createRef} from '@motion-canvas/core';
// ...
const circle = createRef<Circle>();
view.add(<Circle ref={circle} />);
yield * circle().scale(2, 0.3);
请注意,circle 不再只是指向我们圆圈的变量。相反,它是一个 类似 signal 的函数,可用于访问它。在不带任何参数的情况下调用它(circle())会返回我们的实例。
回到更复杂场景的示例,我们现在可以将其重写为:
const rectA = createRef<Rect>();
const rectB = createRef<Rect>();
const circle = createRef<Circle>();
view.add(
<Layout>
<Circle ref={circle}>
<Rect ref={rectA} />
</Circle>
<Rect ref={rectB} />
</Layout>,
);
makeRef() function
ref 属性的另一个常见用例是将新创建的实例分配给某个对象的属性。在以下示例中,我们将圆圈分配给 circle.instance(我们稍后会讨论为什么这可能有用):
const circle = {instance: null as Circle};
view.add(
<Circle
ref={instance => {
circle.instance = instance;
}}
/>,
);
我们可以使用 makeRef() 函数来简化此过程:
import {makeRef} from '@motion-canvas/core';
// ...
const circle = {instance: null as Circle};
view.add(
<Circle ref={makeRef(circle, 'instance')} />,
);
Array of references
当我们创建一个节点数组并希望获取对所有节点的引用时,makeRef() 特别有用:
const circles: Circle[] = [];
view.add(
<Layout>
{range(10).map(index => (
<Circle ref={makeRef(circles, index)} />
))}
</Layout>,
);
在 JavaScript 中,数组是对象,其属性是它们的索引。因此 makeRef(circles, index) 将我们数组的第 n 个元素设置为创建的圆圈。结果,我们最终得到一个大小为 10 的数组,其中填充了我们可以用来为所有圆圈制作动画的圆圈。
你也可以使用 createRefArray() 辅助函数来实现相同的结果:
import {createRefArray, range} from '@motion-canvas/core';
// ...
const circles = createRefArray<Circle>();
view.add(
<Layout>
{range(10).map(() => (
<Circle ref={circles} />
))}
</Layout>,
);
这一次我们不指定索引。每当我们传递 circles 数组给 ref 属性时,新创建的圆圈将被追加到我们的数组中。
查看 flow 指南中的循环部分 以了解如何使用引用数组来编排动画。
Custom functions
makeRef() 也可以用于从自定义函数组件返回多个引用:
function Label({
refs,
children,
}: {
refs: {rect: Rect; text: Txt};
children: string;
}) {
return (
<Rect ref={makeRef(refs, 'rect')}>
<Txt ref={makeRef(refs, 'text')}>{children}</Txt>
</Rect>
);
}
const label = {rect: null as Rect, text: null as Txt};
view.add(<Label refs={label}>HELLO</Label>);
// 我们现在可以为我们的标签的 Rect 和 Text 制作动画:
yield * label.rect.opacity(2, 0.3);
yield * label.text.fontSize(24, 0.3);
在此示例中,我们定义了一个名为 Label 的函数组件,由一个矩形和一些文本组成。使用组件时,我们使用 refs 属性传递我们创建的 label 对象。然后使用 makeRef() 用所有必要的引用填充此对象。
createRefMap() function
随着场景变得越来越复杂,为每个节点声明引用可能会变得乏味。createRefMap() 辅助函数允许我们根据节点的类型将引用分组在一起:
import {createRefMap} from '@motion-canvas/core';
// ...
const labels = createRefMap<Txt>();
view.add(
<>
<Txt ref={labels.a}>A</Txt>
<Txt ref={labels.b}>B</Txt>
<Txt ref={labels.c}>C</Txt>
</>,
);
返回的对象是一个可以存储我们需要数量的引用的映射。在上面的示例中,我们在键 a、b 和 c 下分配了三个 Txt 引用。只需访问映射的属性,如 labels.a,就会为我们创建一个引用。属性的名称是任意的,可以是我们想要的任何东西。
稍后,我们可以使用相同的键检索引用:
yield * labels.a().text('A changes', 0.3);
yield * labels.b().text('B changes', 0.3);
yield * labels.c().text('C changes', 0.3);
要检查引用是否存在,我们可以使用 in 运算符。这将避免创建引用:
if ('d' in labels) {
yield * labels.d().text('D changes', 0.3);
}
返回的对象带有一个 mapRefs 方法,允许我们映射映射中的所有引用。它类似于 Array.prototype.map 函数:
yield * all(...labels.mapRefs(label => label.fill('white', 0.3)));
makeRefs() function
查看前面的示例,你可能会注意到我们必须定义 refs 类型两次。首先在 Label 声明中,然后在创建 label 对象时再次:
function Label({
refs,
children,
}: {
refs: {rect: Rect; text: Txt};
children: string;
}) {
return (
<Rect ref={makeRef(refs, 'rect')}>
<Txt ref={makeRef(refs, 'text')}>{children}</Txt>
</Rect>
);
}
const label = {rect: null as Rect, text: null as Txt};
view.add(<Label refs={label}>HELLO</Label>);
我们可以使用 makeRefs() 来消除这种冗余。它可以从 Label 声明中提取类型并创建一个与之匹配的空对象:
import {makeRef, makeRefs} from '@motion-canvas/core';
// ...
function Label({
refs,
children,
}: {
refs: {rect: Rect; text: Txt};
children: string;
}) {
return (
<Rect ref={makeRef(refs, 'rect')}>
<Txt ref={makeRef(refs, 'text')}>{children}</Txt>
</Rect>
);
}
const label = makeRefs<typeof Label>();
view.add(<Label refs={label}>HELLO</Label>);