mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-11 23:07:57 -05:00
feat: add cloning (#80)
This commit is contained in:
@@ -11,8 +11,6 @@
|
||||
"core:test": "npm run test -w packages/core",
|
||||
"2d:build": "npm run build -w packages/2d",
|
||||
"2d:watch": "npm run watch -w packages/2d",
|
||||
"legacy:build": "npm run build -w packages/legacy",
|
||||
"legacy:watch": "npm run watch -w packages/legacy",
|
||||
"ui:build": "npm run build -w packages/ui",
|
||||
"ui:dev": "npm run dev -w packages/ui",
|
||||
"template:serve": "npm run serve -w packages/template",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
clone,
|
||||
compound,
|
||||
computed,
|
||||
initial,
|
||||
interpolation,
|
||||
Property,
|
||||
property,
|
||||
wrapper,
|
||||
@@ -424,6 +424,7 @@ export class Layout extends Node {
|
||||
public declare readonly scale: Signal<Vector2, this>;
|
||||
|
||||
@wrapper(Vector2)
|
||||
@clone(false)
|
||||
@property()
|
||||
public declare readonly absoluteScale: Signal<Vector2, this>;
|
||||
|
||||
@@ -454,6 +455,7 @@ export class Layout extends Node {
|
||||
public declare readonly position: Signal<Vector2, this>;
|
||||
|
||||
@wrapper(Vector2)
|
||||
@clone(false)
|
||||
@property()
|
||||
public declare readonly absolutePosition: Signal<Vector2, this>;
|
||||
|
||||
@@ -470,8 +472,9 @@ export class Layout extends Node {
|
||||
}
|
||||
}
|
||||
|
||||
@clone(false)
|
||||
@property()
|
||||
public declare readonly absoluteRotation: Signal<Vector2, this>;
|
||||
public declare readonly absoluteRotation: Signal<number, this>;
|
||||
|
||||
protected getAbsoluteRotation() {
|
||||
const matrix = this.localToWorld();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
compound,
|
||||
computed,
|
||||
getPropertiesOf,
|
||||
initial,
|
||||
initialize,
|
||||
interpolation,
|
||||
property,
|
||||
wrapper,
|
||||
} from '../decorators';
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
Signal,
|
||||
SignalValue,
|
||||
} from '@motion-canvas/core/lib/utils';
|
||||
import {ComponentChild, ComponentChildren} from './types';
|
||||
import {ComponentChild, ComponentChildren, NodeConstructor} from './types';
|
||||
import {Promisable} from '@motion-canvas/core/lib/threading';
|
||||
import {View2D, use2DView} from '../scenes';
|
||||
import {TimingFunction} from '@motion-canvas/core/lib/tweening';
|
||||
@@ -209,6 +209,7 @@ export class Node implements Promisable<Node> {
|
||||
|
||||
public readonly children = createSignal<Node[]>([]);
|
||||
public readonly parent = createSignal<Node | null>(null);
|
||||
public readonly properties = getPropertiesOf(this);
|
||||
|
||||
public constructor({children, ...rest}: NodeProps) {
|
||||
initialize(this, {defaults: rest});
|
||||
@@ -346,6 +347,88 @@ export class Node implements Promisable<Node> {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of this node.
|
||||
*
|
||||
* @param customProps - Properties to override.
|
||||
*/
|
||||
public clone(customProps: NodeProps = {}): this {
|
||||
const props: NodeProps & Record<string, any> = {...customProps};
|
||||
if (this.children().length > 0) {
|
||||
props.children ??= this.children().map(child => child.clone());
|
||||
}
|
||||
|
||||
for (const key in this.properties) {
|
||||
const meta = this.properties[key];
|
||||
if (!meta.clone || key in props) continue;
|
||||
|
||||
const signal = (<Record<string, Signal<any>>>(<unknown>this))[key];
|
||||
props[key] = signal();
|
||||
}
|
||||
|
||||
return this.instantiate(props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a raw copy of this node.
|
||||
*
|
||||
* @remarks
|
||||
* A raw copy preserves any reactive properties from the source node.
|
||||
*
|
||||
* @param customProps - Properties to override.
|
||||
*/
|
||||
public rawClone(customProps: NodeProps = {}): this {
|
||||
const props: NodeProps & Record<string, any> = {...customProps};
|
||||
if (this.children().length > 0) {
|
||||
props.children ??= this.children().map(child => child.rawClone());
|
||||
}
|
||||
|
||||
for (const key in this.properties) {
|
||||
const meta = this.properties[key];
|
||||
if (!meta.clone || key in props) continue;
|
||||
|
||||
const signal = (<Record<string, Signal<any>>>(<unknown>this))[key];
|
||||
props[key] = signal.raw();
|
||||
}
|
||||
|
||||
return this.instantiate(props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a reactive copy of this node.
|
||||
*
|
||||
* @remarks
|
||||
* A reactive copy has all its properties dynamically updated to match the
|
||||
* source node.
|
||||
*
|
||||
* @param customProps - Properties to override.
|
||||
*/
|
||||
public reactiveClone(customProps: NodeProps = {}): this {
|
||||
const props: NodeProps & Record<string, any> = {...customProps};
|
||||
if (this.children().length > 0) {
|
||||
props.children ??= this.children().map(child => child.reactiveClone());
|
||||
}
|
||||
|
||||
for (const key in this.properties) {
|
||||
const meta = this.properties[key];
|
||||
if (!meta.clone || key in props) continue;
|
||||
|
||||
const signal = (<Record<string, Signal<any>>>(<unknown>this))[key];
|
||||
props[key] = () => signal();
|
||||
}
|
||||
|
||||
return this.instantiate(props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of this node's class.
|
||||
*
|
||||
* @param props - Properties to pass to the constructor.
|
||||
*/
|
||||
public instantiate(props: NodeProps = {}): this {
|
||||
return new (<NodeConstructor<NodeProps, this>>this.constructor)(props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this node should be cached or not.
|
||||
*/
|
||||
|
||||
@@ -29,6 +29,6 @@ export interface FunctionComponent<T = any> {
|
||||
(props: T): Node | null;
|
||||
}
|
||||
|
||||
export interface NodeConstructor<T = any> {
|
||||
new (props: T): Node;
|
||||
export interface NodeConstructor<TProps = any, TNode = Node> {
|
||||
new (props: TProps): TNode;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {SignalValue, isReactive} from '@motion-canvas/core/lib/utils';
|
||||
import {capitalize, PropertyMetadata} from './property';
|
||||
import {addInitializer} from './initializers';
|
||||
import {capitalize, getPropertyMeta} from './property';
|
||||
|
||||
/**
|
||||
* Create a compound property decorator.
|
||||
@@ -45,13 +44,29 @@ export function compound(
|
||||
mapping: string[] | Record<string, string>,
|
||||
): PropertyDecorator {
|
||||
return (target: any, key) => {
|
||||
const metaKey = `meta${capitalize(key.toString())}`;
|
||||
const meta: PropertyMetadata<any> = target[metaKey];
|
||||
const meta = getPropertyMeta<any>(target, key);
|
||||
if (!meta) {
|
||||
console.error(`Missing property decorator for "${key.toString()}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = Array.isArray(mapping)
|
||||
? mapping.map(key => [key, key])
|
||||
: Object.entries(mapping);
|
||||
|
||||
meta.compound = true;
|
||||
meta.clone = false;
|
||||
for (const [, property] of entries) {
|
||||
const propertyMeta = getPropertyMeta<any>(target, property);
|
||||
if (!propertyMeta) {
|
||||
console.error(
|
||||
`Missing property decorator for "${property.toString()}"`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
propertyMeta.compoundParent = key.toString();
|
||||
}
|
||||
|
||||
target.constructor.prototype[`get${capitalize(key.toString())}`] =
|
||||
function () {
|
||||
const object = Object.fromEntries(
|
||||
@@ -72,11 +87,5 @@ export function compound(
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
addInitializer(target, (instance: any, context: any) => {
|
||||
if (key in context.defaults) {
|
||||
instance[key](context.defaults[key]);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ export interface PropertyMetadata<T> {
|
||||
default?: T;
|
||||
interpolationFunction?: InterpolationFunction<T>;
|
||||
wrapper?: new (value: any) => T;
|
||||
clone?: boolean;
|
||||
compoundParent?: string;
|
||||
compound?: boolean;
|
||||
}
|
||||
|
||||
export interface Property<
|
||||
@@ -33,7 +36,7 @@ export interface Property<
|
||||
> extends SignalGetter<TGetterValue>,
|
||||
SignalSetter<TSetterValue>,
|
||||
SignalTween<TSetterValue>,
|
||||
SignalUtils<TOwner> {}
|
||||
SignalUtils<TSetterValue, TOwner> {}
|
||||
|
||||
export type PropertyOwner<TGetterValue, TSetterValue> = {
|
||||
[key: `get${Capitalize<string>}`]: SignalGetter<TGetterValue> | undefined;
|
||||
@@ -146,9 +149,9 @@ export function createProperty<
|
||||
|
||||
Object.defineProperty(handler, 'reset', {
|
||||
value: signal
|
||||
? () => signal?.reset()
|
||||
? signal.reset
|
||||
: initial !== undefined
|
||||
? () => setter(initial)
|
||||
? () => setter(wrap(initial))
|
||||
: () => node,
|
||||
});
|
||||
|
||||
@@ -156,8 +159,12 @@ export function createProperty<
|
||||
value: () => setter(getter()),
|
||||
});
|
||||
|
||||
Object.defineProperty(handler, 'raw', {
|
||||
value: signal?.raw ?? getter,
|
||||
});
|
||||
|
||||
if (initial !== undefined && !signal) {
|
||||
setter(initial);
|
||||
setter(wrap(initial));
|
||||
}
|
||||
|
||||
return handler;
|
||||
@@ -165,6 +172,13 @@ export function createProperty<
|
||||
|
||||
const PROPERTIES = Symbol.for('properties');
|
||||
|
||||
export function getPropertyMeta<T>(
|
||||
object: any,
|
||||
key: string | symbol,
|
||||
): PropertyMetadata<T> | null {
|
||||
return object[PROPERTIES]?.[key] ?? null;
|
||||
}
|
||||
|
||||
export function getPropertiesOf(
|
||||
value: any,
|
||||
): Record<string, PropertyMetadata<any>> {
|
||||
@@ -214,7 +228,7 @@ export function property<T>(): PropertyDecorator {
|
||||
lookup = target[PROPERTIES];
|
||||
}
|
||||
|
||||
const meta = (lookup[key] = lookup[key] ?? {});
|
||||
const meta = (lookup[key] = lookup[key] ?? {clone: true});
|
||||
addInitializer(target, (instance: any, context: any) => {
|
||||
instance[key] = createProperty(
|
||||
instance,
|
||||
@@ -232,6 +246,7 @@ export function property<T>(): PropertyDecorator {
|
||||
*
|
||||
* @remarks
|
||||
* This decorator specifies the initial value of a property.
|
||||
*
|
||||
* Must be specified before the {@link property} decorator.
|
||||
*
|
||||
* @example
|
||||
@@ -247,7 +262,7 @@ export function property<T>(): PropertyDecorator {
|
||||
*/
|
||||
export function initial<T>(value: T): PropertyDecorator {
|
||||
return (target: any, key) => {
|
||||
const meta: PropertyMetadata<T> = target[PROPERTIES]?.[key];
|
||||
const meta = getPropertyMeta<T>(target, key);
|
||||
if (!meta) {
|
||||
console.error(`Missing property decorator for "${key.toString()}"`);
|
||||
return;
|
||||
@@ -261,8 +276,8 @@ export function initial<T>(value: T): PropertyDecorator {
|
||||
*
|
||||
* @remarks
|
||||
* This decorator specifies the interpolation function of a property.
|
||||
* The interpolation function is used when tweening between different values of
|
||||
* the property.
|
||||
* The interpolation function is used when tweening between different values.
|
||||
*
|
||||
* Must be specified before the {@link property} decorator.
|
||||
*
|
||||
* @example
|
||||
@@ -280,7 +295,7 @@ export function interpolation<T>(
|
||||
value: InterpolationFunction<T>,
|
||||
): PropertyDecorator {
|
||||
return (target: any, key) => {
|
||||
const meta: PropertyMetadata<T> = target[PROPERTIES]?.[key];
|
||||
const meta = getPropertyMeta<T>(target, key);
|
||||
if (!meta) {
|
||||
console.error(`Missing property decorator for "${key.toString()}"`);
|
||||
return;
|
||||
@@ -294,9 +309,12 @@ export function interpolation<T>(
|
||||
*
|
||||
* @remarks
|
||||
* This decorator specifies the wrapper of a property.
|
||||
* Instead of returning the raw value of the property, an instance of the
|
||||
* wrapper is returned. The actual value is passed as the first parameter to the
|
||||
* constructor.
|
||||
* Instead of returning the raw value, an instance of the wrapper is returned.
|
||||
* The actual value is passed as the first parameter to the constructor.
|
||||
*
|
||||
* If the wrapper class has a method called `lerp` it will be set as the
|
||||
* default interpolation function for the property.
|
||||
*
|
||||
* Must be specified before the {@link property} decorator.
|
||||
*
|
||||
* @example
|
||||
@@ -314,7 +332,7 @@ export function wrapper<T>(
|
||||
value: (new (value: any) => T) & {lerp?: InterpolationFunction<T>},
|
||||
): PropertyDecorator {
|
||||
return (target: any, key) => {
|
||||
const meta: PropertyMetadata<T> = target[PROPERTIES]?.[key];
|
||||
const meta = getPropertyMeta<T>(target, key);
|
||||
if (!meta) {
|
||||
console.error(`Missing property decorator for "${key.toString()}"`);
|
||||
return;
|
||||
@@ -325,3 +343,36 @@ export function wrapper<T>(
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cloning property decorator.
|
||||
*
|
||||
* @remarks
|
||||
* This decorator specifies whether the property should be copied over when
|
||||
* cloning the node.
|
||||
*
|
||||
* By default, any property is copied.
|
||||
*
|
||||
* Must be specified before the {@link property} decorator.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* class Example {
|
||||
* \@wrapper(Vector2)
|
||||
* \@property()
|
||||
* public declare offset: Signal<Vector2, this>;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param value - Whether the property should be cloned.
|
||||
*/
|
||||
export function clone<T>(value = true): PropertyDecorator {
|
||||
return (target: any, key) => {
|
||||
const meta = getPropertyMeta<T>(target, key);
|
||||
if (!meta) {
|
||||
console.error(`Missing property decorator for "${key.toString()}"`);
|
||||
return;
|
||||
}
|
||||
meta.clone = value;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
computed,
|
||||
initial,
|
||||
initialize,
|
||||
interpolation,
|
||||
property,
|
||||
wrapper,
|
||||
} from '../decorators';
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface SignalTween<TValue> {
|
||||
): ThreadGenerator;
|
||||
}
|
||||
|
||||
export interface SignalUtils<TReturn> {
|
||||
export interface SignalUtils<TValue, TReturn> {
|
||||
/**
|
||||
* Reset the signal to its initial value (if one has been set).
|
||||
*
|
||||
@@ -59,13 +59,38 @@ export interface SignalUtils<TReturn> {
|
||||
* ```
|
||||
*/
|
||||
save(): TReturn;
|
||||
|
||||
/**
|
||||
* Get the raw value of this signal.
|
||||
*
|
||||
* @remarks
|
||||
* If the signal was provided with a factory function, the function itself
|
||||
* will be returned, without invoking it.
|
||||
*
|
||||
* This method can be used to create copies of signals.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const a = createSignal(2);
|
||||
* const b = createSignal(() => a);
|
||||
* // b() == 2
|
||||
*
|
||||
* const bClone = createSignal(b.raw());
|
||||
* // bClone() == 2
|
||||
*
|
||||
* a(4);
|
||||
* // b() == 4
|
||||
* // bClone() == 4
|
||||
* ```
|
||||
*/
|
||||
raw(): SignalValue<TValue>;
|
||||
}
|
||||
|
||||
export interface Signal<TValue, TReturn = void>
|
||||
extends SignalSetter<TValue, TReturn>,
|
||||
SignalGetter<TValue>,
|
||||
SignalTween<TValue>,
|
||||
SignalUtils<TReturn> {}
|
||||
SignalUtils<TValue, TReturn> {}
|
||||
|
||||
const collectionStack: DependencyContext[] = [];
|
||||
|
||||
@@ -178,6 +203,10 @@ export function createSignal<TValue, TReturn = void>(
|
||||
value: () => set(get()),
|
||||
});
|
||||
|
||||
Object.defineProperty(handler, 'raw', {
|
||||
value: () => current,
|
||||
});
|
||||
|
||||
if (initial !== undefined) {
|
||||
set(initial);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user