Signals
Signals represent a value that may change over time. They can be used to definedependencies between the state of the animation. This way, when a value changes,all other values that depend on it get automatically updated.
Overview
Signals for primitive types are created using the
createSignal()
function, where the firstargument specifies their initial value:
import
{
createSignal
}
from
'@motion-canvas/core/lib/signals'
;
const
signal
=
createSignal
(
0
)
;
Additionally, each complex type has a static
createSignal()
method that can beused to create a signal for said type:
import
{
Vector2
}
from
'@motion-canvas/core/lib/types'
;
const
signal
=
Vector2
.
createSignal
(
Vector2
.
up
)
;
Properties of every node are also represented by signals:
const
circle
=
<
Circle
/>
;
const
signal
=
circle
.
fill
;
Once created, signals can be invoked to perform one of the three possibleactions (The action is chosen based on the number of arguments):
-
retrieve the value:
const value = signal ( ) ;
-
update the value:
signal ( 3 ) ;
-
create a
tween
for the value:
yield * signal ( 2 , 0.3 ) ;
Instead of the actual value, a signal can be provided with a function thatcomputes the value dynamically. Consider the following example:
const
radius
=
createSignal
(
1
)
;
const
area
=
createSignal
(
(
)
=>
Math
.
PI
*
radius
(
)
*
radius
(
)
)
;
console
.
log
(
area
(
)
)
;
// 3.141592653589793
radius
(
2
)
;
console
.
log
(
area
(
)
)
;
// 12.566370614359172
Here, the
area
signal uses the
radius
signal to compute its value.
Explanation
To better understand how signals work, let's modify the example from before tosee when exactly the area is calculated:
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!
This demonstrates three important aspects of signals:
Laziness
Signals are only calculated when their value is requested. The first
"area recalculated!"
message is logged to console only after
area()
iscalled.
Caching
Once the signal is calculated, its value is saved and then returned duringsubsequent calls to
area()
. That's why nothing is logged to the console duringthe second call. This aspect of signals makes them perfect for cachingcomputationally heavy operations. In fact, Motion Canvas uses signals internallyto cache things such as matrices.
Dependency tracking
The
area
signal keeps track of other signals it depends on. When we change the
radius
signal, the
area
signal is notified about that. But it doesn't getrecalculated immediately - laziness is still at play. We can modify the radiushowever many times we want, but the
area
will be recalculated only once itsvalue is requested again by calling
area()
.
DEFAULT
values
Signals keep track of the initial values specified during creation. At any time,we can reset a signal to its initial value by passing the
DEFAULT
symbol to it:
import
{
DEFAULT
,
createSignal
}
from
'@motion-canvas/core/lib/signals'
;
const
signal
=
createSignal
(
3
)
;
// <- initial value is 3
signal
(
2
)
;
signal
(
)
;
// <- value is now 2
signal
(
DEFAULT
)
;
signal
(
)
;
// <- value is reset back to 3
We can also use the
DEFAULT
symbol for
tweening
:
yield
*
signal
(
DEFAULT
,
2
)
;
Resetting to the default value is especially useful with node properties. In theexample below, we set the
lineHeight
of the
Txt
node to
150%
. This will override itsdefault value, which would be simply inherited from its parent:
const
text
=
createRef
<
Txt
>
(
)
;
view
.
add
(
<
Txt
lineHeight
=
{
'150%'
}
ref
=
{
text
}
>
Hello world!
Txt
>
,
)
;
If we want to reset the
lineHeight
back to the default, inherited value, we can do so with
DEFAULT
:
text
(
)
.
lineHeight
(
DEFAULT
)
;
Complex example
We can use the fact that properties of nodes are represented by signals toconstruct scenes that automatically update when the data changes. Following theprevious example, let's create a visualisation for the area of the circle:
Below you'll find the code used to create this animation. We highlighted all theplaces where signals are used:
import
{
makeScene2D
}
from
'@motion-canvas/2d/lib/scenes'
;
import
{
Circle
,
Txt
,
Line
}
from
'@motion-canvas/2d/lib/components'
;
import
{
createSignal
}
from
'@motion-canvas/core/lib/signals'
;
import
{
Vector2
}
from
'@motion-canvas/core/lib/types'
;
import
{
waitFor
}
from
'@motion-canvas/core/lib/flow'
;
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
)
;
}
)
;
With this setup, all we need to do is animate the
radius
signal, and the restof the scene will adjust accordingly:
yield
*
radius
(
4
,
2
)
.
to
(
3
,
2
)
;