Skip to main content

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 ;
}
}