自定义组件
组件是类似 Rect 和 Circle 的类,可以将渲染和数据功能抽象为可重用的模块化部分。要在场景中使用组件,将其添加到视图并为组件提供参数。
<Switch initialState={false} />
要定义组件将接受的参数,首先定义一个接口。接口的所有属性必须包装在 SignalValue<> 中,如下所示:
// 您可以扩展现有的 props 接口
// 如 LayoutProps、ShapeProps 或 NodeProps 以
// 将它们的属性与您定义的属性一起包含
export interface SwitchProps extends NodeProps {
initialState?: SignalValue<boolean>;
// 我们这里不使用 color,因为我们希望
// 能够将十六进制字符串和 rgb 值传递给 accent 而不是 `Color`
accent?: SignalValue<PossibleColor>;
}
接下来,为您的组件创建一个类。组件类必须扩展 Node 或其子类之一。如果您不想从现有组件继承任何方法,请从 Node 扩展您的类。我们建议从与您正在构建的组件最相似的组件扩展。例如,如果您要制作一个包含 Layout 的组件,您应该扩展 Layout 和 LayoutProps。
export interface SwitchProps extends NodeProps {
// 属性
}
export class Switch extends Node {
// 实现
}
要使用接口中定义的属性,您的类_必须_包含具有相同名称的属性。Motion Canvas 提供类型装饰器来促进这一点,如 @initial() 和 @signal()。点击这里获取有关 signal 的更多信息。
以下是定义此类属性的示例:
export class Switch extends Node {
// @initial - 可选,如果未提供属性,则将其设置为
// 初始值。
@initial(false)
// @signal - motion canvas 对每个传入的 props 都需要
@signal()
public declare readonly initialState: SimpleSignal<boolean, this>;
@initial('#68ABDF')
// @colorSignal - 一些复杂类型为 signal 提供了专门的装饰器
// 用于处理解析。
// 在这种情况下,`accent` 将自动将字符串转换为 `Color`
@colorSignal()
public declare readonly accent: ColorSignal<this>;
// ...
}
请注意颜色是如何包装在 ColorSignal<> 中的,而任何其他类型(甚至是用户定义的类型)都包装在 SimpleSignal<> 中。不需要将类型传递给 color signal,因为 Motion Canvas 知道它必须是可解析颜色的类型。在这两种情况下,类都在包装器的末尾传递,以将 signal 注册到类。属性必须使用 public、declare 和 readonly 关键字初始化。
正常属性可以正常定义。例如:
export class Switch extends Node {
public constructor(props?: SwitchProps) {
super({
// 如果您想确保 layout 对此组件始终为 true,
// 您可以在此处添加它,如下所示:
// layout: true
...props,
});
// ...
}
}
props 参数在 super() 调用之外也很有用,可以在其他地方访问您的数据。例如,如果您正在构建一个组件来显示数组,您可以使用 props 为数组中的每个 Rect 设置颜色。
现在我们可以使用 this.add() 向视图添加元素,就像向场景的视图添加元素一样:
export class Switch extends Node {
public constructor(props?: SwitchProps) {
// ...
this.add(
<Rect>
<Circle />
</Rect>,
);
}
}
由于这是一个类,您还可以添加方法。这在想要轻松动画组件时特别有用。以下是切换我们开关的方法的示例:
export class Switch extends Node {
// ...
public *toggle(duration: number) {
yield* all(
tween(duration, value => {
// ...
}),
tween(duration, value => {
// ...
}),
);
this.isOn = !this.isOn;
}
}
以下是我们在本指南中构建的组件的源代码:
import {
Circle,
Node,
NodeProps,
Rect,
colorSignal,
initial,
signal,
} from '@wangyaoshen/locus-2d';
import {
Color,
ColorSignal,
PossibleColor,
SignalValue,
SimpleSignal,
all,
createRef,
createSignal,
easeInOutCubic,
tween,
} from '@wangyaoshen/locus-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;
}
}