Custom Components
Components are classes like
Rect
and
Circle
that can abstract rendering and datafunctionality into reusable, modular pieces. To use a component in a scene, addit to the view and provide arguments to the component.
<
Switch
initialState
=
{
false
}
/>
To define what arguments a component will take, first define an interface. Allproperties of the interface must be wrapped in
SignalValue<>
as such:
// You can extend an existing props interface
// such as LayoutProps, ShapeProps or NodeProps to
// include their properties alongside the ones you
// define
export
interface
SwitchProps
extends
NodeProps
{
initialState
?
:
SignalValue
<
boolean
>
;
// We don't use color here because we want
// to be able to pass hex strings and rgb
// values to accent rather than a `Color`
accent
?
:
SignalValue
<
PossibleColor
>
;
}
Next, create a class for your components. The component class must extend
Node
or one of its subclasses. If you don't want toinherit any methods from an existing component, extend your class from
Node
.We advise extending from the component most similar to the component you arebuilding. For instance, if you were to make a component including a
Layout
, you should extend
Layout
and
LayoutProps
.
export
interface
SwitchProps
extends
NodeProps
{
// properties
}
export
class
Switch
extends
Node
{
// implementation
}
To use the properties defined in the interface, your class
must
contain aproperty with the same name. Motion Canvas provides type decorators tofacilitate this like
@initial()
and
@signal()
. Click
here
for more information on signals.
Here is an example of how you would define such properties:
export
class
Switch
extends
Node
{
// @initial - optional, sets the property to an
// initial value if it was not provided.
@
initial
(
false
)
// @signal - is required by motion canvas
// for every prop that was passed in.
@
signal
(
)
public
declare
readonly
initialState
:
SimpleSignal
<
boolean
,
this
>
;
@
initial
(
'#68ABDF'
)
// @colorSignal - some complex types provide a dedicated decorator for
// signals that takes care of parsing.
// In this case, `accent` will automatically convert strings into `Color`s
@
colorSignal
(
)
public
declare
readonly
accent
:
ColorSignal
<
this
>
;
// ...
}
Notice how colors are wrapped in
ColorSignal<>
while any other type (evenuser-defined ones) are wrapped in
SimpleSignal<>
. The type does not need to bepassed to color signal as Motion Canvas knows that it must be of acolor-resolvable type. In both, the class is passed at the end of the wrapper toregister the signal to the class. Properties must be initialised with the
public
,
declare
and
readonly
keywords.
Normal properties can be defined as normal. For example:
export
class
Switch
extends
Node
{
public
constructor
(
props
?
:
SwitchProps
)
{
super
(
{
// If you wanted to ensure that layout was always
// true for this component, you could add it here
// as such:
// layout: true
...
props
,
}
)
;
// ...
}
}
The
props
parameter can also be useful outside the
super()
call to accessyour data elsewhere. For example, if you were building a component to display anarray, you could use props to set the color of every
Rect
in the array.
Now we can add elements to the view by using
this.add()
, much like you wouldadd to a scene's view:
export
class
Switch
extends
Node
{
public
constructor
(
props
?
:
SwitchProps
)
{
// ...
this
.
add
(
<
Rect
>
<
Circle
/>
Rect
>
,
)
;
}
}
Since this is a class, you can also add methods. This is especially useful whenwanting to animate a component easily. Here is an example of a method fortoggling our switch:
export
class
Switch
extends
Node
{
// ...
public
*
toggle
(
duration
:
number
)
{
yield
*
all
(
tween
(
duration
,
value
=>
{
// ...
}
)
,
tween
(
duration
,
value
=>
{
// ...
}
)
,
)
;
this
.
isOn
=
!
this
.
isOn
;
}
}
Here is the source code for the component we have built throughout this guide:
import
{
Circle
,
Node
,
NodeProps
,
Rect
}
from
'@motion-canvas/2d/lib/components'
;
import
{
easeInOutCubic
,
tween
}
from
'@motion-canvas/core/lib/tweening'
;
import
{
Color
,
ColorSignal
,
PossibleColor
,
}
from
'@motion-canvas/core/lib/types/Color'
;
import
{
colorSignal
,
initial
,
signal
}
from
'@motion-canvas/2d/lib/decorators'
;
import
{
createSignal
,
SignalValue
,
SimpleSignal
,
}
from
'@motion-canvas/core/lib/signals'
;
import
{
createRef
}
from
'@motion-canvas/core/lib/utils'
;
import
{
all
}
from
'@motion-canvas/core/lib/flow'
;
export
interface
SwitchProps
extends
NodeProps
{
initialState
?
:
SignalValue
<
boolean
>
;
accent
?
:
SignalValue
<
PossibleColor
>
;
}
export
class
Switch
extends
Node
{
@
initial
(
false
)
@
signal
(
)
public
declare
readonly
initialState
:
SimpleSignal
<
boolean
,
this
>
;
@
initial
(
'#68ABDF'
)
@
colorSignal
(
)
public
declare
readonly
accent
:
ColorSignal
<
this
>
;
private
isOn
:
boolean
;
private
readonly
indicatorPosition
=
createSignal
(
0
)
;
private
readonly
offColor
=
new
Color
(
'#242424'
)
;
private
readonly
indicator
=
createRef
<
Circle
>
(
)
;
private
readonly
container
=
createRef
<
Rect
>
(
)
;
public
constructor
(
props
?
:
SwitchProps
)
{
super
(
{
...
props
,
}
)
;
this
.
isOn
=
this
.
initialState
(
)
;
this
.
indicatorPosition
(
this
.
isOn
?
50
:
-
50
)
;
this
.
add
(
<
Rect
ref
=
{
this
.
container
}
fill
=
{
this
.
isOn
?
this
.
accent
(
)
:
this
.
offColor
}
size
=
{
[
200
,
100
]
}
radius
=
{
100
}
>
<
Circle
x
=
{
(
)
=>
this
.
indicatorPosition
(
)
}
ref
=
{
this
.
indicator
}
size
=
{
[
80
,
80
]
}
fill
=
"
#ffffff
"
/>
Rect
>
,
)
;
}
public
*
toggle
(
duration
:
number
)
{
yield
*
all
(
tween
(
duration
,
value
=>
{
const
oldColor
=
this
.
isOn
?
this
.
accent
(
)
:
this
.
offColor
;
const
newColor
=
this
.
isOn
?
this
.
offColor
:
this
.
accent
(
)
;
this
.
container
(
)
.
fill
(
Color
.
lerp
(
oldColor
,
newColor
,
easeInOutCubic
(
value
)
)
,
)
;
}
)
,
tween
(
duration
,
value
=>
{
const
currentPos
=
this
.
indicator
(
)
.
position
(
)
;
this
.
indicatorPosition
(
easeInOutCubic
(
value
,
currentPos
.
x
,
this
.
isOn
?
-
50
:
50
)
,
)
;
}
)
,
)
;
this
.
isOn
=
!
this
.
isOn
;
}
}