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