References
Note: These docs were adopted from the original Motion Canvas docs
Usually, when creating a node, we want to store a reference to it, so we can animate it later. One way to do that is by assigning it to a variable first, and then adding it to the scene:
const circle = <Circle />;
view.add(circle);
// we can now animate our circle:
yield * circle.scale(2, 0.3);
info
If you're used to libraries such as React, the above example may seem strange. In Motion Canvas, the JSX components immediately create and return an instance of the given class. It's completely valid to store it as a reference and use it throughout the animation.
But this approach doesn't scale well. The more nodes we add, the harder it gets to see the overall structure of our scene. Consider the following example:
const rectA = <Rect />;
const rectB = <Rect />;
const circle = <Circle>{rectA}</Circle>;
view.add(
<Layout>
{circle}
{rectB}
</Layout>,
);
And now compare it to a version that doesn't store any references:
view.add(
<Layout>
<Circle>
<Rect />
</Circle>
<Rect />
</Layout>,
);
If you find the latter example more readable, this guide is for you.
ref
property​
ref
property​Each node in Motion Canvas has a property called ref
that allows you to create a reference to said node. It accepts a callback that will be invoked right after the node has been created, with the first argument being the newly created instance.
With this in mind, we can rewrite the initial example as:
let circle: Circle;
view.add(
<Circle
ref={instance => {
circle = instance;
}}
/>,
);
yield * circle.scale(2, 0.3);
Using the ref
property in this way is not really practical, and we wouldn't recommend it. But it's crucial to understand how it works because all the upcoming methods use this property as a base.
createRef()
function​
createRef()
function​The preferred way of using the ref
property is in conjunction with the createRef()
function. Continuing with our example, we can rewrite it as:
import {createRef} from '@revideo/core';
// ...
const circle = createRef<Circle>();
view.add(<Circle ref={circle} />);
yield * circle().scale(2, 0.3);
Notice that circle
is no longer just a variable that points to our circle. Instead, it's a signal-like function that can be used to access it. Invoking it without any arguments (circle()
) returns our instance.
Going back to the example with the more complex scene, we can now rewrite it as:
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​
makeRef()
function​Another common use case of the ref
property is to assign the newly created instance to a property of some object. In the following example, we assign our circle to circle.instance
(We'll talk about why this may be useful in a bit):
const circle = {instance: null as Circle};
view.add(
<Circle
ref={instance => {
circle.instance = instance;
}}
/>,
);
We can use the makeRef()
function to simplify this process:
import {makeRef} from '@revideo/core';
// ...
const circle = {instance: null as Circle};
view.add(
<Circle ref={makeRef(circle, 'instance')} />,
);
Array of references​
makeRef()
can be particularly useful when we create an array of nodes and want to grab references to all of them:
const circles: Circle[] = [];
view.add(
<Layout>
{range(10).map(index => (
<Circle ref={makeRef(circles, index)} />
))}
</Layout>,
);
In JavaScript, arrays are objects whose properties are their indices. So makeRef(circles, index)
will set the nth element of our array to the created circle. As a result, we end up with an array of size 10
filled with circles that we can use to animate all of them.
You can also use the createRefArray()
helper function to achieve the same result:
import {createRefArray, range} from '@revideo/core';
// ...
const circles = createRefArray<Circle>();
view.add(
<Layout>
{range(10).map(() => (
<Circle ref={circles} />
))}
</Layout>,
);
This time we don't specify the index. Whenever we pass the circles
array to the ref
property, the newly created circle will be appended to our array.
tip
Check out the looping section in the flow guide to see how an array of references can be used to orchestrate animations.
Custom functions​
makeRef()
can also be used to return more than one reference from a custom function component:
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>);
// we can now animate both the Rect and the Text of our label:
yield * label.rect.opacity(2, 0.3);
yield * label.text.fontSize(24, 0.3);
In this example, we define a function component called Label
consisting of a rectangle with some text inside. When using the component, we use the refs
property to pass the label
object created by us. makeRef()
is then used to fill this object with all the necessary references.
createRefMap()
function​
createRefMap()
function​As the scene grows in complexity, declaring a reference for each node can become tedious. The createRefMap()
helper function lets us group references together based on the type of the node:
import {createRefMap} from '@revideo/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>
</>,
);
The returned object is a map that can store however many references we need. In the above example, we assign three Txt
references under the keys a
, b
, and c
. Simply accessing a property of the map, like labels.a
will create a reference for us. The names of the properties are arbitrary and can be anything we want.
Later on, we can retrieve the references using the same keys:
yield * labels.a().text('A changes', 0.3);
yield * labels.b().text('B changes', 0.3);
yield * labels.c().text('C changes', 0.3);
To check if a reference exists, we can use the in
operator. This will avoid creating a reference:
if ('d' in labels) {
yield * labels.d().text('D changes', 0.3);
}
The returned object comes with a mapRefs
method that lets us map over all references in the map. It's similar to the Array.prototype.map function:
yield * all(...labels.mapRefs(label => label.fill('white', 0.3)));
makeRefs()
function​
makeRefs()
function​Looking at the previous example, you may notice that we had to define the refs
type twice. First in the Label
declaration and then again when creating the label
object:
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>);
We can use makeRefs()
to eliminate this redundancy. It can extract the type from the Label
declaration and create an empty object matching it:
import {makeRef, makeRefs} from '@revideo/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>);
Last updated