Skip to main content

Custom Components

Components are classes like Rect and Circle that can abstract rendering and data functionality into reusable, modular pieces. To use a component in a scene, add it to the view and provide arguments to the component.

<Switch initialState={false} />

To define what arguments a component will take, first define an interface. All properties 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 to inherit any methods from an existing component, extend your class from Node. We advise extending from the component most similar to the component you are building. 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 a property with the same name. Motion Canvas provides type decorators to facilitate 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 (even user-defined ones) are wrapped in SimpleSignal<>. The type does not need to be passed to color signal as Motion Canvas knows that it must be of a color-resolvable type. In both, the class is passed at the end of the wrapper to register 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 access your data elsewhere. For example, if you were building a component to display an array, 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 would add 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 when wanting to animate a component easily. Here is an example of a method for toggling 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,
colorSignal,
initial,
signal,
} from '@motion-canvas/2d';
import {
Color,
ColorSignal,
PossibleColor,
SignalValue,
SimpleSignal,
all,
createRef,
createSignal,
easeInOutCubic,
tween,
} from '@motion-canvas/core';

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