mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-11 14:57:56 -05:00
feat: switch to signals (#64)
The base Node class now uses signals for most functionalities: Parent and child relations between nodes are represented by signals. Layout changes are propagated through signals. It makes it possible to access the computed layout right after changing it. Previously: ```ts rect.layout.height(100); yield; rect.layout.height(200); console.log(rect.height()); // 100 ``` Now: ```ts rect.layout.height(100); yield; rect.layout.height(200); console.log(rect.height()); // 200 ``` A new signal primitive called `Computed` has been added. It behaves like a signal except it can only be set once - during initialization. A corresponding `@computed` method decorator has been added. It turns the given method into a `Computed` value. Properties marked with the `@property` decorator can now implement custom getters, setters and tween providers.
This commit is contained in:
@@ -16,23 +16,18 @@ export class Circle extends Node<CircleProps> {
|
||||
context.save();
|
||||
this.transformContext(context);
|
||||
|
||||
const width = this.width();
|
||||
const height = this.height();
|
||||
|
||||
context.save();
|
||||
context.fillStyle = this.fill();
|
||||
context.beginPath();
|
||||
context.ellipse(
|
||||
0,
|
||||
0,
|
||||
this.width() / 2,
|
||||
this.height() / 2,
|
||||
0,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
);
|
||||
context.ellipse(0, 0, width / 2, height / 2, 0, 0, Math.PI * 2);
|
||||
context.closePath();
|
||||
context.fill();
|
||||
context.restore();
|
||||
|
||||
for (const child of this.children) {
|
||||
for (const child of this.children()) {
|
||||
child.render(context);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
import {compoundProperty, initialize, property} from '../decorators';
|
||||
import {Vector2} from '@motion-canvas/core/lib/types';
|
||||
import {Reference, Signal, useSignal} from '@motion-canvas/core/lib/utils';
|
||||
import {vector2dLerp} from '@motion-canvas/core/lib/tweening';
|
||||
import {Layout, LayoutMode, LayoutProps} from '../layout';
|
||||
import {compound, computed, initialize, property} from '../decorators';
|
||||
import {
|
||||
Vector2,
|
||||
transformPoint,
|
||||
transformAngle,
|
||||
Rect,
|
||||
Size,
|
||||
} from '@motion-canvas/core/lib/types';
|
||||
import {
|
||||
createSignal,
|
||||
isReactive,
|
||||
Reference,
|
||||
Signal,
|
||||
SignalValue,
|
||||
} from '@motion-canvas/core/lib/utils';
|
||||
import {map, vector2dLerp, sizeLerp} from '@motion-canvas/core/lib/tweening';
|
||||
import {Layout, LayoutProps} from '../layout';
|
||||
import {ComponentChild, ComponentChildren} from './types';
|
||||
import {threadable} from '@motion-canvas/core/lib/decorators';
|
||||
import {ThreadGenerator} from '@motion-canvas/core/lib/threading';
|
||||
|
||||
export interface NodeProps {
|
||||
ref?: Reference<Node>;
|
||||
@@ -13,11 +27,14 @@ export interface NodeProps {
|
||||
position?: Vector2;
|
||||
width?: number;
|
||||
height?: number;
|
||||
size?: Size;
|
||||
rotation?: number;
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
offset?: Vector2;
|
||||
scaleX?: number;
|
||||
scaleY?: number;
|
||||
scale?: Vector2;
|
||||
layout?: LayoutProps;
|
||||
}
|
||||
|
||||
@@ -28,15 +45,101 @@ export class Node<TProps extends NodeProps = NodeProps> {
|
||||
|
||||
@property(0)
|
||||
public declare readonly x: Signal<number, this>;
|
||||
protected readonly customX = createSignal(0, map, this);
|
||||
|
||||
protected getX(): number {
|
||||
const mode = this.realMode();
|
||||
const parent = this.parent();
|
||||
|
||||
if (mode === 'enabled' && parent) {
|
||||
const parentLayout = parent.computedLayout();
|
||||
const thisLayout = this.computedLayout();
|
||||
|
||||
const offsetX = (thisLayout.width / 2) * this.offsetX();
|
||||
|
||||
return (
|
||||
thisLayout.x -
|
||||
parentLayout.x -
|
||||
(parentLayout.width - thisLayout.width) / 2 +
|
||||
offsetX
|
||||
);
|
||||
} else {
|
||||
return this.customX();
|
||||
}
|
||||
}
|
||||
|
||||
protected setX(value: SignalValue<number>) {
|
||||
this.customX(value);
|
||||
}
|
||||
|
||||
@property(0)
|
||||
public declare readonly y: Signal<number, this>;
|
||||
protected readonly customY = createSignal(0, map, this);
|
||||
|
||||
protected getY(): number {
|
||||
const mode = this.realMode();
|
||||
const parent = this.parent();
|
||||
|
||||
if (mode === 'enabled' && parent) {
|
||||
const parentLayout = parent.computedLayout();
|
||||
const thisLayout = this.computedLayout();
|
||||
|
||||
const offsetX = (thisLayout.width / 2) * this.offsetX();
|
||||
|
||||
return (
|
||||
thisLayout.y -
|
||||
parentLayout.y -
|
||||
(parentLayout.height - thisLayout.height) / 2 +
|
||||
offsetX
|
||||
);
|
||||
} else {
|
||||
return this.customY();
|
||||
}
|
||||
}
|
||||
|
||||
protected setY(value: SignalValue<number>) {
|
||||
this.customY(value);
|
||||
}
|
||||
|
||||
@property(0)
|
||||
public declare readonly width: Signal<number, this>;
|
||||
protected readonly customWidth = createSignal(0, map, this);
|
||||
|
||||
protected getWidth(): number {
|
||||
const mode = this.realMode();
|
||||
|
||||
if (mode === 'disabled') {
|
||||
return this.customWidth();
|
||||
} else {
|
||||
return this.computedLayout().width;
|
||||
}
|
||||
}
|
||||
|
||||
protected setWidth(value: SignalValue<number>) {
|
||||
this.customWidth(value);
|
||||
}
|
||||
|
||||
@property(0)
|
||||
public declare readonly height: Signal<number, this>;
|
||||
protected readonly customHeight = createSignal(0, map, this);
|
||||
|
||||
protected getHeight(): number {
|
||||
const mode = this.realMode();
|
||||
|
||||
if (mode === 'disabled') {
|
||||
return this.customHeight();
|
||||
} else {
|
||||
return this.computedLayout().height;
|
||||
}
|
||||
}
|
||||
|
||||
protected setHeight(value: SignalValue<number>) {
|
||||
this.customHeight(value);
|
||||
}
|
||||
|
||||
@compound(['width', 'height'])
|
||||
@property(undefined, sizeLerp)
|
||||
public declare readonly size: Signal<Size, this>;
|
||||
|
||||
@property(0)
|
||||
public declare readonly rotation: Signal<number, this>;
|
||||
@@ -47,23 +150,81 @@ export class Node<TProps extends NodeProps = NodeProps> {
|
||||
@property(1)
|
||||
public declare readonly scaleY: Signal<number, this>;
|
||||
|
||||
@compound({x: 'scaleX', y: 'scaleY'})
|
||||
@property(undefined, vector2dLerp)
|
||||
public declare readonly scale: Signal<Vector2, this>;
|
||||
|
||||
@property(0)
|
||||
public declare readonly offsetX: Signal<number, this>;
|
||||
|
||||
@property(0)
|
||||
public declare readonly offsetY: Signal<number, this>;
|
||||
|
||||
@compoundProperty(['x', 'y'], vector2dLerp)
|
||||
@compound({x: 'offsetX', y: 'offsetY'})
|
||||
@property(undefined, vector2dLerp)
|
||||
public declare readonly offset: Signal<Vector2, this>;
|
||||
|
||||
@compound(['x', 'y'])
|
||||
@property(undefined, vector2dLerp)
|
||||
public declare readonly position: Signal<Vector2, this>;
|
||||
|
||||
public readonly absolutePosition = useSignal(() => {
|
||||
const matrix = this.globalMatrix();
|
||||
return {x: matrix.e, y: matrix.f};
|
||||
});
|
||||
@property(undefined, vector2dLerp)
|
||||
public declare readonly absolutePosition: Signal<Vector2, this>;
|
||||
|
||||
protected readonly globalMatrix = useSignal(() => this.localMatrix());
|
||||
protected getAbsolutePosition() {
|
||||
const matrix = this.localToWorld();
|
||||
return {x: matrix.m41, y: matrix.m42};
|
||||
}
|
||||
|
||||
protected readonly localMatrix = useSignal(() => {
|
||||
protected setAbsolutePosition(value: SignalValue<Vector2>) {
|
||||
if (isReactive(value)) {
|
||||
this.position(() => transformPoint(value(), this.worldToLocal()));
|
||||
} else {
|
||||
this.position(transformPoint(value, this.worldToLocal()));
|
||||
}
|
||||
}
|
||||
|
||||
@property()
|
||||
public declare readonly absoluteRotation: Signal<Vector2, this>;
|
||||
|
||||
protected getAbsoluteRotation() {
|
||||
const matrix = this.localToWorld();
|
||||
return (Math.atan2(matrix.m12, matrix.m11) * 180) / Math.PI;
|
||||
}
|
||||
|
||||
protected setAbsoluteRotation(value: SignalValue<number>) {
|
||||
if (isReactive(value)) {
|
||||
this.rotation(() => transformAngle(value(), this.worldToLocal()));
|
||||
} else {
|
||||
this.rotation(transformAngle(value, this.worldToLocal()));
|
||||
}
|
||||
}
|
||||
|
||||
protected children = createSignal<Node[]>([]);
|
||||
protected parent = createSignal<Node | null>(null);
|
||||
|
||||
public constructor({children, layout, ...rest}: TProps) {
|
||||
this.layout = new Layout(layout ?? {});
|
||||
initialize(this, {defaults: rest});
|
||||
this.append(children);
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected localToWorld(): DOMMatrix {
|
||||
const parent = this.parent();
|
||||
return parent
|
||||
? parent.localToWorld().multiply(this.localToParent())
|
||||
: new DOMMatrix();
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected worldToLocal(): DOMMatrix {
|
||||
const parent = this.parent();
|
||||
return parent ? parent.localToWorld().inverse() : new DOMMatrix();
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected localToParent(): DOMMatrix {
|
||||
const matrix = new DOMMatrix();
|
||||
matrix.translateSelf(this.x(), this.y());
|
||||
matrix.rotateSelf(0, 0, this.rotation());
|
||||
@@ -74,15 +235,51 @@ export class Node<TProps extends NodeProps = NodeProps> {
|
||||
);
|
||||
|
||||
return matrix;
|
||||
});
|
||||
}
|
||||
|
||||
protected children: Node[] = [];
|
||||
protected parent: Node | null = null;
|
||||
@computed()
|
||||
protected computedLayout(): Rect {
|
||||
this.requestLayoutUpdate();
|
||||
const rect = this.layout.getComputedLayout();
|
||||
return {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
};
|
||||
}
|
||||
|
||||
public constructor({children, layout, ...rest}: TProps) {
|
||||
initialize(this, {defaults: rest});
|
||||
this.layout = new Layout(layout ?? {});
|
||||
this.append(children);
|
||||
/**
|
||||
* Find the closest layout root and apply any new layout changes.
|
||||
*/
|
||||
@computed()
|
||||
protected requestLayoutUpdate() {
|
||||
const mode = this.layout.mode();
|
||||
const parent = this.parent();
|
||||
if (mode === 'disabled' || mode === 'root' || !parent) {
|
||||
this.updateLayout();
|
||||
} else {
|
||||
parent.requestLayoutUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply any new layout changes to this node and its children.
|
||||
*/
|
||||
@computed()
|
||||
protected updateLayout() {
|
||||
this.applyLayoutChanges();
|
||||
this.layout.apply();
|
||||
for (const child of this.children()) {
|
||||
child.updateLayout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply any custom layout changes to this node.
|
||||
*/
|
||||
protected applyLayoutChanges() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public append(node: ComponentChildren) {
|
||||
@@ -99,46 +296,34 @@ export class Node<TProps extends NodeProps = NodeProps> {
|
||||
}
|
||||
|
||||
protected moveTo(parent: Node | null) {
|
||||
if (this.parent === parent) {
|
||||
const current = this.parent();
|
||||
if (current === parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.parent) {
|
||||
this.globalMatrix(() => this.localMatrix());
|
||||
this.parent.layout.element.removeChild(this.layout.element);
|
||||
this.parent.children = this.parent.children.filter(
|
||||
child => child !== this,
|
||||
);
|
||||
this.parent = null;
|
||||
if (current) {
|
||||
current.layout.element.removeChild(this.layout.element);
|
||||
current.children(current.children().filter(child => child !== this));
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
this.globalMatrix(() =>
|
||||
parent.globalMatrix().multiply(this.localMatrix()),
|
||||
);
|
||||
parent.layout.element.append(this.layout.element);
|
||||
parent.children.push(this);
|
||||
this.parent = parent;
|
||||
parent.children([...parent.children(), this]);
|
||||
}
|
||||
|
||||
this.parent(parent);
|
||||
}
|
||||
|
||||
public removeChildren() {
|
||||
for (const node of this.children) {
|
||||
for (const node of this.children()) {
|
||||
node.moveTo(null);
|
||||
}
|
||||
}
|
||||
|
||||
public updateLayout(): boolean {
|
||||
let isDirty = this.layout.updateIfNecessary();
|
||||
|
||||
for (const child of this.children) {
|
||||
isDirty ||= child.updateLayout();
|
||||
}
|
||||
|
||||
return isDirty;
|
||||
}
|
||||
|
||||
public handleLayoutChange(parentMode?: LayoutMode) {
|
||||
@computed()
|
||||
public realMode() {
|
||||
const parent = this.parent();
|
||||
const parentMode = parent?.realMode();
|
||||
let mode = this.layout.mode();
|
||||
|
||||
if (mode === null) {
|
||||
@@ -149,45 +334,14 @@ export class Node<TProps extends NodeProps = NodeProps> {
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === 'enabled' && this.parent) {
|
||||
//TODO Cache this call or pass it as an argument
|
||||
const parentLayout = this.parent.layout.getComputedLayout();
|
||||
const thisLayout = this.layout.getComputedLayout();
|
||||
|
||||
const offsetX = (thisLayout.width / 2) * this.offsetX();
|
||||
const offsetY = (thisLayout.height / 2) * this.offsetY();
|
||||
|
||||
this.x(
|
||||
thisLayout.x -
|
||||
parentLayout.x -
|
||||
(parentLayout.width - thisLayout.width) / 2 +
|
||||
offsetX,
|
||||
);
|
||||
this.y(
|
||||
thisLayout.y -
|
||||
parentLayout.y -
|
||||
(parentLayout.height - thisLayout.height) / 2 +
|
||||
offsetY,
|
||||
);
|
||||
this.width(thisLayout.width);
|
||||
this.height(thisLayout.height);
|
||||
}
|
||||
if (mode === 'pop' || mode === 'root') {
|
||||
const thisLayout = this.layout.getComputedLayout();
|
||||
this.width(thisLayout.width);
|
||||
this.height(thisLayout.height);
|
||||
}
|
||||
|
||||
for (const child of this.children) {
|
||||
child.handleLayoutChange(mode);
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
public render(context: CanvasRenderingContext2D) {
|
||||
context.save();
|
||||
this.transformContext(context);
|
||||
|
||||
for (const child of this.children) {
|
||||
for (const child of this.children()) {
|
||||
child.render(context);
|
||||
}
|
||||
|
||||
@@ -195,7 +349,7 @@ export class Node<TProps extends NodeProps = NodeProps> {
|
||||
}
|
||||
|
||||
protected transformContext(context: CanvasRenderingContext2D) {
|
||||
const matrix = this.localMatrix();
|
||||
const matrix = this.localToParent();
|
||||
context.transform(
|
||||
matrix.a,
|
||||
matrix.b,
|
||||
@@ -205,6 +359,26 @@ export class Node<TProps extends NodeProps = NodeProps> {
|
||||
matrix.f,
|
||||
);
|
||||
}
|
||||
|
||||
@threadable()
|
||||
public *tweenSize(update: (node: this) => void): ThreadGenerator {
|
||||
const mode = this.realMode();
|
||||
const initialSize = this.size();
|
||||
|
||||
if (mode === 'disabled') {
|
||||
update(this);
|
||||
const toSize = this.size();
|
||||
this.size(initialSize);
|
||||
yield* this.size(toSize, 2);
|
||||
} else {
|
||||
this.layout.width(null).height(null);
|
||||
update(this);
|
||||
const toSize = this.size();
|
||||
this.layout.size(initialSize);
|
||||
yield* this.layout.size(toSize, 2);
|
||||
this.layout.width(null).height(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*@__PURE__*/
|
||||
|
||||
@@ -24,7 +24,7 @@ export class Rect extends Node<RectProps> {
|
||||
context.fillRect(-width / 2, -height / 2, width, height);
|
||||
context.restore();
|
||||
|
||||
for (const child of this.children) {
|
||||
for (const child of this.children()) {
|
||||
child.render(context);
|
||||
}
|
||||
|
||||
|
||||
78
packages/2d/src/decorators/compound.ts
Normal file
78
packages/2d/src/decorators/compound.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import {SignalValue, isReactive} from '@motion-canvas/core/lib/utils';
|
||||
import {capitalize} from './property';
|
||||
import {addInitializer} from './initializers';
|
||||
|
||||
/**
|
||||
* Create a compound property decorator.
|
||||
*
|
||||
* @remarks
|
||||
* This decorator generates a getter and setter for a compound property.
|
||||
* These methods can then be used by the {@link property} decorator to create
|
||||
* a signal that acts as a shortcut for accessing multiple other signals.
|
||||
*
|
||||
* Both the getter and setter operate on an object whose properties correspond
|
||||
* to individual signals.
|
||||
* For example, `\@property(['x', 'y'])` will operate on an object of type
|
||||
* `{x: number, y: number}`. Here, the `x` property can retrieve or set the
|
||||
* value of the `this.x` signal.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* class Example {
|
||||
* \@property(1)
|
||||
* public declare readonly scaleX: Signal<number, this>;
|
||||
*
|
||||
* \@property(1)
|
||||
* public declare readonly scaleY: Signal<number, this>;
|
||||
*
|
||||
* \@compound({x: 'scaleX', y: 'scaleY'})
|
||||
* \@property(undefined, vector2dLerp)
|
||||
* public declare readonly scale: Signal<Vector2, this>;
|
||||
*
|
||||
* public setScale() {
|
||||
* this.scale({x: 7, y: 3});
|
||||
* // same as:
|
||||
* this.scaleX(7).scaleY(3);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param mapping - An array of signals to turn into a compound property or a
|
||||
* record mapping the property in the compound object to the
|
||||
* corresponding signal.
|
||||
*/
|
||||
export function compound(
|
||||
mapping: string[] | Record<string, string>,
|
||||
): PropertyDecorator {
|
||||
return (target: any, key) => {
|
||||
const entries = Array.isArray(mapping)
|
||||
? mapping.map(key => [key, key])
|
||||
: Object.entries(mapping);
|
||||
|
||||
target.constructor.prototype[`get${capitalize(key.toString())}`] =
|
||||
function () {
|
||||
return Object.fromEntries(
|
||||
entries.map(([key, property]) => [key, this[property]()]),
|
||||
);
|
||||
};
|
||||
|
||||
target.constructor.prototype[`set${capitalize(key.toString())}`] =
|
||||
function set(value: SignalValue<any>) {
|
||||
if (isReactive(value)) {
|
||||
for (const [key, property] of entries) {
|
||||
this[property](() => value()[key]);
|
||||
}
|
||||
} else {
|
||||
for (const [key, property] of entries) {
|
||||
this[property](value[key]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
addInitializer(target, (instance: any, context: any) => {
|
||||
if (key in context.defaults) {
|
||||
instance[key](context.defaults[key]);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import {
|
||||
deepLerp,
|
||||
easeInOutCubic,
|
||||
TimingFunction,
|
||||
tween,
|
||||
InterpolationFunction,
|
||||
} from '@motion-canvas/core/lib/tweening';
|
||||
import {addInitializer} from './initializers';
|
||||
import {Signal, SignalValue, isReactive} from '@motion-canvas/core/lib/utils';
|
||||
|
||||
type SignalRecord<T extends keyof any> = {
|
||||
[P in T]: Signal<any, any>;
|
||||
};
|
||||
|
||||
type CompoundValue<T extends keyof any> = {
|
||||
[P in T]: any;
|
||||
};
|
||||
|
||||
export function createCompoundProperty<
|
||||
TProperties extends keyof TNode,
|
||||
TNode extends SignalRecord<TProperties>,
|
||||
TValue extends CompoundValue<TProperties> = CompoundValue<TProperties>,
|
||||
>(
|
||||
node: TNode,
|
||||
propertyKeys: TProperties[],
|
||||
initial?: TValue,
|
||||
defaultInterpolation: InterpolationFunction<TValue> = deepLerp,
|
||||
): Signal<TValue, TNode> {
|
||||
const handler = <Signal<TValue, TNode>>(
|
||||
function (
|
||||
newValue?: SignalValue<TValue>,
|
||||
duration?: number,
|
||||
timingFunction: TimingFunction = easeInOutCubic,
|
||||
interpolationFunction: InterpolationFunction<TValue> = defaultInterpolation,
|
||||
) {
|
||||
if (duration !== undefined && newValue !== undefined) {
|
||||
const from = <TValue>(
|
||||
Object.fromEntries(propertyKeys.map(key => [key, node[key]()]))
|
||||
);
|
||||
|
||||
return tween(duration, value => {
|
||||
const interpolatedValue = interpolationFunction(
|
||||
from,
|
||||
isReactive(newValue) ? newValue() : newValue,
|
||||
timingFunction(value),
|
||||
);
|
||||
for (const key of propertyKeys) {
|
||||
node[key](interpolatedValue[key]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (newValue !== undefined) {
|
||||
if (typeof newValue === 'function') {
|
||||
for (const key of propertyKeys) {
|
||||
node[key](() => newValue()[key]);
|
||||
}
|
||||
} else {
|
||||
for (const key of propertyKeys) {
|
||||
node[key](newValue[key]);
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
return Object.fromEntries(propertyKeys.map(key => [key, node[key]()]));
|
||||
}
|
||||
);
|
||||
|
||||
if (initial !== undefined) {
|
||||
handler(initial);
|
||||
}
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
export function compoundProperty(
|
||||
keys: string[],
|
||||
mapper?: InterpolationFunction<any>,
|
||||
): PropertyDecorator {
|
||||
return (target: any, key) => {
|
||||
addInitializer(target, (instance: any, context: any) => {
|
||||
instance[key] = createCompoundProperty(
|
||||
instance,
|
||||
keys,
|
||||
context.defaults[key],
|
||||
mapper ?? deepLerp,
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
18
packages/2d/src/decorators/computed.ts
Normal file
18
packages/2d/src/decorators/computed.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {addInitializer} from './initializers';
|
||||
import {createComputed} from '@motion-canvas/core/lib/utils/createComputed';
|
||||
|
||||
/**
|
||||
* Create a computed method decorator.
|
||||
*
|
||||
* @remarks
|
||||
* This decorator turns the given method into a computed value.
|
||||
* See {@link createComputed} for more information.
|
||||
*/
|
||||
export function computed(): MethodDecorator {
|
||||
return (target: any, key) => {
|
||||
const method = target[key];
|
||||
addInitializer(target, (instance: any) => {
|
||||
instance[key] = createComputed(method.bind(instance));
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './compoundProperty';
|
||||
export * from './initializers';
|
||||
export * from './computed';
|
||||
export * from './compound';
|
||||
export * from './property';
|
||||
export * from './initializers';
|
||||
|
||||
@@ -1,17 +1,176 @@
|
||||
import {InterpolationFunction, map} from '@motion-canvas/core/lib/tweening';
|
||||
import {useSignal} from '@motion-canvas/core/lib/utils';
|
||||
import {
|
||||
deepLerp,
|
||||
easeInOutCubic,
|
||||
TimingFunction,
|
||||
tween,
|
||||
InterpolationFunction,
|
||||
} from '@motion-canvas/core/lib/tweening';
|
||||
import {addInitializer} from './initializers';
|
||||
import {
|
||||
Signal,
|
||||
SignalValue,
|
||||
isReactive,
|
||||
createSignal,
|
||||
SignalGetter,
|
||||
SignalSetter,
|
||||
SignalTween,
|
||||
} from '@motion-canvas/core/lib/utils';
|
||||
|
||||
export function capitalize<T extends string>(value: T): Capitalize<T> {
|
||||
return <Capitalize<T>>(value[0].toUpperCase() + value.slice(1));
|
||||
}
|
||||
|
||||
export type PropertyOwner<TValue> = {
|
||||
[key: `get${Capitalize<string>}`]: SignalGetter<TValue> | undefined;
|
||||
[key: `set${Capitalize<string>}`]: SignalSetter<TValue> | undefined;
|
||||
[key: `tween${Capitalize<string>}`]: SignalTween<TValue> | undefined;
|
||||
};
|
||||
|
||||
export function createProperty<
|
||||
TValue,
|
||||
TNode extends PropertyOwner<TValue>,
|
||||
TProperty extends string & keyof TNode,
|
||||
>(
|
||||
node: TNode,
|
||||
property: TProperty,
|
||||
initial?: TValue,
|
||||
defaultInterpolation: InterpolationFunction<TValue> = deepLerp,
|
||||
): Signal<TValue, TNode> {
|
||||
let getter: () => TValue;
|
||||
let setter: (value: SignalValue<TValue>) => void;
|
||||
|
||||
const originalGetter = node[`get${capitalize(property)}`];
|
||||
const originalSetter = node[`set${capitalize(property)}`];
|
||||
const tweener = node[`tween${capitalize(property)}`];
|
||||
|
||||
if (!originalGetter !== !originalSetter) {
|
||||
console.warn(
|
||||
`The "${property}" property needs to provide either both the setter and getter or none of them`,
|
||||
);
|
||||
}
|
||||
|
||||
let signal: Signal<TValue, TNode> | null = null;
|
||||
if (!originalGetter || !originalSetter) {
|
||||
signal = createSignal(initial, defaultInterpolation, node);
|
||||
if (!tweener) {
|
||||
return signal;
|
||||
}
|
||||
|
||||
getter = signal;
|
||||
setter = signal;
|
||||
} else {
|
||||
getter = originalGetter.bind(node);
|
||||
setter = originalSetter.bind(node);
|
||||
}
|
||||
|
||||
const handler = <Signal<TValue, TNode>>(
|
||||
function (
|
||||
newValue?: SignalValue<TValue>,
|
||||
duration?: number,
|
||||
timingFunction: TimingFunction = easeInOutCubic,
|
||||
interpolationFunction: InterpolationFunction<TValue> = defaultInterpolation,
|
||||
) {
|
||||
if (newValue === undefined) {
|
||||
return getter();
|
||||
}
|
||||
|
||||
if (duration === undefined) {
|
||||
return setter(newValue);
|
||||
}
|
||||
|
||||
if (tweener) {
|
||||
return tweener.call(
|
||||
node,
|
||||
newValue,
|
||||
duration,
|
||||
timingFunction,
|
||||
interpolationFunction,
|
||||
);
|
||||
}
|
||||
|
||||
const from = getter();
|
||||
return tween(duration, value => {
|
||||
setter(
|
||||
interpolationFunction(
|
||||
from,
|
||||
isReactive(newValue) ? newValue() : newValue,
|
||||
timingFunction(value),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
Object.defineProperty(handler, 'reset', {
|
||||
value: signal
|
||||
? () => signal?.reset()
|
||||
: initial !== undefined
|
||||
? () => setter(initial)
|
||||
: () => node,
|
||||
});
|
||||
|
||||
Object.defineProperty(handler, 'save', {
|
||||
value: () => setter(getter()),
|
||||
});
|
||||
|
||||
if (initial !== undefined && originalSetter) {
|
||||
setter(initial);
|
||||
}
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a signal property decorator.
|
||||
*
|
||||
* @remarks
|
||||
* This decorator turns the given property into a signal.
|
||||
*
|
||||
* The class using this decorator can implement the following methods:
|
||||
* - `get[PropertyName]` - A property getter.
|
||||
* - `get[PropertyName]` - A property setter.
|
||||
* - `tween[PropertyName]` - a tween provider.
|
||||
*
|
||||
* See the {@link PropertyOwner} type for more detailed method signatures.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* class Example {
|
||||
* \@property()
|
||||
* public declare color: Signal<Color, this>;
|
||||
*
|
||||
* \@customProperty()
|
||||
* public declare colorString: Signal<string, this>;
|
||||
*
|
||||
* protected getColorString() {
|
||||
* return this.color().toString();
|
||||
* }
|
||||
*
|
||||
* protected setColorString(value: SignalValue<string>) {
|
||||
* this.color(
|
||||
* isReactive(value)
|
||||
* ? () => new Color(value())
|
||||
* : new Color(value)
|
||||
* );
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param initial - An option initial value of the property.
|
||||
* @param interpolationFunction - The default function used to interpolate
|
||||
* between values.
|
||||
*/
|
||||
export function property<T>(
|
||||
initial?: T,
|
||||
mapper?: InterpolationFunction<T>,
|
||||
interpolationFunction?: InterpolationFunction<T>,
|
||||
): PropertyDecorator {
|
||||
return (target: any, key) => {
|
||||
addInitializer(target, (instance: any, context: any) => {
|
||||
instance[key] = useSignal(
|
||||
context.defaults[key] ?? initial,
|
||||
mapper ?? map,
|
||||
instance[key] = createProperty(
|
||||
instance,
|
||||
<string>key,
|
||||
context.defaults[key] ?? initial,
|
||||
interpolationFunction ?? deepLerp,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ export function jsx(
|
||||
config: JSXProps,
|
||||
): ComponentChildren {
|
||||
const {ref, children, ...rest} = config;
|
||||
const flatChildren = Array.isArray(children) ? children.flat() : [children];
|
||||
const flatChildren = Array.isArray(children) ? children.flat() : children;
|
||||
|
||||
if (type === Fragment) {
|
||||
return flatChildren;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {initialize, property} from '../decorators';
|
||||
import {Signal, useSignal} from '@motion-canvas/core/lib/utils';
|
||||
import {Rect} from '@motion-canvas/core/lib/types';
|
||||
import {compound, computed, initialize, property} from '../decorators';
|
||||
import {Signal} from '@motion-canvas/core/lib/utils';
|
||||
import {Rect, Size} from '@motion-canvas/core/lib/types';
|
||||
import {AlignItems, FlexDirection, JustifyContent, LayoutMode} from './types';
|
||||
import {sizeLerp} from '@motion-canvas/core/lib/tweening';
|
||||
|
||||
export interface LayoutProps {
|
||||
mode?: LayoutMode;
|
||||
@@ -11,6 +12,10 @@ export interface LayoutProps {
|
||||
marginBottom?: number;
|
||||
marginLeft?: number;
|
||||
marginRight?: number;
|
||||
paddingTop?: number;
|
||||
paddingBottom?: number;
|
||||
paddingLeft?: number;
|
||||
paddingRight?: number;
|
||||
direction?: FlexDirection;
|
||||
justifyContent?: JustifyContent;
|
||||
alignItems?: AlignItems;
|
||||
@@ -21,9 +26,13 @@ export class Layout {
|
||||
@property(null)
|
||||
public declare readonly mode: Signal<LayoutMode, this>;
|
||||
@property(null)
|
||||
public declare readonly width: Signal<number | `${number}%`, this>;
|
||||
public declare readonly width: Signal<null | number | `${number}%`, this>;
|
||||
@property(null)
|
||||
public declare readonly height: Signal<number | `${number}%`, this>;
|
||||
public declare readonly height: Signal<null | number | `${number}%`, this>;
|
||||
@property(undefined, sizeLerp)
|
||||
@compound(['width', 'height'])
|
||||
public declare readonly size: Signal<Size, this>;
|
||||
|
||||
@property(0)
|
||||
public declare readonly marginTop: Signal<number, this>;
|
||||
@property(0)
|
||||
@@ -32,6 +41,16 @@ export class Layout {
|
||||
public declare readonly marginLeft: Signal<number, this>;
|
||||
@property(0)
|
||||
public declare readonly marginRight: Signal<number, this>;
|
||||
|
||||
@property(0)
|
||||
public declare readonly paddingTop: Signal<number, this>;
|
||||
@property(0)
|
||||
public declare readonly paddingBottom: Signal<number, this>;
|
||||
@property(0)
|
||||
public declare readonly paddingLeft: Signal<number, this>;
|
||||
@property(0)
|
||||
public declare readonly paddingRight: Signal<number, this>;
|
||||
|
||||
@property('row')
|
||||
public declare readonly direction: Signal<FlexDirection, this>;
|
||||
@property('none')
|
||||
@@ -43,16 +62,12 @@ export class Layout {
|
||||
|
||||
public readonly element: HTMLDivElement;
|
||||
|
||||
private isDirty = true;
|
||||
|
||||
public constructor(props: LayoutProps) {
|
||||
this.element = document.createElement('div');
|
||||
this.element.style.display = 'flex';
|
||||
this.element.style.boxSizing = 'border-box';
|
||||
|
||||
initialize(this, {defaults: props});
|
||||
this.update.onChanged.subscribe(() => {
|
||||
this.isDirty = true;
|
||||
});
|
||||
}
|
||||
|
||||
public toPixels(value: number) {
|
||||
@@ -69,17 +84,8 @@ export class Layout {
|
||||
};
|
||||
}
|
||||
|
||||
public updateIfNecessary(): boolean {
|
||||
if (this.isDirty) {
|
||||
this.update();
|
||||
this.isDirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private readonly update = useSignal(() => {
|
||||
@computed()
|
||||
public apply() {
|
||||
const mode = this.mode();
|
||||
this.element.style.position =
|
||||
mode === 'disabled' || mode === 'root' ? 'absolute' : '';
|
||||
@@ -106,9 +112,13 @@ export class Layout {
|
||||
this.element.style.marginBottom = this.toPixels(this.marginBottom());
|
||||
this.element.style.marginLeft = this.toPixels(this.marginLeft());
|
||||
this.element.style.marginRight = this.toPixels(this.marginRight());
|
||||
this.element.style.paddingTop = this.toPixels(this.paddingTop());
|
||||
this.element.style.paddingBottom = this.toPixels(this.paddingBottom());
|
||||
this.element.style.paddingLeft = this.toPixels(this.paddingLeft());
|
||||
this.element.style.paddingRight = this.toPixels(this.paddingRight());
|
||||
this.element.style.flexDirection = this.direction();
|
||||
this.element.style.aspectRatio = this.ratio();
|
||||
this.element.style.justifyContent = this.justifyContent();
|
||||
this.element.style.alignItems = this.alignItems();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,6 @@ export class TwoDScene extends GeneratorScene<TwoDView> {
|
||||
return this.view;
|
||||
}
|
||||
|
||||
public update() {
|
||||
this.view.updateLayout();
|
||||
}
|
||||
|
||||
public render(
|
||||
context: CanvasRenderingContext2D,
|
||||
canvas: HTMLCanvasElement,
|
||||
|
||||
@@ -14,8 +14,8 @@ export class TwoDView extends Node<any> {
|
||||
frame.id = TwoDView.frameID;
|
||||
frame.style.position = 'absolute';
|
||||
frame.style.pointerEvents = 'none';
|
||||
frame.style.top = '0';
|
||||
frame.style.left = '0';
|
||||
frame.style.right = '0';
|
||||
frame.style.opacity = '0';
|
||||
frame.style.border = 'none';
|
||||
|
||||
@@ -27,19 +27,11 @@ export class TwoDView extends Node<any> {
|
||||
}
|
||||
}
|
||||
|
||||
public updateLayout(): boolean {
|
||||
let isDirty = super.updateLayout();
|
||||
let limit = 10;
|
||||
while (isDirty && limit > 0) {
|
||||
limit--;
|
||||
this.handleLayoutChange();
|
||||
isDirty = super.updateLayout();
|
||||
}
|
||||
|
||||
if (limit === 0) {
|
||||
console.warn('Layout iteration limit exceeded');
|
||||
}
|
||||
|
||||
return false;
|
||||
public override render(context: CanvasRenderingContext2D) {
|
||||
this.x();
|
||||
this.y();
|
||||
this.width();
|
||||
this.height();
|
||||
super.render(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Color from 'colorjs.io';
|
||||
import type {Rect, Vector2, PossibleSpacing, Spacing} from '../types';
|
||||
import type {Rect, Vector2, PossibleSpacing, Spacing, Size} from '../types';
|
||||
|
||||
export interface InterpolationFunction<T, Rest extends unknown[] = unknown[]> {
|
||||
(from: T, to: T, value: number, ...args: Rest): T;
|
||||
@@ -162,6 +162,13 @@ export function vector2dLerp(from: Vector2, to: Vector2, value: number) {
|
||||
};
|
||||
}
|
||||
|
||||
export function sizeLerp(from: Size, to: Size, value: number) {
|
||||
return {
|
||||
width: map(from.width, to.width, value),
|
||||
height: map(from.height, to.height, value),
|
||||
};
|
||||
}
|
||||
|
||||
export function spacingLerp(
|
||||
from: Spacing,
|
||||
to: Spacing,
|
||||
|
||||
24
packages/core/src/types/Matrix.ts
Normal file
24
packages/core/src/types/Matrix.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {Vector2} from './Vector';
|
||||
|
||||
export function transformPoint(vector: Vector2, matrix: DOMMatrix) {
|
||||
return {
|
||||
x: vector.x * matrix.m11 + vector.y * matrix.m21 + matrix.m41,
|
||||
y: vector.x * matrix.m12 + vector.y * matrix.m22 + matrix.m42,
|
||||
};
|
||||
}
|
||||
|
||||
export function transformVector(vector: Vector2, matrix: DOMMatrix) {
|
||||
return {
|
||||
x: vector.x * matrix.m11 + vector.y * matrix.m21,
|
||||
y: vector.x * matrix.m12 + vector.y * matrix.m22,
|
||||
};
|
||||
}
|
||||
|
||||
export function transformAngle(angle: number, matrix: DOMMatrix) {
|
||||
const radians = (angle / 180) * Math.PI;
|
||||
const vector = transformVector(
|
||||
{x: Math.cos(radians), y: Math.sin(radians)},
|
||||
matrix,
|
||||
);
|
||||
return (Math.atan2(vector.y, vector.x) * 180) / Math.PI;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './Canvas';
|
||||
export * from './Matrix';
|
||||
export * from './Origin';
|
||||
export * from './Rect';
|
||||
export * from './Size';
|
||||
|
||||
43
packages/core/src/utils/createComputed.test.ts
Normal file
43
packages/core/src/utils/createComputed.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import {createSignal} from './createSignal';
|
||||
import {createComputed} from './createComputed';
|
||||
|
||||
describe('createComputed()', () => {
|
||||
test('Value is updated when its dependencies change', () => {
|
||||
const a = createSignal(1);
|
||||
const b = createSignal(true);
|
||||
const c = createSignal(2);
|
||||
const d = createComputed(() => (b() ? a() : c()));
|
||||
|
||||
expect(d()).toBe(1);
|
||||
|
||||
a(3);
|
||||
|
||||
expect(d()).toBe(3);
|
||||
|
||||
b(false);
|
||||
|
||||
expect(d()).toBe(2);
|
||||
|
||||
c(4);
|
||||
|
||||
expect(d()).toBe(4);
|
||||
});
|
||||
|
||||
test('Value is cached and recalculated only when necessary', () => {
|
||||
const a = createSignal(1);
|
||||
|
||||
const value = jest.fn(() => a() * 2);
|
||||
const c = createComputed(value);
|
||||
|
||||
expect(value.mock.calls.length).toBe(0);
|
||||
|
||||
a(2);
|
||||
|
||||
expect(value.mock.calls.length).toBe(0);
|
||||
|
||||
c();
|
||||
c();
|
||||
|
||||
expect(value.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
38
packages/core/src/utils/createComputed.ts
Normal file
38
packages/core/src/utils/createComputed.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import {FlagDispatcher, Subscribable} from '../events';
|
||||
import {
|
||||
collect,
|
||||
finishCollecting,
|
||||
SignalGetter,
|
||||
startCollecting,
|
||||
} from './createSignal';
|
||||
|
||||
export type Computed<TValue> = SignalGetter<TValue>;
|
||||
|
||||
export function createComputed<TValue>(
|
||||
factory: SignalGetter<TValue>,
|
||||
): Computed<TValue> {
|
||||
let last: TValue;
|
||||
const dependencies = new Set<Subscribable<void>>();
|
||||
const event = new FlagDispatcher();
|
||||
|
||||
function markDirty() {
|
||||
event.raise();
|
||||
}
|
||||
|
||||
const handler = <Computed<TValue>>function handler() {
|
||||
if (event.isRaised()) {
|
||||
dependencies.forEach(dep => dep.unsubscribe(markDirty));
|
||||
dependencies.clear();
|
||||
startCollecting([dependencies, markDirty]);
|
||||
last = factory();
|
||||
finishCollecting([dependencies, markDirty]);
|
||||
}
|
||||
event.reset();
|
||||
collect(event.subscribable);
|
||||
return last;
|
||||
};
|
||||
|
||||
markDirty();
|
||||
|
||||
return handler;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import {useSignal} from './useSignal';
|
||||
import {createSignal} from './createSignal';
|
||||
|
||||
describe('useSignal()', () => {
|
||||
describe('createSignal()', () => {
|
||||
test('Works correctly with plain values', () => {
|
||||
const signal = useSignal(7);
|
||||
const signal = createSignal(7);
|
||||
|
||||
expect(signal()).toBe(7);
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('useSignal()', () => {
|
||||
});
|
||||
|
||||
test('Works correctly with computed values', () => {
|
||||
const signal = useSignal(() => 7);
|
||||
const signal = createSignal(() => 7);
|
||||
|
||||
expect(signal()).toBe(7);
|
||||
|
||||
@@ -22,10 +22,10 @@ describe('useSignal()', () => {
|
||||
});
|
||||
|
||||
test('Value is updated when its dependencies change', () => {
|
||||
const a = useSignal(1);
|
||||
const b = useSignal(true);
|
||||
const c = useSignal(2);
|
||||
const d = useSignal(() => (b() ? a() : c()));
|
||||
const a = createSignal(1);
|
||||
const b = createSignal(true);
|
||||
const c = createSignal(2);
|
||||
const d = createSignal(() => (b() ? a() : c()));
|
||||
|
||||
expect(d()).toBe(1);
|
||||
|
||||
@@ -43,10 +43,10 @@ describe('useSignal()', () => {
|
||||
});
|
||||
|
||||
test('Value is cached and recalculated only when necessary', () => {
|
||||
const a = useSignal(1);
|
||||
const a = createSignal(1);
|
||||
|
||||
const value = jest.fn(() => a() * 2);
|
||||
const c = useSignal(value);
|
||||
const c = createSignal(value);
|
||||
|
||||
expect(value.mock.calls.length).toBe(0);
|
||||
|
||||
@@ -59,19 +59,4 @@ describe('useSignal()', () => {
|
||||
|
||||
expect(value.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
test('onChanged events are dispatched only once per handler', () => {
|
||||
const handler = jest.fn();
|
||||
const c = useSignal(0);
|
||||
|
||||
c(1);
|
||||
|
||||
c.onChanged.subscribe(handler);
|
||||
expect(handler.mock.calls.length).toBe(1);
|
||||
|
||||
c(2);
|
||||
c(3);
|
||||
|
||||
expect(handler.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -12,16 +12,56 @@ type DependencyContext = [Set<Subscribable<void>>, EventHandler<void>];
|
||||
|
||||
export type SignalValue<TValue> = TValue | (() => TValue);
|
||||
|
||||
export interface Signal<TValue, TReturn = void> {
|
||||
(): TValue;
|
||||
export interface SignalSetter<TValue, TReturn = void> {
|
||||
(value: SignalValue<TValue>): TReturn;
|
||||
}
|
||||
|
||||
export interface SignalGetter<TValue> {
|
||||
(): TValue;
|
||||
}
|
||||
|
||||
export interface SignalTween<TValue> {
|
||||
(
|
||||
value: SignalValue<TValue>,
|
||||
time: number,
|
||||
timingFunction?: TimingFunction,
|
||||
interpolationFunction?: InterpolationFunction<TValue>,
|
||||
): ThreadGenerator;
|
||||
get onChanged(): Subscribable<void>;
|
||||
}
|
||||
|
||||
export interface Signal<TValue, TReturn = void>
|
||||
extends SignalSetter<TValue, TReturn>,
|
||||
SignalGetter<TValue>,
|
||||
SignalTween<TValue> {
|
||||
/**
|
||||
* Reset the signal to its initial value (if one has been set).
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const signal = createSignal(7);
|
||||
*
|
||||
* signal.reset();
|
||||
* // same as:
|
||||
* signal(7);
|
||||
* ```
|
||||
*/
|
||||
reset(): TReturn;
|
||||
|
||||
/**
|
||||
* Compute the current value of the signal and immediately set it.
|
||||
*
|
||||
* @remarks
|
||||
* This method can be used to stop the signal from updating while keeping its
|
||||
* current value.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* signal.save();
|
||||
* // same as:
|
||||
* signal(signal());
|
||||
* ```
|
||||
*/
|
||||
save(): TReturn;
|
||||
}
|
||||
|
||||
const collectionStack: DependencyContext[] = [];
|
||||
@@ -48,7 +88,7 @@ export function isReactive<T>(value: SignalValue<T>): value is () => T {
|
||||
return typeof value === 'function';
|
||||
}
|
||||
|
||||
export function useSignal<TValue, TReturn = void>(
|
||||
export function createSignal<TValue, TReturn = void>(
|
||||
initial?: SignalValue<TValue>,
|
||||
defaultInterpolation: InterpolationFunction<TValue> = deepLerp,
|
||||
setterReturn?: TReturn,
|
||||
@@ -60,7 +100,7 @@ export function useSignal<TValue, TReturn = void>(
|
||||
|
||||
function set(value: SignalValue<TValue>) {
|
||||
if (current === value) {
|
||||
return;
|
||||
return setterReturn;
|
||||
}
|
||||
|
||||
current = value;
|
||||
@@ -74,10 +114,14 @@ export function useSignal<TValue, TReturn = void>(
|
||||
if (!isReactive(value)) {
|
||||
last = value;
|
||||
}
|
||||
|
||||
return setterReturn;
|
||||
}
|
||||
|
||||
function get(): TValue {
|
||||
if (event.isRaised() && isReactive(current)) {
|
||||
dependencies.forEach(dep => dep.unsubscribe(markDirty));
|
||||
dependencies.clear();
|
||||
startCollecting([dependencies, markDirty]);
|
||||
last = current();
|
||||
finishCollecting([dependencies, markDirty]);
|
||||
@@ -98,18 +142,14 @@ export function useSignal<TValue, TReturn = void>(
|
||||
timingFunction: TimingFunction = easeInOutCubic,
|
||||
interpolationFunction: InterpolationFunction<TValue> = defaultInterpolation,
|
||||
) {
|
||||
// Getter
|
||||
if (value === undefined) {
|
||||
return get();
|
||||
}
|
||||
|
||||
// Setter
|
||||
if (duration === undefined) {
|
||||
set(value);
|
||||
return setterReturn;
|
||||
return set(value);
|
||||
}
|
||||
|
||||
// Tween
|
||||
const from = get();
|
||||
return tween(duration, v => {
|
||||
set(
|
||||
@@ -123,12 +163,16 @@ export function useSignal<TValue, TReturn = void>(
|
||||
}
|
||||
);
|
||||
|
||||
Object.defineProperty(handler, 'onChanged', {
|
||||
value: event.subscribable,
|
||||
Object.defineProperty(handler, 'reset', {
|
||||
value: initial !== undefined ? () => set(initial) : () => setterReturn,
|
||||
});
|
||||
|
||||
Object.defineProperty(handler, 'save', {
|
||||
value: () => set(get()),
|
||||
});
|
||||
|
||||
if (initial !== undefined) {
|
||||
handler(initial);
|
||||
set(initial);
|
||||
}
|
||||
|
||||
return handler;
|
||||
@@ -1,8 +1,9 @@
|
||||
export * from './createComputed';
|
||||
export * from './createSignal';
|
||||
export * from './useAnimator';
|
||||
export * from './useProject';
|
||||
export * from './useRef';
|
||||
export * from './useScene';
|
||||
export * from './useSignal';
|
||||
export * from './useThread';
|
||||
export * from './useTime';
|
||||
export * from './useContext';
|
||||
|
||||
Reference in New Issue
Block a user