mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-12 15:28:03 -05:00
feat: add basic transform to Node class (#83)
- The Node class now has a basic position, rotation and scale controls. - Compound properties have been changed to make the code less repetitive. The signals making up a compound property are now nested within it: ```ts // before node.scaleX(); // now node.scale.x(); ``` Which means that they don't need to be redeclared each time a compound property is used.
This commit is contained in:
@@ -3,20 +3,23 @@ import {
|
||||
compound,
|
||||
computed,
|
||||
initial,
|
||||
Property,
|
||||
inspectable,
|
||||
property,
|
||||
Vector2LengthProperty,
|
||||
Vector2Property,
|
||||
vector2Property,
|
||||
wrapper,
|
||||
} from '../decorators';
|
||||
import {
|
||||
Origin,
|
||||
PossibleSpacing,
|
||||
Rect,
|
||||
Spacing,
|
||||
transformAngle,
|
||||
Vector2,
|
||||
originToOffset,
|
||||
SerializedVector2,
|
||||
PossibleVector2,
|
||||
} from '@motion-canvas/core/lib/types';
|
||||
import {isReactive, Signal, SignalValue} from '@motion-canvas/core/lib/utils';
|
||||
import {createSignal, Signal, SignalValue} from '@motion-canvas/core/lib/utils';
|
||||
import {
|
||||
InterpolationFunction,
|
||||
TimingFunction,
|
||||
@@ -36,6 +39,7 @@ import {ThreadGenerator} from '@motion-canvas/core/lib/threading';
|
||||
import {Node, NodeProps} from './Node';
|
||||
import {View2D} from '../scenes';
|
||||
import {drawLine, lineTo} from '../utils';
|
||||
import {spacingProperty, SpacingProperty} from '../decorators/spacingProperty';
|
||||
|
||||
export interface LayoutProps extends NodeProps {
|
||||
layout?: LayoutMode;
|
||||
@@ -81,17 +85,10 @@ export interface LayoutProps extends NodeProps {
|
||||
letterSpacing?: SignalValue<number>;
|
||||
textWrap?: SignalValue<boolean>;
|
||||
|
||||
x?: SignalValue<number>;
|
||||
y?: SignalValue<number>;
|
||||
position?: SignalValue<Vector2>;
|
||||
size?: SignalValue<Vector2>;
|
||||
rotation?: SignalValue<number>;
|
||||
size?: SignalValue<PossibleVector2>;
|
||||
offsetX?: SignalValue<number>;
|
||||
offsetY?: SignalValue<number>;
|
||||
offset?: SignalValue<Vector2>;
|
||||
scaleX?: SignalValue<number>;
|
||||
scaleY?: SignalValue<number>;
|
||||
scale?: SignalValue<Vector2>;
|
||||
offset?: SignalValue<PossibleVector2>;
|
||||
clip?: SignalValue<boolean>;
|
||||
}
|
||||
|
||||
@@ -116,49 +113,11 @@ export class Layout extends Node {
|
||||
@property()
|
||||
public declare readonly ratio: Signal<number | null, this>;
|
||||
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly marginTop: Signal<number, this>;
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly marginBottom: Signal<number, this>;
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly marginLeft: Signal<number, this>;
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly marginRight: Signal<number, this>;
|
||||
@compound({
|
||||
top: 'marginTop',
|
||||
bottom: 'marginBottom',
|
||||
left: 'marginLeft',
|
||||
right: 'marginRight',
|
||||
})
|
||||
@wrapper(Spacing)
|
||||
@property()
|
||||
public declare readonly margin: Property<PossibleSpacing, Spacing, this>;
|
||||
@spacingProperty('margin')
|
||||
public declare readonly margin: SpacingProperty<this>;
|
||||
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly paddingTop: Signal<number, this>;
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly paddingBottom: Signal<number, this>;
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly paddingLeft: Signal<number, this>;
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly paddingRight: Signal<number, this>;
|
||||
@compound({
|
||||
top: 'paddingTop',
|
||||
bottom: 'paddingBottom',
|
||||
left: 'paddingLeft',
|
||||
right: 'paddingRight',
|
||||
})
|
||||
@wrapper(Spacing)
|
||||
@property()
|
||||
public declare readonly padding: Property<PossibleSpacing, Spacing, this>;
|
||||
@spacingProperty('padding')
|
||||
public declare readonly padding: SpacingProperty<this>;
|
||||
|
||||
@initial('row')
|
||||
@property()
|
||||
@@ -214,12 +173,10 @@ export class Layout extends Node {
|
||||
@property()
|
||||
public declare readonly textWrap: Signal<boolean | null, this>;
|
||||
|
||||
@initial(0)
|
||||
@cloneable(false)
|
||||
@inspectable(false)
|
||||
@property()
|
||||
protected declare readonly customX: Signal<number, this>;
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly x: Signal<number, this>;
|
||||
protected getX(): number {
|
||||
if (this.isLayoutRoot()) {
|
||||
return this.customX();
|
||||
@@ -231,12 +188,11 @@ export class Layout extends Node {
|
||||
this.customX(value);
|
||||
}
|
||||
|
||||
@initial(0)
|
||||
@cloneable(false)
|
||||
@inspectable(false)
|
||||
@property()
|
||||
protected declare readonly customY: Signal<number, this>;
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly y: Signal<number, this>;
|
||||
|
||||
protected getY(): number {
|
||||
if (this.isLayoutRoot()) {
|
||||
return this.customY();
|
||||
@@ -248,12 +204,6 @@ export class Layout extends Node {
|
||||
this.customY(value);
|
||||
}
|
||||
|
||||
@initial(null)
|
||||
@property()
|
||||
protected declare readonly customWidth: Signal<Length, this>;
|
||||
@initial(null)
|
||||
@property()
|
||||
public declare readonly width: Property<Length, number, this>;
|
||||
protected getWidth(): number {
|
||||
return this.computedSize().width;
|
||||
}
|
||||
@@ -272,34 +222,28 @@ export class Layout extends Node {
|
||||
const lock = typeof width !== 'number' || typeof value !== 'number';
|
||||
let from: number;
|
||||
if (lock) {
|
||||
from = this.width();
|
||||
from = this.size.x();
|
||||
} else {
|
||||
from = width;
|
||||
}
|
||||
|
||||
let to: number;
|
||||
if (lock) {
|
||||
this.width(value);
|
||||
to = this.width();
|
||||
this.size.x(value);
|
||||
to = this.size.x();
|
||||
} else {
|
||||
to = value;
|
||||
}
|
||||
|
||||
this.width(from);
|
||||
this.size.x(from);
|
||||
lock && this.lockSize();
|
||||
yield* tween(time, value =>
|
||||
this.width(interpolationFunction(from, to, timingFunction(value))),
|
||||
this.size.x(interpolationFunction(from, to, timingFunction(value))),
|
||||
);
|
||||
this.width(value);
|
||||
this.size.x(value);
|
||||
lock && this.releaseSize();
|
||||
}
|
||||
|
||||
@initial(null)
|
||||
@property()
|
||||
protected declare readonly customHeight: Signal<Length, this>;
|
||||
@initial(null)
|
||||
@property()
|
||||
public declare readonly height: Property<Length, number, this>;
|
||||
protected getHeight(): number {
|
||||
return this.computedSize().height;
|
||||
}
|
||||
@@ -319,55 +263,57 @@ export class Layout extends Node {
|
||||
|
||||
let from: number;
|
||||
if (lock) {
|
||||
from = this.height();
|
||||
from = this.size.y();
|
||||
} else {
|
||||
from = height;
|
||||
}
|
||||
|
||||
let to: number;
|
||||
if (lock) {
|
||||
this.height(value);
|
||||
to = this.height();
|
||||
this.size.y(value);
|
||||
to = this.size.y();
|
||||
} else {
|
||||
to = value;
|
||||
}
|
||||
|
||||
this.height(from);
|
||||
this.size.y(from);
|
||||
lock && this.lockSize();
|
||||
yield* tween(time, value =>
|
||||
this.height(interpolationFunction(from, to, timingFunction(value))),
|
||||
this.size.y(interpolationFunction(from, to, timingFunction(value))),
|
||||
);
|
||||
this.height(value);
|
||||
this.size.y(value);
|
||||
lock && this.releaseSize();
|
||||
}
|
||||
|
||||
@compound({x: 'width', y: 'height'})
|
||||
@cloneable(false)
|
||||
@wrapper(Vector2)
|
||||
@property()
|
||||
public declare readonly size: Property<
|
||||
{width: Length; height: Length},
|
||||
Vector2,
|
||||
this
|
||||
>;
|
||||
@compound({x: 'width', y: 'height'})
|
||||
public declare readonly size: Vector2LengthProperty<this>;
|
||||
|
||||
@inspectable(false)
|
||||
@property()
|
||||
protected declare readonly customWidth: Signal<Length, this>;
|
||||
@inspectable(false)
|
||||
@property()
|
||||
protected declare readonly customHeight: Signal<Length, this>;
|
||||
@computed()
|
||||
protected customSize(): {width: Length; height: Length} {
|
||||
protected customSize(): SerializedVector2<Length> {
|
||||
return {
|
||||
width: this.customWidth(),
|
||||
height: this.customHeight(),
|
||||
x: this.customWidth(),
|
||||
y: this.customHeight(),
|
||||
};
|
||||
}
|
||||
|
||||
@threadable()
|
||||
protected *tweenSize(
|
||||
value: SignalValue<{width: Length; height: Length}>,
|
||||
value: SignalValue<SerializedVector2<Length>>,
|
||||
time: number,
|
||||
timingFunction: TimingFunction,
|
||||
interpolationFunction: InterpolationFunction<Vector2>,
|
||||
): ThreadGenerator {
|
||||
const size = this.customSize();
|
||||
let from: Vector2;
|
||||
if (typeof size.height !== 'number' || typeof size.width !== 'number') {
|
||||
if (typeof size.x !== 'number' || typeof size.y !== 'number') {
|
||||
from = this.size();
|
||||
} else {
|
||||
from = <Vector2>size;
|
||||
@@ -376,8 +322,8 @@ export class Layout extends Node {
|
||||
let to: Vector2;
|
||||
if (
|
||||
typeof value === 'object' &&
|
||||
typeof value.height === 'number' &&
|
||||
typeof value.width === 'number'
|
||||
typeof value.x === 'number' &&
|
||||
typeof value.y === 'number'
|
||||
) {
|
||||
to = <Vector2>value;
|
||||
} else {
|
||||
@@ -394,101 +340,8 @@ export class Layout extends Node {
|
||||
this.size(value);
|
||||
}
|
||||
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly rotation: Signal<number, this>;
|
||||
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly offsetX: Signal<number, this>;
|
||||
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly offsetY: Signal<number, this>;
|
||||
|
||||
@compound({x: 'offsetX', y: 'offsetY'})
|
||||
@wrapper(Vector2)
|
||||
@property()
|
||||
public declare readonly offset: Signal<Vector2, this>;
|
||||
|
||||
@initial(1)
|
||||
@property()
|
||||
public declare readonly scaleX: Signal<number, this>;
|
||||
|
||||
@initial(1)
|
||||
@property()
|
||||
public declare readonly scaleY: Signal<number, this>;
|
||||
|
||||
@compound({x: 'scaleX', y: 'scaleY'})
|
||||
@wrapper(Vector2)
|
||||
@property()
|
||||
public declare readonly scale: Signal<Vector2, this>;
|
||||
|
||||
@wrapper(Vector2)
|
||||
@cloneable(false)
|
||||
@property()
|
||||
public declare readonly absoluteScale: Signal<Vector2, this>;
|
||||
|
||||
protected getAbsoluteScale(): Vector2 {
|
||||
const matrix = this.localToWorld();
|
||||
return new Vector2(
|
||||
Vector2.magnitude(matrix.m11, matrix.m12),
|
||||
Vector2.magnitude(matrix.m21, matrix.m22),
|
||||
);
|
||||
}
|
||||
|
||||
protected setAbsoluteScale(value: SignalValue<Vector2>) {
|
||||
if (isReactive(value)) {
|
||||
this.scale(() => this.getRelativeScale(value()));
|
||||
} else {
|
||||
this.scale(this.getRelativeScale(value));
|
||||
}
|
||||
}
|
||||
|
||||
private getRelativeScale(scale: Vector2): Vector2 {
|
||||
const parentScale = this.parentTransform()?.absoluteScale() ?? Vector2.one;
|
||||
return scale.div(parentScale);
|
||||
}
|
||||
|
||||
@compound(['x', 'y'])
|
||||
@wrapper(Vector2)
|
||||
@property()
|
||||
public declare readonly position: Signal<Vector2, this>;
|
||||
|
||||
@wrapper(Vector2)
|
||||
@cloneable(false)
|
||||
@property()
|
||||
public declare readonly absolutePosition: Signal<Vector2, this>;
|
||||
|
||||
protected getAbsolutePosition(): Vector2 {
|
||||
const matrix = this.localToWorld();
|
||||
return new Vector2(matrix.m41, matrix.m42);
|
||||
}
|
||||
|
||||
protected setAbsolutePosition(value: SignalValue<Vector2>) {
|
||||
if (isReactive(value)) {
|
||||
this.position(() => value().transformAsPoint(this.worldToParent()));
|
||||
} else {
|
||||
this.position(value.transformAsPoint(this.worldToParent()));
|
||||
}
|
||||
}
|
||||
|
||||
@cloneable(false)
|
||||
@property()
|
||||
public declare readonly absoluteRotation: Signal<number, 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.worldToParent()));
|
||||
} else {
|
||||
this.rotation(transformAngle(value, this.worldToParent()));
|
||||
}
|
||||
}
|
||||
@vector2Property('offset')
|
||||
public declare readonly offset: Vector2Property<this>;
|
||||
|
||||
@initial(false)
|
||||
@property()
|
||||
@@ -497,9 +350,7 @@ export class Layout extends Node {
|
||||
public readonly element: HTMLElement;
|
||||
public readonly styles: CSSStyleDeclaration;
|
||||
|
||||
@initial(0)
|
||||
@property()
|
||||
protected declare readonly sizeLockCounter: Signal<number, this>;
|
||||
protected readonly sizeLockCounter = createSignal(0);
|
||||
|
||||
public constructor({tagName = 'div', ...props}: LayoutProps) {
|
||||
super(props);
|
||||
@@ -561,14 +412,11 @@ export class Layout extends Node {
|
||||
|
||||
public override localToParent(): DOMMatrix {
|
||||
const matrix = new DOMMatrix();
|
||||
const size = this.computedSize();
|
||||
matrix.translateSelf(this.x(), this.y());
|
||||
const offset = this.size().mul(this.offset()).scale(-0.5);
|
||||
matrix.translateSelf(this.position.x(), this.position.y());
|
||||
matrix.rotateSelf(0, 0, this.rotation());
|
||||
matrix.scaleSelf(this.scaleX(), this.scaleY());
|
||||
matrix.translateSelf(
|
||||
(size.width / -2) * this.offsetX(),
|
||||
(size.height / -2) * this.offsetY(),
|
||||
);
|
||||
matrix.scaleSelf(this.scale.x(), this.scale.y());
|
||||
matrix.translateSelf(offset.x, offset.y);
|
||||
|
||||
return matrix;
|
||||
}
|
||||
@@ -583,8 +431,8 @@ export class Layout extends Node {
|
||||
const rect = this.getComputedLayout();
|
||||
|
||||
const position = new Vector2(
|
||||
rect.x + (rect.width / 2) * this.offsetX(),
|
||||
rect.y + (rect.height / 2) * this.offsetY(),
|
||||
rect.x + (rect.width / 2) * this.offset.x(),
|
||||
rect.y + (rect.height / 2) * this.offset.y(),
|
||||
);
|
||||
|
||||
const parent = this.parentTransform();
|
||||
@@ -626,23 +474,29 @@ export class Layout extends Node {
|
||||
this.applyFont();
|
||||
this.applyFlex();
|
||||
if (this.layoutEnabled()) {
|
||||
this.syncDOM();
|
||||
const children = this.layoutChildren();
|
||||
for (const child of children) {
|
||||
child.updateLayout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected syncDOM() {
|
||||
protected layoutChildren(): Layout[] {
|
||||
this.element.innerText = '';
|
||||
const queue = [...this.children()];
|
||||
const result: Layout[] = [];
|
||||
while (queue.length) {
|
||||
const child = queue.shift();
|
||||
if (child instanceof Layout) {
|
||||
this.element.append(child.element);
|
||||
child.updateLayout();
|
||||
result.push(child);
|
||||
} else if (child) {
|
||||
queue.push(...child.children());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -774,15 +628,15 @@ export class Layout extends Node {
|
||||
this.element.style.minWidth = this.parseLength(this.minWidth());
|
||||
this.element.style.aspectRatio = this.parseValue(this.ratio());
|
||||
|
||||
this.element.style.marginTop = this.parsePixels(this.marginTop());
|
||||
this.element.style.marginBottom = this.parsePixels(this.marginBottom());
|
||||
this.element.style.marginLeft = this.parsePixels(this.marginLeft());
|
||||
this.element.style.marginRight = this.parsePixels(this.marginRight());
|
||||
this.element.style.marginTop = this.parsePixels(this.margin.top());
|
||||
this.element.style.marginBottom = this.parsePixels(this.margin.bottom());
|
||||
this.element.style.marginLeft = this.parsePixels(this.margin.left());
|
||||
this.element.style.marginRight = this.parsePixels(this.margin.right());
|
||||
|
||||
this.element.style.paddingTop = this.parsePixels(this.paddingTop());
|
||||
this.element.style.paddingBottom = this.parsePixels(this.paddingBottom());
|
||||
this.element.style.paddingLeft = this.parsePixels(this.paddingLeft());
|
||||
this.element.style.paddingRight = this.parsePixels(this.paddingRight());
|
||||
this.element.style.paddingTop = this.parsePixels(this.padding.top());
|
||||
this.element.style.paddingBottom = this.parsePixels(this.padding.bottom());
|
||||
this.element.style.paddingLeft = this.parsePixels(this.padding.left());
|
||||
this.element.style.paddingRight = this.parsePixels(this.padding.right());
|
||||
|
||||
this.element.style.flexDirection = this.direction();
|
||||
this.element.style.flexBasis = this.parseLength(this.basis());
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import {
|
||||
compound,
|
||||
cloneable,
|
||||
ColorProperty,
|
||||
colorProperty,
|
||||
computed,
|
||||
getPropertiesOf,
|
||||
initial,
|
||||
initialize,
|
||||
Property,
|
||||
property,
|
||||
Vector2Property,
|
||||
vector2Property,
|
||||
wrapper,
|
||||
} from '../decorators';
|
||||
import {
|
||||
@@ -13,7 +17,8 @@ import {
|
||||
Rect,
|
||||
transformScalar,
|
||||
PossibleColor,
|
||||
Color,
|
||||
transformAngle,
|
||||
PossibleVector2,
|
||||
} from '@motion-canvas/core/lib/types';
|
||||
import {
|
||||
createSignal,
|
||||
@@ -32,6 +37,15 @@ import {drawLine} from '../utils';
|
||||
export interface NodeProps {
|
||||
ref?: Reference<any>;
|
||||
children?: ComponentChildren;
|
||||
|
||||
x?: SignalValue<number>;
|
||||
y?: SignalValue<number>;
|
||||
position?: SignalValue<PossibleVector2>;
|
||||
rotation?: SignalValue<number>;
|
||||
scaleX?: SignalValue<number>;
|
||||
scaleY?: SignalValue<number>;
|
||||
scale?: SignalValue<PossibleVector2>;
|
||||
|
||||
opacity?: SignalValue<number>;
|
||||
blur?: SignalValue<number>;
|
||||
brightness?: SignalValue<number>;
|
||||
@@ -45,7 +59,7 @@ export interface NodeProps {
|
||||
shadowBlur?: SignalValue<number>;
|
||||
shadowOffsetX?: SignalValue<number>;
|
||||
shadowOffsetY?: SignalValue<number>;
|
||||
shadowOffset?: SignalValue<Vector2>;
|
||||
shadowOffset?: SignalValue<PossibleVector2>;
|
||||
cache?: SignalValue<boolean>;
|
||||
composite?: SignalValue<boolean>;
|
||||
compositeOperation?: SignalValue<GlobalCompositeOperation>;
|
||||
@@ -54,6 +68,86 @@ export interface NodeProps {
|
||||
export class Node implements Promisable<Node> {
|
||||
public declare isClass: boolean;
|
||||
|
||||
@vector2Property()
|
||||
public declare readonly position: Vector2Property<this>;
|
||||
|
||||
@wrapper(Vector2)
|
||||
@cloneable(false)
|
||||
@property()
|
||||
public declare readonly absolutePosition: Property<
|
||||
PossibleVector2,
|
||||
Vector2,
|
||||
this
|
||||
>;
|
||||
|
||||
protected getAbsolutePosition(): Vector2 {
|
||||
const matrix = this.localToWorld();
|
||||
return new Vector2(matrix.m41, matrix.m42);
|
||||
}
|
||||
|
||||
protected setAbsolutePosition(value: SignalValue<Vector2>) {
|
||||
if (isReactive(value)) {
|
||||
this.position(() => value().transformAsPoint(this.worldToParent()));
|
||||
} else {
|
||||
this.position(value.transformAsPoint(this.worldToParent()));
|
||||
}
|
||||
}
|
||||
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly rotation: Signal<number, this>;
|
||||
|
||||
@cloneable(false)
|
||||
@property()
|
||||
public declare readonly absoluteRotation: Signal<number, 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.worldToParent()));
|
||||
} else {
|
||||
this.rotation(transformAngle(value, this.worldToParent()));
|
||||
}
|
||||
}
|
||||
|
||||
@initial(Vector2.one)
|
||||
@vector2Property('scale')
|
||||
public declare readonly scale: Vector2Property<this>;
|
||||
|
||||
@wrapper(Vector2)
|
||||
@cloneable(false)
|
||||
@property()
|
||||
public declare readonly absoluteScale: Property<
|
||||
PossibleVector2,
|
||||
Vector2,
|
||||
this
|
||||
>;
|
||||
|
||||
protected getAbsoluteScale(): Vector2 {
|
||||
const matrix = this.localToWorld();
|
||||
return new Vector2(
|
||||
Vector2.magnitude(matrix.m11, matrix.m12),
|
||||
Vector2.magnitude(matrix.m21, matrix.m22),
|
||||
);
|
||||
}
|
||||
|
||||
protected setAbsoluteScale(value: SignalValue<Vector2>) {
|
||||
if (isReactive(value)) {
|
||||
this.scale(() => this.getRelativeScale(value()));
|
||||
} else {
|
||||
this.scale(this.getRelativeScale(value));
|
||||
}
|
||||
}
|
||||
|
||||
private getRelativeScale(scale: Vector2): Vector2 {
|
||||
const parentScale = this.parent()?.absoluteScale() ?? Vector2.one;
|
||||
return scale.div(parentScale);
|
||||
}
|
||||
|
||||
@initial(false)
|
||||
@property()
|
||||
public declare readonly cache: Signal<boolean, this>;
|
||||
@@ -131,26 +225,15 @@ export class Node implements Promisable<Node> {
|
||||
public declare readonly sepia: Signal<number, this>;
|
||||
|
||||
@initial('#0000')
|
||||
@wrapper(Color)
|
||||
@property()
|
||||
public declare readonly shadowColor: Property<PossibleColor, Color, this>;
|
||||
@colorProperty()
|
||||
public declare readonly shadowColor: ColorProperty<this>;
|
||||
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly shadowBlur: Signal<number, this>;
|
||||
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly shadowOffsetX: Signal<number, this>;
|
||||
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly shadowOffsetY: Signal<number, this>;
|
||||
|
||||
@compound({x: 'shadowOffsetX', y: 'shadowOffsetY'})
|
||||
@wrapper(Vector2)
|
||||
@property()
|
||||
public declare readonly shadowOffset: Signal<Vector2, this>;
|
||||
@vector2Property('shadowOffset')
|
||||
public declare readonly shadowOffset: Vector2Property<this>;
|
||||
|
||||
@computed()
|
||||
protected hasFilters() {
|
||||
@@ -171,8 +254,8 @@ export class Node implements Promisable<Node> {
|
||||
return (
|
||||
!!this.shadowColor() &&
|
||||
(this.shadowBlur() > 0 ||
|
||||
this.shadowOffsetX() !== 0 ||
|
||||
this.shadowOffsetY() !== 0)
|
||||
this.shadowOffset.x() !== 0 ||
|
||||
this.shadowOffset.y() !== 0)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -223,6 +306,9 @@ export class Node implements Promisable<Node> {
|
||||
|
||||
public constructor({children, ...rest}: NodeProps) {
|
||||
initialize(this, {defaults: rest});
|
||||
for (const {signal} of this) {
|
||||
signal.reset();
|
||||
}
|
||||
this.add(children);
|
||||
this.key = use2DView()?.registerNode(this) ?? '';
|
||||
}
|
||||
@@ -247,7 +333,12 @@ export class Node implements Promisable<Node> {
|
||||
|
||||
@computed()
|
||||
public localToParent(): DOMMatrix {
|
||||
return new DOMMatrix();
|
||||
const matrix = new DOMMatrix();
|
||||
matrix.translateSelf(this.position.x(), this.position.y());
|
||||
matrix.rotateSelf(0, 0, this.rotation());
|
||||
matrix.scaleSelf(this.scale.x(), this.scale.y());
|
||||
|
||||
return matrix;
|
||||
}
|
||||
|
||||
@computed()
|
||||
@@ -434,37 +525,40 @@ export class Node implements Promisable<Node> {
|
||||
props.children ??= this.children().map(child => child.clone());
|
||||
}
|
||||
|
||||
for (const key in this.properties) {
|
||||
const meta = this.properties[key];
|
||||
for (const {key, meta, signal} of this) {
|
||||
if (!meta.cloneable || key in props) continue;
|
||||
|
||||
const signal = (<Record<string, Signal<any>>>(<unknown>this))[key];
|
||||
props[key] = signal();
|
||||
if (meta.compound) {
|
||||
for (const [key, property] of meta.compoundEntries) {
|
||||
props[property] = (<Record<string, Signal<any>>>(<unknown>signal))[
|
||||
key
|
||||
].raw();
|
||||
}
|
||||
} else {
|
||||
props[key] = signal.raw();
|
||||
}
|
||||
}
|
||||
|
||||
return this.instantiate(props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a raw copy of this node.
|
||||
* Create a copy of this node.
|
||||
*
|
||||
* @remarks
|
||||
* A raw copy preserves any reactive properties from the source node.
|
||||
* Unlike {@link clone}, a snapshot clone calculates any reactive properties
|
||||
* at the moment of cloning and passes the raw values to the copy.
|
||||
*
|
||||
* @param customProps - Properties to override.
|
||||
*/
|
||||
public rawClone(customProps: NodeProps = {}): this {
|
||||
public snapshotClone(customProps: NodeProps = {}): this {
|
||||
const props: NodeProps & Record<string, any> = {...customProps};
|
||||
if (this.children().length > 0) {
|
||||
props.children ??= this.children().map(child => child.rawClone());
|
||||
props.children ??= this.children().map(child => child.snapshotClone());
|
||||
}
|
||||
|
||||
for (const key in this.properties) {
|
||||
const meta = this.properties[key];
|
||||
for (const {key, meta, signal} of this) {
|
||||
if (!meta.cloneable || key in props) continue;
|
||||
|
||||
const signal = (<Record<string, Signal<any>>>(<unknown>this))[key];
|
||||
props[key] = signal.raw();
|
||||
props[key] = signal();
|
||||
}
|
||||
|
||||
return this.instantiate(props);
|
||||
@@ -485,11 +579,8 @@ export class Node implements Promisable<Node> {
|
||||
props.children ??= this.children().map(child => child.reactiveClone());
|
||||
}
|
||||
|
||||
for (const key in this.properties) {
|
||||
const meta = this.properties[key];
|
||||
for (const {key, meta, signal} of this) {
|
||||
if (!meta.cloneable || key in props) continue;
|
||||
|
||||
const signal = (<Record<string, Signal<any>>>(<unknown>this))[key];
|
||||
props[key] = () => signal();
|
||||
}
|
||||
|
||||
@@ -779,6 +870,14 @@ export class Node implements Promisable<Node> {
|
||||
await this.waitForAsyncResources();
|
||||
return this;
|
||||
}
|
||||
|
||||
public *[Symbol.iterator]() {
|
||||
for (const key in this.properties) {
|
||||
const meta = this.properties[key];
|
||||
const signal = (<Record<string, Signal<any>>>(<unknown>this))[key];
|
||||
yield {meta, signal, key};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*@__PURE__*/
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import {CanvasStyle, PossibleCanvasStyle} from '../partials';
|
||||
import {computed, initial, parser, Property, property} from '../decorators';
|
||||
import {PossibleCanvasStyle} from '../partials';
|
||||
import {
|
||||
computed,
|
||||
initial,
|
||||
property,
|
||||
CanvasStyleProperty,
|
||||
canvasStyleProperty,
|
||||
} from '../decorators';
|
||||
import {createSignal, Signal, SignalValue} from '@motion-canvas/core/lib/utils';
|
||||
import {Rect} from '@motion-canvas/core/lib/types';
|
||||
import {Layout, LayoutProps} from './Layout';
|
||||
import {threadable} from '@motion-canvas/core/lib/decorators';
|
||||
import {easeOutExpo, linear, map} from '@motion-canvas/core/lib/tweening';
|
||||
import {resolveCanvasStyle, canvasStyleParser} from '../utils';
|
||||
import {resolveCanvasStyle} from '../utils';
|
||||
|
||||
export interface ShapeProps extends LayoutProps {
|
||||
fill?: SignalValue<PossibleCanvasStyle>;
|
||||
@@ -19,22 +25,10 @@ export interface ShapeProps extends LayoutProps {
|
||||
}
|
||||
|
||||
export abstract class Shape extends Layout {
|
||||
@initial(null)
|
||||
@parser(canvasStyleParser)
|
||||
@property()
|
||||
public declare readonly fill: Property<
|
||||
PossibleCanvasStyle,
|
||||
CanvasStyle,
|
||||
this
|
||||
>;
|
||||
@initial(null)
|
||||
@parser(canvasStyleParser)
|
||||
@property()
|
||||
public declare readonly stroke: Property<
|
||||
PossibleCanvasStyle,
|
||||
CanvasStyle,
|
||||
this
|
||||
>;
|
||||
@canvasStyleProperty()
|
||||
public declare readonly fill: CanvasStyleProperty<this>;
|
||||
@canvasStyleProperty()
|
||||
public declare readonly stroke: CanvasStyleProperty<this>;
|
||||
@initial(false)
|
||||
@property()
|
||||
public declare readonly strokeFirst: Signal<boolean, this>;
|
||||
|
||||
17
packages/2d/src/decorators/canvasStyleProperty.ts
Normal file
17
packages/2d/src/decorators/canvasStyleProperty.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {initial, parser, property, Property} from './property';
|
||||
import {canvasStyleParser} from '../utils';
|
||||
import {CanvasStyle, PossibleCanvasStyle} from '../partials';
|
||||
|
||||
export type CanvasStyleProperty<T> = Property<
|
||||
PossibleCanvasStyle,
|
||||
CanvasStyle,
|
||||
T
|
||||
>;
|
||||
|
||||
export function canvasStyleProperty(): PropertyDecorator {
|
||||
return (target, key) => {
|
||||
property()(target, key);
|
||||
parser(canvasStyleParser)(target, key);
|
||||
initial(null)(target, key);
|
||||
};
|
||||
}
|
||||
11
packages/2d/src/decorators/colorProperty.ts
Normal file
11
packages/2d/src/decorators/colorProperty.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import {Color, PossibleColor} from '@motion-canvas/core/lib/types';
|
||||
import {property, Property, wrapper} from './property';
|
||||
|
||||
export type ColorProperty<T> = Property<PossibleColor, Color, T>;
|
||||
|
||||
export function colorProperty(): PropertyDecorator {
|
||||
return (target, key) => {
|
||||
property()(target, key);
|
||||
wrapper(Color)(target, key);
|
||||
};
|
||||
}
|
||||
@@ -1,92 +1,127 @@
|
||||
import {SignalValue, isReactive} from '@motion-canvas/core/lib/utils';
|
||||
import {capitalize, getPropertyMeta} from './property';
|
||||
import {
|
||||
capitalize,
|
||||
createProperty,
|
||||
getPropertyMetaOrCreate,
|
||||
Property,
|
||||
} from './property';
|
||||
import {addInitializer} from './initializers';
|
||||
import {deepLerp} from '@motion-canvas/core/lib/tweening';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* This decorator turns a given property into a signal consisting of one or more
|
||||
* nested signals.
|
||||
*
|
||||
* @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);
|
||||
* this.scale.x(7).scale.y(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.
|
||||
* @param entries - A record mapping the property in the compound object to the
|
||||
* corresponding property on the owner node.
|
||||
*/
|
||||
export function compound(
|
||||
mapping: string[] | Record<string, string>,
|
||||
): PropertyDecorator {
|
||||
return (target: any, key) => {
|
||||
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);
|
||||
|
||||
export function compound(entries: Record<string, string>): PropertyDecorator {
|
||||
return (target, key) => {
|
||||
const meta = getPropertyMetaOrCreate<any>(target, key);
|
||||
meta.compound = true;
|
||||
meta.cloneable = false;
|
||||
for (const [, property] of entries) {
|
||||
const propertyMeta = getPropertyMeta<any>(target, property);
|
||||
if (!propertyMeta) {
|
||||
console.error(
|
||||
`Missing property decorator for "${property.toString()}"`,
|
||||
);
|
||||
meta.compoundEntries = Object.entries(entries);
|
||||
|
||||
addInitializer(target, (instance: any, context: any) => {
|
||||
if (!meta.parser) {
|
||||
console.error(`Missing parser decorator for "${key.toString()}"`);
|
||||
return;
|
||||
}
|
||||
propertyMeta.compoundParent = key.toString();
|
||||
propertyMeta.inspectable = false;
|
||||
}
|
||||
const parser = meta.parser;
|
||||
const initial = context.defaults[key] ?? meta.default;
|
||||
const initialWrapped: SignalValue<any> = isReactive(initial)
|
||||
? () => parser(initial())
|
||||
: parser(initial);
|
||||
|
||||
target.constructor.prototype[`get${capitalize(key.toString())}`] =
|
||||
function () {
|
||||
const signals: [string, Property<any, any, any>][] = [];
|
||||
for (const [key, property] of meta.compoundEntries) {
|
||||
signals.push([
|
||||
key,
|
||||
createProperty(
|
||||
instance,
|
||||
property,
|
||||
context.defaults[property] ??
|
||||
(isReactive(initialWrapped)
|
||||
? () => initialWrapped()[key]
|
||||
: initialWrapped[key]),
|
||||
undefined,
|
||||
undefined,
|
||||
instance[`get${capitalize(<string>property)}`],
|
||||
instance[`set${capitalize(<string>property)}`],
|
||||
instance[`tween${capitalize(<string>property)}`],
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
function getter() {
|
||||
const object = Object.fromEntries(
|
||||
entries.map(([key, property]) => [key, this[property]()]),
|
||||
signals.map(([key, property]) => [key, property()]),
|
||||
);
|
||||
return meta?.parser ? meta.parser(object) : object;
|
||||
};
|
||||
return parser(object);
|
||||
}
|
||||
|
||||
target.constructor.prototype[`set${capitalize(key.toString())}`] =
|
||||
function set(value: SignalValue<any>) {
|
||||
function setter(value: SignalValue<any>) {
|
||||
if (isReactive(value)) {
|
||||
for (const [key, property] of entries) {
|
||||
this[property](() => value()[key]);
|
||||
for (const [key, property] of signals) {
|
||||
property(() => value()[key]);
|
||||
}
|
||||
} else {
|
||||
for (const [key, property] of entries) {
|
||||
this[property](value[key]);
|
||||
for (const [key, property] of signals) {
|
||||
property(value[key]);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const property = createProperty(
|
||||
instance,
|
||||
<string>key,
|
||||
undefined,
|
||||
meta.interpolationFunction ?? deepLerp,
|
||||
parser,
|
||||
getter,
|
||||
setter,
|
||||
instance[`tween${capitalize(<string>key)}`],
|
||||
);
|
||||
|
||||
for (const [key, signal] of signals) {
|
||||
Object.defineProperty(property, key, {value: signal});
|
||||
}
|
||||
|
||||
Object.defineProperty(property, 'reset', {
|
||||
value: () => {
|
||||
for (const [, signal] of signals) {
|
||||
signal.reset();
|
||||
}
|
||||
return instance;
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(property, 'save', {
|
||||
value: () => {
|
||||
for (const [, signal] of signals) {
|
||||
signal.save();
|
||||
}
|
||||
return instance;
|
||||
},
|
||||
});
|
||||
|
||||
instance[key] = property;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export * from './computed';
|
||||
export * from './canvasStyleProperty';
|
||||
export * from './colorProperty';
|
||||
export * from './compound';
|
||||
export * from './property';
|
||||
export * from './computed';
|
||||
export * from './initializers';
|
||||
export * from './property';
|
||||
export * from './vector2Property';
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface PropertyMetadata<T> {
|
||||
inspectable?: boolean;
|
||||
compoundParent?: string;
|
||||
compound?: boolean;
|
||||
compoundEntries: [string, string][];
|
||||
}
|
||||
|
||||
export interface Property<
|
||||
@@ -56,14 +57,13 @@ export function createProperty<
|
||||
initial?: TSetterValue,
|
||||
defaultInterpolation: InterpolationFunction<TGetterValue> = deepLerp,
|
||||
parser?: (value: TSetterValue) => TGetterValue,
|
||||
originalGetter?: SignalGetter<TGetterValue>,
|
||||
originalSetter?: SignalSetter<TSetterValue>,
|
||||
tweener?: SignalTween<TGetterValue>,
|
||||
): Property<TSetterValue, TGetterValue, TNode> {
|
||||
let getter: SignalGetter<TGetterValue>;
|
||||
let setter: SignalSetter<TSetterValue>;
|
||||
|
||||
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`,
|
||||
@@ -148,6 +148,7 @@ export function createProperty<
|
||||
);
|
||||
|
||||
Object.defineProperty(handler, 'reset', {
|
||||
configurable: true,
|
||||
value: signal
|
||||
? signal.reset
|
||||
: initial !== undefined
|
||||
@@ -156,6 +157,7 @@ export function createProperty<
|
||||
});
|
||||
|
||||
Object.defineProperty(handler, 'save', {
|
||||
configurable: true,
|
||||
value: () => setter(getter()),
|
||||
});
|
||||
|
||||
@@ -163,10 +165,6 @@ export function createProperty<
|
||||
value: signal?.raw ?? getter,
|
||||
});
|
||||
|
||||
if (initial !== undefined && !signal) {
|
||||
setter(wrap(initial));
|
||||
}
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
@@ -179,6 +177,34 @@ export function getPropertyMeta<T>(
|
||||
return object[PROPERTIES]?.[key] ?? null;
|
||||
}
|
||||
|
||||
export function getPropertyMetaOrCreate<T>(
|
||||
object: any,
|
||||
key: string | symbol,
|
||||
): PropertyMetadata<T> {
|
||||
let lookup: Record<string | symbol, PropertyMetadata<T>>;
|
||||
if (!object[PROPERTIES]) {
|
||||
object[PROPERTIES] = lookup = {};
|
||||
} else if (
|
||||
object[PROPERTIES] &&
|
||||
!Object.prototype.hasOwnProperty.call(object, PROPERTIES)
|
||||
) {
|
||||
object[PROPERTIES] = lookup = Object.fromEntries<PropertyMetadata<T>>(
|
||||
Object.entries(
|
||||
<Record<string | symbol, PropertyMetadata<T>>>object[PROPERTIES],
|
||||
).map(([key, meta]) => [key, {...meta}]),
|
||||
);
|
||||
} else {
|
||||
lookup = object[PROPERTIES];
|
||||
}
|
||||
|
||||
lookup[key] ??= {
|
||||
cloneable: true,
|
||||
inspectable: true,
|
||||
compoundEntries: [],
|
||||
};
|
||||
return lookup[key];
|
||||
}
|
||||
|
||||
export function getPropertiesOf(
|
||||
value: any,
|
||||
): Record<string, PropertyMetadata<any>> {
|
||||
@@ -212,26 +238,7 @@ export function getPropertiesOf(
|
||||
*/
|
||||
export function property<T>(): PropertyDecorator {
|
||||
return (target: any, key) => {
|
||||
let lookup: Record<string | symbol, PropertyMetadata<T>>;
|
||||
if (!target[PROPERTIES]) {
|
||||
target[PROPERTIES] = lookup = {};
|
||||
} else if (
|
||||
target[PROPERTIES] &&
|
||||
!Object.prototype.hasOwnProperty.call(target, PROPERTIES)
|
||||
) {
|
||||
target[PROPERTIES] = lookup = Object.fromEntries<PropertyMetadata<T>>(
|
||||
Object.entries(
|
||||
<Record<string | symbol, PropertyMetadata<T>>>target[PROPERTIES],
|
||||
).map(([key, meta]) => [key, {...meta}]),
|
||||
);
|
||||
} else {
|
||||
lookup = target[PROPERTIES];
|
||||
}
|
||||
|
||||
const meta = (lookup[key] = lookup[key] ?? {
|
||||
cloneable: true,
|
||||
inspectable: true,
|
||||
});
|
||||
const meta = getPropertyMetaOrCreate<T>(target, key);
|
||||
addInitializer(target, (instance: any, context: any) => {
|
||||
instance[key] = createProperty(
|
||||
instance,
|
||||
@@ -239,6 +246,9 @@ export function property<T>(): PropertyDecorator {
|
||||
context.defaults[key] ?? meta.default,
|
||||
meta.interpolationFunction ?? deepLerp,
|
||||
meta.parser,
|
||||
target[`get${capitalize(<string>key)}`],
|
||||
target[`set${capitalize(<string>key)}`],
|
||||
target[`tween${capitalize(<string>key)}`],
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -346,12 +356,11 @@ export function parser<T>(value: (value: any) => T): PropertyDecorator {
|
||||
* Create a property wrapper decorator.
|
||||
*
|
||||
* @remarks
|
||||
* This decorator specifies the wrapper of a property.
|
||||
* 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.
|
||||
* This is a shortcut decorator for setting both the {@link parser} and
|
||||
* {@link interpolation}.
|
||||
*
|
||||
* If the wrapper class has a method called `lerp` it will be set as the
|
||||
* default interpolation function for the property.
|
||||
* The interpolation function will be set only if the wrapper class has a method
|
||||
* called `lerp`, which will be used as said function.
|
||||
*
|
||||
* Must be specified before the {@link property} decorator.
|
||||
*
|
||||
@@ -361,6 +370,12 @@ export function parser<T>(value: (value: any) => T): PropertyDecorator {
|
||||
* \@wrapper(Vector2)
|
||||
* \@property()
|
||||
* public declare offset: Signal<Vector2, this>;
|
||||
*
|
||||
* // same as:
|
||||
* \@parser(value => new Vector2(value))
|
||||
* \@interpolation(Vector2.lerp)
|
||||
* \@property()
|
||||
* public declare offset: Signal<Vector2, this>;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
@@ -416,7 +431,7 @@ export function cloneable<T>(value = true): PropertyDecorator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a inspectable property decorator.
|
||||
* Create an inspectable property decorator.
|
||||
*
|
||||
* @remarks
|
||||
* This decorator specifies whether the property should be visible in the
|
||||
|
||||
23
packages/2d/src/decorators/spacingProperty.ts
Normal file
23
packages/2d/src/decorators/spacingProperty.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {PossibleSpacing, Spacing} from '@motion-canvas/core/lib/types';
|
||||
import {Property, wrapper} from './property';
|
||||
import {compound} from './compound';
|
||||
import {Signal} from '@motion-canvas/core/lib/utils';
|
||||
|
||||
export type SpacingProperty<T> = Property<PossibleSpacing, Spacing, T> & {
|
||||
top: Signal<number, T>;
|
||||
right: Signal<number, T>;
|
||||
bottom: Signal<number, T>;
|
||||
left: Signal<number, T>;
|
||||
};
|
||||
|
||||
export function spacingProperty(prefix?: string): PropertyDecorator {
|
||||
return (target, key) => {
|
||||
compound({
|
||||
top: prefix ? `${prefix}Top` : 'top',
|
||||
right: prefix ? `${prefix}Right` : 'right',
|
||||
bottom: prefix ? `${prefix}Bottom` : 'bottom',
|
||||
left: prefix ? `${prefix}Left` : 'left',
|
||||
})(target, key);
|
||||
wrapper(Spacing)(target, key);
|
||||
};
|
||||
}
|
||||
29
packages/2d/src/decorators/vector2Property.ts
Normal file
29
packages/2d/src/decorators/vector2Property.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import {PossibleVector2, Vector2} from '@motion-canvas/core/lib/types';
|
||||
import {Property, wrapper} from './property';
|
||||
import {compound} from './compound';
|
||||
import {Signal} from '@motion-canvas/core/lib/utils';
|
||||
import {Length} from '../partials';
|
||||
|
||||
export type Vector2Property<T> = Property<PossibleVector2, Vector2, T> & {
|
||||
x: Signal<number, T>;
|
||||
y: Signal<number, T>;
|
||||
};
|
||||
|
||||
export type Vector2LengthProperty<TOwner> = Property<
|
||||
PossibleVector2<Length>,
|
||||
Vector2,
|
||||
TOwner
|
||||
> & {
|
||||
x: Property<Length, number, TOwner>;
|
||||
y: Property<Length, number, TOwner>;
|
||||
};
|
||||
|
||||
export function vector2Property(prefix?: string): PropertyDecorator {
|
||||
return (target, key) => {
|
||||
compound({
|
||||
x: prefix ? `${prefix}X` : 'x',
|
||||
y: prefix ? `${prefix}Y` : 'y',
|
||||
})(target, key);
|
||||
wrapper(Vector2)(target, key);
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import {
|
||||
compound,
|
||||
computed,
|
||||
initial,
|
||||
initialize,
|
||||
property,
|
||||
wrapper,
|
||||
Vector2Property,
|
||||
vector2Property,
|
||||
} from '../decorators';
|
||||
import {Color, PossibleColor, Vector2} from '@motion-canvas/core/lib/types';
|
||||
import {Signal} from '@motion-canvas/core/lib/utils';
|
||||
@@ -35,27 +35,11 @@ export class Gradient {
|
||||
@property()
|
||||
public declare readonly type: Signal<GradientType, this>;
|
||||
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly fromX: Signal<number, this>;
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly fromY: Signal<number, this>;
|
||||
@compound({x: 'fromX', y: 'fromY'})
|
||||
@wrapper(Vector2)
|
||||
@property()
|
||||
public declare readonly from: Signal<Vector2, this>;
|
||||
@vector2Property('from')
|
||||
public declare readonly from: Vector2Property<this>;
|
||||
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly toX: Signal<number, this>;
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly toY: Signal<number, this>;
|
||||
@compound({x: 'toX', y: 'toY'})
|
||||
@wrapper(Vector2)
|
||||
@property()
|
||||
public declare readonly to: Signal<Vector2, this>;
|
||||
@vector2Property('to')
|
||||
public declare readonly to: Vector2Property<this>;
|
||||
|
||||
@initial(0)
|
||||
@property()
|
||||
@@ -80,26 +64,26 @@ export class Gradient {
|
||||
switch (this.type()) {
|
||||
case 'linear':
|
||||
gradient = context.createLinearGradient(
|
||||
this.fromX(),
|
||||
this.fromY(),
|
||||
this.toX(),
|
||||
this.toY(),
|
||||
this.from.x(),
|
||||
this.from.y(),
|
||||
this.to.x(),
|
||||
this.to.y(),
|
||||
);
|
||||
break;
|
||||
case 'conic':
|
||||
gradient = context.createConicGradient(
|
||||
this.angle(),
|
||||
this.fromX(),
|
||||
this.fromY(),
|
||||
this.from.x(),
|
||||
this.from.y(),
|
||||
);
|
||||
break;
|
||||
case 'radial':
|
||||
gradient = context.createRadialGradient(
|
||||
this.fromX(),
|
||||
this.fromY(),
|
||||
this.from.x(),
|
||||
this.from.y(),
|
||||
this.fromRadius(),
|
||||
this.toX(),
|
||||
this.toY(),
|
||||
this.to.x(),
|
||||
this.to.y(),
|
||||
this.toRadius(),
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
SceneRenderEvent,
|
||||
} from '@motion-canvas/core/lib/scenes';
|
||||
import {View2D} from './View2D';
|
||||
import {Signal, useScene} from '@motion-canvas/core/lib/utils';
|
||||
import {useScene} from '@motion-canvas/core/lib/utils';
|
||||
import {Node} from '../components';
|
||||
import {Vector2} from '@motion-canvas/core/lib/types';
|
||||
|
||||
@@ -59,12 +59,9 @@ export class Scene2D extends GeneratorScene<View2D> implements Inspectable {
|
||||
): InspectedAttributes | null {
|
||||
if (!(element instanceof Node)) return null;
|
||||
const attributes: Record<string, any> = {};
|
||||
for (const key in element.properties) {
|
||||
const meta = element.properties[key];
|
||||
for (const {key, meta, signal} of element) {
|
||||
if (!meta.inspectable) continue;
|
||||
attributes[key] = (<Record<string, Signal<any>>>(<unknown>element))[
|
||||
key
|
||||
]();
|
||||
attributes[key] = signal();
|
||||
}
|
||||
|
||||
return attributes;
|
||||
|
||||
@@ -62,15 +62,16 @@ export class View2D extends Layout {
|
||||
customMatrix.e !== currentMatrix.e ||
|
||||
customMatrix.f !== currentMatrix.f
|
||||
) {
|
||||
this.x(customMatrix.m41)
|
||||
.y(customMatrix.m42)
|
||||
.scaleX(
|
||||
this.position
|
||||
.x(customMatrix.m41)
|
||||
.position.y(customMatrix.m42)
|
||||
.scale.x(
|
||||
Math.sqrt(
|
||||
customMatrix.m11 * customMatrix.m11 +
|
||||
customMatrix.m12 * customMatrix.m12,
|
||||
),
|
||||
)
|
||||
.scaleY(
|
||||
.scale.y(
|
||||
Math.sqrt(
|
||||
customMatrix.m21 * customMatrix.m21 +
|
||||
customMatrix.m22 * customMatrix.m22,
|
||||
|
||||
@@ -101,15 +101,33 @@ export function drawImage(
|
||||
}
|
||||
}
|
||||
|
||||
export function moveTo(context: CanvasRenderingContext2D, position: Vector2) {
|
||||
export function moveTo(
|
||||
context: CanvasRenderingContext2D | Path2D,
|
||||
position: Vector2,
|
||||
) {
|
||||
context.moveTo(position.x, position.y);
|
||||
}
|
||||
|
||||
export function lineTo(context: CanvasRenderingContext2D, position: Vector2) {
|
||||
export function lineTo(
|
||||
context: CanvasRenderingContext2D | Path2D,
|
||||
position: Vector2,
|
||||
) {
|
||||
context.lineTo(position.x, position.y);
|
||||
}
|
||||
|
||||
export function drawLine(context: CanvasRenderingContext2D, points: Vector2[]) {
|
||||
export function arcTo(
|
||||
context: CanvasRenderingContext2D | Path2D,
|
||||
through: Vector2,
|
||||
position: Vector2,
|
||||
radius: number,
|
||||
) {
|
||||
context.arcTo(through.x, through.y, position.x, position.y, radius);
|
||||
}
|
||||
|
||||
export function drawLine(
|
||||
context: CanvasRenderingContext2D | Path2D,
|
||||
points: Vector2[],
|
||||
) {
|
||||
if (points.length < 2) return;
|
||||
moveTo(context, points[0]);
|
||||
for (const point of points.slice(1)) {
|
||||
|
||||
@@ -127,6 +127,10 @@ export class Rect implements Type {
|
||||
return new Vector2(this.width, this.height);
|
||||
}
|
||||
|
||||
public get center() {
|
||||
return new Vector2(this.x + this.width / 2, this.y + this.height / 2);
|
||||
}
|
||||
|
||||
public get left() {
|
||||
return this.x;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import {Rect} from './Rect';
|
||||
import {arcLerp, map} from '../tweening';
|
||||
import {Direction, Origin} from './Origin';
|
||||
import {Type} from './Type';
|
||||
|
||||
export type SerializedVector2 = {
|
||||
x: number;
|
||||
y: number;
|
||||
export type SerializedVector2<T = number> = {
|
||||
x: T;
|
||||
y: T;
|
||||
};
|
||||
|
||||
export type PossibleVector2 =
|
||||
| SerializedVector2
|
||||
| {width: number; height: number}
|
||||
| number
|
||||
| [number, number]
|
||||
| Rect;
|
||||
export type PossibleVector2<T = number> =
|
||||
| SerializedVector2<T>
|
||||
| {width: T; height: T}
|
||||
| T
|
||||
| [T, T];
|
||||
|
||||
export class Vector2 implements Type {
|
||||
public static readonly symbol = Symbol.for(
|
||||
@@ -141,7 +139,7 @@ export class Vector2 implements Type {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof one === 'number') {
|
||||
if (typeof one !== 'object') {
|
||||
this.x = one;
|
||||
this.y = two ?? one;
|
||||
return;
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"experimentalDecorators": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import {makeScene2D} from '@motion-canvas/2d';
|
||||
import {Circle} from '@motion-canvas/2d/lib/components';
|
||||
import {waitFor, waitUntil} from '@motion-canvas/core/lib/flow';
|
||||
import {useRef} from '@motion-canvas/core/lib/utils';
|
||||
import {Vector2} from '@motion-canvas/core/lib/types';
|
||||
|
||||
export default makeScene2D(function* (view) {
|
||||
const circle = useRef<Circle>();
|
||||
@@ -12,7 +11,7 @@ export default makeScene2D(function* (view) {
|
||||
);
|
||||
|
||||
yield* waitUntil('circle');
|
||||
yield* circle.value.scale(Vector2.fromScalar(2), 2);
|
||||
yield* circle.value.scale(2, 2);
|
||||
|
||||
yield* waitFor(5);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import {useMemo} from 'preact/hooks';
|
||||
import {useCurrentScene, useCurrentFrame} from '../../hooks';
|
||||
import type {
|
||||
Inspectable,
|
||||
InspectedAttributes,
|
||||
} from '@motion-canvas/core/lib/scenes';
|
||||
import type {Inspectable} from '@motion-canvas/core/lib/scenes';
|
||||
import {isInspectable} from '@motion-canvas/core/lib/scenes/Inspectable';
|
||||
import {Pane} from '../tabs';
|
||||
import {useInspection} from '../../contexts';
|
||||
|
||||
Reference in New Issue
Block a user