feat: merge properties and signals (#124)

Properties and signals are now represented by the same class.
Complex types such as `Vector2` can create compound signals representing them:

```ts
const vector = Vector2.createSignal();

vector(Vector2.up);
vector.x(20);
```

The internal implementation has been rewritten to use classes instead of function scopes.
This commit is contained in:
Jacob
2023-01-12 04:55:21 +01:00
committed by GitHub
parent 0fdd85ecf5
commit da3ba83d82
59 changed files with 1462 additions and 1232 deletions

View File

@@ -1,6 +1,6 @@
import {Shape, ShapeProps} from './Shape';
import {Signal, SignalValue} from '@motion-canvas/core/lib/utils';
import {initial, property} from '../decorators';
import {SignalValue, SimpleSignal} from '@motion-canvas/core/lib/signals';
import {initial, signal} from '../decorators';
export interface CircleProps extends ShapeProps {
startAngle?: SignalValue<number>;
@@ -9,12 +9,12 @@ export interface CircleProps extends ShapeProps {
export class Circle extends Shape {
@initial(0)
@property()
public declare readonly startAngle: Signal<number, this>;
@signal()
public declare readonly startAngle: SimpleSignal<number, this>;
@initial(360)
@property()
public declare readonly endAngle: Signal<number, this>;
@signal()
public declare readonly endAngle: SimpleSignal<number, this>;
public constructor(props: CircleProps) {
super(props);

View File

@@ -1,11 +1,5 @@
import {computed, initial, property} from '../decorators';
import {
createComputedAsync,
createSignal,
Signal,
SignalValue,
useLogger,
} from '@motion-canvas/core/lib/utils';
import {computed, initial, signal} from '../decorators';
import {useLogger} from '@motion-canvas/core/lib/utils';
import {Shape, ShapeProps} from './Shape';
import {CodeTree, parse, diff, ready, MorphToken, Token} from 'code-fns';
import {
@@ -17,6 +11,12 @@ import {
import {threadable} from '@motion-canvas/core/lib/decorators';
import {Length} from '../partials';
import {SerializedVector2, Vector2} from '@motion-canvas/core/lib/types';
import {
createComputedAsync,
createSignal,
SignalValue,
SimpleSignal,
} from '@motion-canvas/core/lib/signals';
export interface CodeProps extends ShapeProps {
children?: CodeTree;
@@ -30,8 +30,8 @@ export class CodeBlock extends Shape {
);
@initial('')
@property()
public declare readonly code: Signal<CodeTree, this>;
@signal()
public declare readonly code: SimpleSignal<CodeTree, this>;
private progress = createSignal<number | null>(null);
private diffed: MorphToken[] | null = null;

View File

@@ -1,7 +1,7 @@
import {Shape, ShapeProps} from './Shape';
import {SignalValue} from '@motion-canvas/core/lib/utils';
import {PossibleVector2} from '@motion-canvas/core/lib/types';
import {initial, vector2Property, Vector2Property} from '../decorators';
import {PossibleVector2, Vector2Signal} from '@motion-canvas/core/lib/types';
import {initial, vector2Signal} from '../decorators';
import {SignalValue} from '@motion-canvas/core/lib/signals';
export interface GridProps extends ShapeProps {
spacing?: SignalValue<PossibleVector2>;
@@ -9,8 +9,8 @@ export interface GridProps extends ShapeProps {
export class Grid extends Shape {
@initial(80)
@vector2Property('spacing')
public declare readonly spacing: Vector2Property<this>;
@vector2Signal('spacing')
public declare readonly spacing: Vector2Signal<this>;
public constructor(props: GridProps) {
super(props);

View File

@@ -1,9 +1,4 @@
import {
collectPromise,
Signal,
SignalValue,
} from '@motion-canvas/core/lib/utils';
import {computed, initial, property} from '../decorators';
import {computed, initial, signal} from '../decorators';
import {
Color,
Rect as RectType,
@@ -13,6 +8,11 @@ import {
import {drawImage} from '../utils';
import {Rect, RectProps} from './Rect';
import {Length} from '../partials';
import {
DependencyContext,
SignalValue,
SimpleSignal,
} from '@motion-canvas/core/lib/signals';
export interface ImageProps extends RectProps {
src?: SignalValue<string>;
@@ -23,16 +23,16 @@ export interface ImageProps extends RectProps {
export class Image extends Rect {
private static pool: Record<string, HTMLImageElement> = {};
@property()
public declare readonly src: Signal<string, this>;
@signal()
public declare readonly src: SimpleSignal<string, this>;
@initial(1)
@property()
public declare readonly alpha: Signal<number, this>;
@signal()
public declare readonly alpha: SimpleSignal<number, this>;
@initial(true)
@property()
public declare readonly smoothing: Signal<boolean, this>;
@signal()
public declare readonly smoothing: SimpleSignal<boolean, this>;
public constructor(props: ImageProps) {
super(props);
@@ -61,7 +61,7 @@ export class Image extends Rect {
const image = document.createElement('img');
image.src = src;
if (!image.complete) {
collectPromise(
DependencyContext.collectPromise(
new Promise((resolve, reject) => {
image.addEventListener('load', resolve);
image.addEventListener('error', reject);

View File

@@ -1,14 +1,11 @@
import {
cloneable,
compound,
computed,
initial,
inspectable,
property,
Vector2LengthProperty,
Vector2Property,
vector2Property,
wrapper,
signal,
Vector2LengthSignal,
vector2Signal,
} from '../decorators';
import {
Origin,
@@ -18,8 +15,9 @@ import {
originToOffset,
SerializedVector2,
PossibleVector2,
SpacingSignal,
Vector2Signal,
} from '@motion-canvas/core/lib/types';
import {createSignal, Signal, SignalValue} from '@motion-canvas/core/lib/utils';
import {
InterpolationFunction,
TimingFunction,
@@ -38,7 +36,12 @@ import {threadable} from '@motion-canvas/core/lib/decorators';
import {ThreadGenerator} from '@motion-canvas/core/lib/threading';
import {Node, NodeProps} from './Node';
import {drawLine, lineTo} from '../utils';
import {spacingProperty, SpacingProperty} from '../decorators/spacingProperty';
import {spacingSignal} from '../decorators/spacingSignal';
import {
createSignal,
SignalValue,
SimpleSignal,
} from '@motion-canvas/core/lib/signals';
export interface LayoutProps extends NodeProps {
layout?: LayoutMode;
@@ -93,89 +96,89 @@ export interface LayoutProps extends NodeProps {
export class Layout extends Node {
@initial(null)
@property()
public declare readonly layout: Signal<LayoutMode, this>;
@signal()
public declare readonly layout: SimpleSignal<LayoutMode, this>;
@initial(null)
@property()
public declare readonly maxWidth: Signal<Length, this>;
@signal()
public declare readonly maxWidth: SimpleSignal<Length, this>;
@initial(null)
@property()
public declare readonly maxHeight: Signal<Length, this>;
@signal()
public declare readonly maxHeight: SimpleSignal<Length, this>;
@initial(null)
@property()
public declare readonly minWidth: Signal<Length, this>;
@signal()
public declare readonly minWidth: SimpleSignal<Length, this>;
@initial(null)
@property()
public declare readonly minHeight: Signal<Length, this>;
@signal()
public declare readonly minHeight: SimpleSignal<Length, this>;
@initial(null)
@property()
public declare readonly ratio: Signal<number | null, this>;
@signal()
public declare readonly ratio: SimpleSignal<number | null, this>;
@spacingProperty('margin')
public declare readonly margin: SpacingProperty<this>;
@spacingSignal('margin')
public declare readonly margin: SpacingSignal<this>;
@spacingProperty('padding')
public declare readonly padding: SpacingProperty<this>;
@spacingSignal('padding')
public declare readonly padding: SpacingSignal<this>;
@initial('row')
@property()
public declare readonly direction: Signal<FlexDirection, this>;
@signal()
public declare readonly direction: SimpleSignal<FlexDirection, this>;
@initial(null)
@property()
public declare readonly basis: Signal<FlexBasis, this>;
@signal()
public declare readonly basis: SimpleSignal<FlexBasis, this>;
@initial(0)
@property()
public declare readonly grow: Signal<number, this>;
@signal()
public declare readonly grow: SimpleSignal<number, this>;
@initial(1)
@property()
public declare readonly shrink: Signal<number, this>;
@signal()
public declare readonly shrink: SimpleSignal<number, this>;
@initial('nowrap')
@property()
public declare readonly wrap: Signal<FlexWrap, this>;
@signal()
public declare readonly wrap: SimpleSignal<FlexWrap, this>;
@initial('normal')
@property()
public declare readonly justifyContent: Signal<FlexJustify, this>;
@signal()
public declare readonly justifyContent: SimpleSignal<FlexJustify, this>;
@initial('normal')
@property()
public declare readonly alignItems: Signal<FlexAlign, this>;
@signal()
public declare readonly alignItems: SimpleSignal<FlexAlign, this>;
@initial(null)
@property()
public declare readonly gap: Signal<Length, this>;
@signal()
public declare readonly gap: SimpleSignal<Length, this>;
@initial(null)
@property()
public declare readonly rowGap: Signal<Length, this>;
@signal()
public declare readonly rowGap: SimpleSignal<Length, this>;
@initial(null)
@property()
public declare readonly columnGap: Signal<Length, this>;
@signal()
public declare readonly columnGap: SimpleSignal<Length, this>;
@initial(null)
@property()
public declare readonly fontFamily: Signal<string | null, this>;
@signal()
public declare readonly fontFamily: SimpleSignal<string | null, this>;
@initial(null)
@property()
public declare readonly fontSize: Signal<number | null, this>;
@signal()
public declare readonly fontSize: SimpleSignal<number | null, this>;
@initial(null)
@property()
public declare readonly fontStyle: Signal<string | null, this>;
@signal()
public declare readonly fontStyle: SimpleSignal<string | null, this>;
@initial(null)
@property()
public declare readonly fontWeight: Signal<number | null, this>;
@signal()
public declare readonly fontWeight: SimpleSignal<number | null, this>;
@initial(null)
@property()
public declare readonly lineHeight: Signal<number | null, this>;
@signal()
public declare readonly lineHeight: SimpleSignal<number | null, this>;
@initial(null)
@property()
public declare readonly letterSpacing: Signal<number | null, this>;
@signal()
public declare readonly letterSpacing: SimpleSignal<number | null, this>;
@initial(null)
@property()
public declare readonly textWrap: Signal<boolean | null, this>;
@signal()
public declare readonly textWrap: SimpleSignal<boolean | null, this>;
@cloneable(false)
@inspectable(false)
@property()
protected declare readonly customX: Signal<number, this>;
@signal()
protected declare readonly customX: SimpleSignal<number, this>;
protected getX(): number {
if (this.isLayoutRoot()) {
return this.customX();
@@ -189,8 +192,8 @@ export class Layout extends Node {
@cloneable(false)
@inspectable(false)
@property()
protected declare readonly customY: Signal<number, this>;
@signal()
protected declare readonly customY: SimpleSignal<number, this>;
protected getY(): number {
if (this.isLayoutRoot()) {
@@ -285,17 +288,16 @@ export class Layout extends Node {
}
@cloneable(false)
@wrapper(Vector2)
@initial({x: null, y: null})
@compound({x: 'width', y: 'height'})
public declare readonly size: Vector2LengthProperty<this>;
@vector2Signal({x: 'width', y: 'height'})
public declare readonly size: Vector2LengthSignal<this>;
@inspectable(false)
@property()
protected declare readonly customWidth: Signal<Length, this>;
@signal()
protected declare readonly customWidth: SimpleSignal<Length, this>;
@inspectable(false)
@property()
protected declare readonly customHeight: Signal<Length, this>;
@signal()
protected declare readonly customHeight: SimpleSignal<Length, this>;
@computed()
protected desiredSize(): SerializedVector2<Length> {
return {
@@ -340,12 +342,12 @@ export class Layout extends Node {
this.size(value);
}
@vector2Property('offset')
public declare readonly offset: Vector2Property<this>;
@vector2Signal('offset')
public declare readonly offset: Vector2Signal<this>;
@initial(false)
@property()
public declare readonly clip: Signal<boolean, this>;
@signal()
public declare readonly clip: SimpleSignal<boolean, this>;
public readonly element: HTMLElement;
public readonly styles: CSSStyleDeclaration;

View File

@@ -1,8 +1,8 @@
import {Shape, ShapeProps} from './Shape';
import {Node} from './Node';
import {computed, initial, property} from '../decorators';
import {computed, initial, signal} from '../decorators';
import {arc, lineTo, moveTo, resolveCanvasStyle} from '../utils';
import {Signal, SignalValue} from '@motion-canvas/core/lib/utils';
import {SignalValue, SimpleSignal} from '@motion-canvas/core/lib/signals';
import {Rect, SerializedVector2, Vector2} from '@motion-canvas/core/lib/types';
import {clamp} from '@motion-canvas/core/lib/tweening';
import {Length} from '../partials';
@@ -30,40 +30,40 @@ export interface LineProps extends ShapeProps {
export class Line extends Shape {
@initial(0)
@property()
public declare readonly radius: Signal<number, this>;
@signal()
public declare readonly radius: SimpleSignal<number, this>;
@initial(false)
@property()
public declare readonly closed: Signal<boolean, this>;
@signal()
public declare readonly closed: SimpleSignal<boolean, this>;
@initial(0)
@property()
public declare readonly start: Signal<number, this>;
@signal()
public declare readonly start: SimpleSignal<number, this>;
@initial(0)
@property()
public declare readonly startOffset: Signal<number, this>;
@signal()
public declare readonly startOffset: SimpleSignal<number, this>;
@initial(false)
@property()
public declare readonly startArrow: Signal<boolean, this>;
@signal()
public declare readonly startArrow: SimpleSignal<boolean, this>;
@initial(1)
@property()
public declare readonly end: Signal<number, this>;
@signal()
public declare readonly end: SimpleSignal<number, this>;
@initial(1)
@property()
public declare readonly endOffset: Signal<number, this>;
@signal()
public declare readonly endOffset: SimpleSignal<number, this>;
@initial(false)
@property()
public declare readonly endArrow: Signal<boolean, this>;
@signal()
public declare readonly endArrow: SimpleSignal<boolean, this>;
@initial(24)
@property()
public declare readonly arrowSize: Signal<number, this>;
@signal()
public declare readonly arrowSize: SimpleSignal<number, this>;
protected override desiredSize(): SerializedVector2<Length> {
return this.childrenRect().size;

View File

@@ -1,18 +1,13 @@
import {
cloneable,
ColorProperty,
colorProperty,
colorSignal,
computed,
getPropertiesOf,
initial,
initialize,
Property,
property,
Vector2Property,
vector2Property,
signal,
vector2Signal,
wrapper,
FiltersProperty,
filtersProperty,
} from '../decorators';
import {
Vector2,
@@ -21,23 +16,28 @@ import {
PossibleColor,
transformAngle,
PossibleVector2,
Vector2Signal,
ColorSignal,
} from '@motion-canvas/core/lib/types';
import {
consumePromises,
createSignal,
isReactive,
Reference,
Signal,
SignalValue,
} from '@motion-canvas/core/lib/utils';
import {ComponentChild, ComponentChildren, NodeConstructor} from './types';
import {Reference} from '@motion-canvas/core/lib/utils';
import type {ComponentChild, ComponentChildren, NodeConstructor} from './types';
import {Promisable} from '@motion-canvas/core/lib/threading';
import {useScene2D} from '../scenes';
import {useScene2D} from '../scenes/useScene2D';
import {TimingFunction} from '@motion-canvas/core/lib/tweening';
import {threadable} from '@motion-canvas/core/lib/decorators';
import {drawLine} from '../utils';
import type {View2D} from './View2D';
import {Filter} from '../partials';
import {filtersSignal, FiltersSignal} from '../decorators/filtersSignal';
import {
createSignal,
Signal,
DependencyContext,
SignalValue,
SimpleSignal,
SignalContext,
isReactive,
} from '@motion-canvas/core/lib/signals';
export interface NodeProps {
ref?: Reference<any>;
@@ -67,13 +67,13 @@ export interface NodeProps {
export class Node implements Promisable<Node> {
public declare isClass: boolean;
@vector2Property()
public declare readonly position: Vector2Property<this>;
@vector2Signal()
public declare readonly position: Vector2Signal<this>;
@wrapper(Vector2)
@cloneable(false)
@property()
public declare readonly absolutePosition: Property<
@signal()
public declare readonly absolutePosition: SignalContext<
PossibleVector2,
Vector2,
this
@@ -93,12 +93,12 @@ export class Node implements Promisable<Node> {
}
@initial(0)
@property()
public declare readonly rotation: Signal<number, this>;
@signal()
public declare readonly rotation: SimpleSignal<number, this>;
@cloneable(false)
@property()
public declare readonly absoluteRotation: Signal<number, this>;
@signal()
public declare readonly absoluteRotation: SimpleSignal<number, this>;
protected getAbsoluteRotation() {
const matrix = this.localToWorld();
@@ -114,17 +114,13 @@ export class Node implements Promisable<Node> {
}
@initial(Vector2.one)
@vector2Property('scale')
public declare readonly scale: Vector2Property<this>;
@vector2Signal('scale')
public declare readonly scale: Vector2Signal<this>;
@wrapper(Vector2)
@cloneable(false)
@property()
public declare readonly absoluteScale: Property<
PossibleVector2,
Vector2,
this
>;
@signal()
public declare readonly absoluteScale: Signal<PossibleVector2, Vector2, this>;
protected getAbsoluteScale(): Vector2 {
const matrix = this.localToWorld();
@@ -148,16 +144,16 @@ export class Node implements Promisable<Node> {
}
@initial(false)
@property()
public declare readonly cache: Signal<boolean, this>;
@signal()
public declare readonly cache: SimpleSignal<boolean, this>;
@initial(false)
@property()
public declare readonly composite: Signal<boolean, this>;
@signal()
public declare readonly composite: SimpleSignal<boolean, this>;
@initial('source-over')
@property()
public declare readonly compositeOperation: Signal<
@signal()
public declare readonly compositeOperation: SimpleSignal<
GlobalCompositeOperation,
this
>;
@@ -183,27 +179,27 @@ export class Node implements Promisable<Node> {
}
@initial(1)
@property()
public declare readonly opacity: Signal<number, this>;
@signal()
public declare readonly opacity: SimpleSignal<number, this>;
@computed()
public absoluteOpacity(): number {
return (this.parent()?.absoluteOpacity() ?? 1) * this.opacity();
}
@filtersProperty()
public declare readonly filters: FiltersProperty;
@filtersSignal()
public declare readonly filters: FiltersSignal<this>;
@initial('#0000')
@colorProperty()
public declare readonly shadowColor: ColorProperty<this>;
@colorSignal()
public declare readonly shadowColor: ColorSignal<this>;
@initial(0)
@property()
public declare readonly shadowBlur: Signal<number, this>;
@signal()
public declare readonly shadowBlur: SimpleSignal<number, this>;
@vector2Property('shadowOffset')
public declare readonly shadowOffset: Vector2Property<this>;
@vector2Signal('shadowOffset')
public declare readonly shadowOffset: Vector2Signal<this>;
@computed()
protected hasFilters(): boolean {
@@ -472,12 +468,12 @@ export class Node implements Promisable<Node> {
if (!meta.cloneable || key in props) continue;
if (meta.compound) {
for (const [key, property] of meta.compoundEntries) {
props[property] = (<Record<string, Signal<any>>>(<unknown>signal))[
key
].raw();
props[property] = (<Record<string, SimpleSignal<any>>>(
(<unknown>signal)
))[key].context.raw();
}
} else {
props[key] = signal.raw();
props[key] = signal.context.raw();
}
}
@@ -802,7 +798,7 @@ export class Node implements Promisable<Node> {
*/
public waitForAsyncResources() {
this.collectAsyncResources();
const promises = consumePromises();
const promises = DependencyContext.consumePromises();
return Promise.all(promises.map(handle => handle.promise));
}
@@ -823,7 +819,7 @@ export class Node implements Promisable<Node> {
public *[Symbol.iterator]() {
for (const key in this.properties) {
const meta = this.properties[key];
const signal = (<Record<string, Signal<any>>>(<unknown>this))[key];
const signal = (<Record<string, SimpleSignal<any>>>(<unknown>this))[key];
yield {meta, signal, key};
}
}

View File

@@ -1,16 +1,20 @@
import {SignalValue} from '@motion-canvas/core/lib/utils';
import {PossibleSpacing, Rect as RectType} from '@motion-canvas/core/lib/types';
import {
PossibleSpacing,
Rect as RectType,
SpacingSignal,
} from '@motion-canvas/core/lib/types';
import {Shape, ShapeProps} from './Shape';
import {drawRoundRect} from '../utils';
import {spacingProperty, SpacingProperty} from '../decorators/spacingProperty';
import {spacingSignal} from '../decorators/spacingSignal';
import {SignalValue} from '@motion-canvas/core/lib/signals';
export interface RectProps extends ShapeProps {
radius?: SignalValue<PossibleSpacing>;
}
export class Rect extends Shape {
@spacingProperty('radius')
public declare readonly radius: SpacingProperty<this>;
@spacingSignal('radius')
public declare readonly radius: SpacingSignal<this>;
public constructor(props: RectProps) {
super(props);

View File

@@ -1,17 +1,19 @@
import {PossibleCanvasStyle} from '../partials';
import {
computed,
initial,
property,
CanvasStyleProperty,
canvasStyleProperty,
} from '../decorators';
import {createSignal, Signal, SignalValue} from '@motion-canvas/core/lib/utils';
import {computed, initial, signal} from '../decorators';
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} from '../utils';
import {
canvasStyleSignal,
CanvasStyleSignal,
} from '../decorators/canvasStyleSignal';
import {
createSignal,
SignalValue,
SimpleSignal,
} from '@motion-canvas/core/lib/signals';
export interface ShapeProps extends LayoutProps {
fill?: SignalValue<PossibleCanvasStyle>;
@@ -25,28 +27,28 @@ export interface ShapeProps extends LayoutProps {
}
export abstract class Shape extends Layout {
@canvasStyleProperty()
public declare readonly fill: CanvasStyleProperty<this>;
@canvasStyleProperty()
public declare readonly stroke: CanvasStyleProperty<this>;
@canvasStyleSignal()
public declare readonly fill: CanvasStyleSignal<this>;
@canvasStyleSignal()
public declare readonly stroke: CanvasStyleSignal<this>;
@initial(false)
@property()
public declare readonly strokeFirst: Signal<boolean, this>;
@signal()
public declare readonly strokeFirst: SimpleSignal<boolean, this>;
@initial(0)
@property()
public declare readonly lineWidth: Signal<number, this>;
@signal()
public declare readonly lineWidth: SimpleSignal<number, this>;
@initial('miter')
@property()
public declare readonly lineJoin: Signal<CanvasLineJoin, this>;
@signal()
public declare readonly lineJoin: SimpleSignal<CanvasLineJoin, this>;
@initial('butt')
@property()
public declare readonly lineCap: Signal<CanvasLineCap, this>;
@signal()
public declare readonly lineCap: SimpleSignal<CanvasLineCap, this>;
@initial([])
@property()
public declare readonly lineDash: Signal<number[], this>;
@signal()
public declare readonly lineDash: SimpleSignal<number[], this>;
@initial(0)
@property()
public declare readonly lineDashOffset: Signal<number, this>;
@signal()
public declare readonly lineDashOffset: SimpleSignal<number, this>;
protected readonly rippleStrength = createSignal<number, this>(0);

View File

@@ -1,8 +1,9 @@
import {initial, interpolation, property} from '../decorators';
import {Signal, SignalValue, useLogger} from '@motion-canvas/core/lib/utils';
import {initial, interpolation, signal} from '../decorators';
import {useLogger} from '@motion-canvas/core/lib/utils';
import {textLerp} from '@motion-canvas/core/lib/tweening';
import {Shape, ShapeProps} from './Shape';
import {Rect} from '@motion-canvas/core/lib/types';
import {SignalValue, SimpleSignal} from '@motion-canvas/core/lib/signals';
export interface TextProps extends ShapeProps {
children?: string;
@@ -24,8 +25,8 @@ export class Text extends Shape {
@initial('')
@interpolation(textLerp)
@property()
public declare readonly text: Signal<string, this>;
@signal()
public declare readonly text: SimpleSignal<string, this>;
public constructor({children, ...rest}: TextProps) {
super(rest);

View File

@@ -3,18 +3,17 @@ import {
SerializedVector2,
} from '@motion-canvas/core/lib/types';
import {drawImage} from '../utils';
import {computed, initial, property} from '../decorators';
import {
collectPromise,
Signal,
SignalValue,
useProject,
useThread,
} from '@motion-canvas/core/lib/utils';
import {computed, initial, signal} from '../decorators';
import {useProject, useThread} from '@motion-canvas/core/lib/utils';
import {PlaybackState} from '@motion-canvas/core';
import {clamp} from '@motion-canvas/core/lib/tweening';
import {Rect, RectProps} from './Rect';
import {Length} from '../partials';
import {
DependencyContext,
SignalValue,
SimpleSignal,
} from '@motion-canvas/core/lib/signals';
export interface VideoProps extends RectProps {
src?: SignalValue<string>;
@@ -27,24 +26,24 @@ export interface VideoProps extends RectProps {
export class Video extends Rect {
private static readonly pool: Record<string, HTMLVideoElement> = {};
@property()
public declare readonly src: Signal<string, this>;
@signal()
public declare readonly src: SimpleSignal<string, this>;
@initial(1)
@property()
public declare readonly alpha: Signal<number, this>;
@signal()
public declare readonly alpha: SimpleSignal<number, this>;
@initial(true)
@property()
public declare readonly smoothing: Signal<boolean, this>;
@signal()
public declare readonly smoothing: SimpleSignal<boolean, this>;
@initial(0)
@property()
protected declare readonly time: Signal<number, this>;
@signal()
protected declare readonly time: SimpleSignal<number, this>;
@initial(false)
@property()
protected declare readonly playing: Signal<boolean, this>;
@signal()
protected declare readonly playing: SimpleSignal<boolean, this>;
private lastTime = -1;
@@ -81,7 +80,7 @@ export class Video extends Rect {
const video = document.createElement('video');
video.src = src;
if (video.readyState < 2) {
collectPromise(
DependencyContext.collectPromise(
new Promise<void>(resolve => {
const listener = () => {
resolve();
@@ -125,7 +124,7 @@ export class Video extends Rect {
const playing = this.playing() && time < video.duration;
if (playing) {
if (video.paused) {
collectPromise(video.play());
DependencyContext.collectPromise(video.play());
}
} else {
if (!video.paused) {
@@ -181,7 +180,7 @@ export class Video extends Rect {
video.currentTime = value;
this.lastTime = value;
if (video.seeking) {
collectPromise(
DependencyContext.collectPromise(
new Promise<void>(resolve => {
const listener = () => {
resolve();

View File

@@ -1,17 +0,0 @@
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);
};
}

View File

@@ -0,0 +1,14 @@
import {initial, parser, signal} from './signal';
import {canvasStyleParser} from '../utils';
import type {CanvasStyle, PossibleCanvasStyle} from '../partials';
import {Signal} from '@motion-canvas/core/lib/signals';
export type CanvasStyleSignal<T> = Signal<PossibleCanvasStyle, CanvasStyle, T>;
export function canvasStyleSignal(): PropertyDecorator {
return (target, key) => {
signal()(target, key);
parser(canvasStyleParser)(target, key);
initial(null)(target, key);
};
}

View File

@@ -1,11 +0,0 @@
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);
};
}

View File

@@ -0,0 +1,9 @@
import {Color} from '@motion-canvas/core/lib/types';
import {signal, wrapper} from './signal';
export function colorSignal(): PropertyDecorator {
return (target, key) => {
signal()(target, key);
wrapper(Color)(target, key);
};
}

View File

@@ -1,12 +1,9 @@
import {
SignalValue,
isReactive,
useLogger,
capitalize,
} from '@motion-canvas/core/lib/utils';
import {createProperty, getPropertyMetaOrCreate, Property} from './property';
import {useLogger} from '@motion-canvas/core/lib/utils';
import {getPropertyMetaOrCreate} from './signal';
import {addInitializer} from './initializers';
import {deepLerp} from '@motion-canvas/core/lib/tweening';
import {CompoundSignalContext} from '@motion-canvas/core/lib/signals';
import {patchSignal} from '../utils/patchSignal';
/**
* Create a compound property decorator.
@@ -43,85 +40,25 @@ export function compound(entries: Record<string, string>): PropertyDecorator {
useLogger().error(`Missing parser decorator for "${key.toString()}"`);
return;
}
const parser = meta.parser;
const initial = context.defaults[key] ?? meta.default;
const initialWrapped: SignalValue<any> = isReactive(initial)
? () => parser(initial())
: parser(initial);
const signals: [string, Property<any, any, any>][] = [];
const signalContext = new CompoundSignalContext(
Object.keys(entries),
meta.parser,
context.defaults[key] ?? meta.default,
meta.interpolationFunction ?? deepLerp,
instance,
);
patchSignal(signalContext, meta.parser, instance, <string>key);
const signal = signalContext.toSignal();
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(
signals.map(([key, property]) => [key, property()]),
);
return parser(object);
}
function setter(value: SignalValue<any>) {
if (isReactive(value)) {
for (const [key, property] of signals) {
property(() => value()[key]);
}
} else {
for (const [key, property] of signals) {
property(value[key]);
}
patchSignal(signal[key].context, undefined, instance, property);
if (property in context.defaults) {
signal[key].context.setInitial(context.defaults[property]);
}
}
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;
instance[key] = signal;
});
};
}

View File

@@ -1,5 +1,5 @@
import {addInitializer} from './initializers';
import {createComputed} from '@motion-canvas/core/lib/utils/createComputed';
import {createComputed} from '@motion-canvas/core/lib/signals';
/**
* Create a computed method decorator.

View File

@@ -1,185 +0,0 @@
import {getPropertyMetaOrCreate, PropertyOwner} from './property';
import {Filter, FilterName, FILTERS} from '../partials';
import {
createSignal,
isReactive,
Signal,
SignalGenerator,
SignalValue,
} from '@motion-canvas/core/lib/utils';
import {easeInOutCubic, TimingFunction} from '@motion-canvas/core/lib/tweening';
import {ThreadGenerator} from '@motion-canvas/core/lib/threading';
import {decorate, threadable} from '@motion-canvas/core/lib/decorators';
import {all} from '@motion-canvas/core/lib/flow';
import {addInitializer} from './initializers';
export type FiltersProperty = Signal<Filter[]> & {
[K in FilterName]: Signal<number>;
};
export function createFiltersProperty<
TNode extends PropertyOwner<Filter[], Filter[]>,
TProperty extends string & keyof TNode,
>(
node: TNode,
property: TProperty,
initial: SignalValue<Filter[]> = [],
): FiltersProperty {
const signal = createSignal(initial, undefined, node);
const handler = <Signal<Filter[], TNode>>(
function (
newValue?: SignalValue<Filter[]>,
duration?: number,
timingFunction: TimingFunction = easeInOutCubic,
) {
if (newValue === undefined) {
return signal();
}
if (duration === undefined) {
return signal(newValue);
}
return makeAnimate(timingFunction)(newValue, duration);
}
);
function makeAnimate(
defaultTimingFunction: TimingFunction,
before?: ThreadGenerator,
) {
function animate(
value: SignalValue<Filter[]>,
duration: number,
timingFunction = defaultTimingFunction,
) {
const task = <SignalGenerator<Filter[]>>(
makeTask(value, duration, timingFunction, before)
);
task.to = makeAnimate(timingFunction, task);
return task;
}
return animate;
}
decorate(<any>makeTask, threadable());
function* makeTask(
value: SignalValue<Filter[]>,
duration: number,
timingFunction: TimingFunction,
before?: ThreadGenerator,
) {
if (before) {
yield* before;
}
const from = signal();
const to = isReactive(value) ? value() : value;
if (areFiltersCompatible(from, to)) {
yield* all(
...from.map((filter, i) =>
filter.value(to[i].value(), duration, timingFunction),
),
);
signal(to);
return;
}
for (const filter of to) {
filter.value(filter.default);
}
const toValues = to.map(filter => filter.value.raw());
const partialDuration =
from.length > 0 && to.length > 0 ? duration / 2 : duration;
if (from.length > 0) {
yield* all(
...from.map(filter =>
filter.value(filter.default, partialDuration, timingFunction),
),
);
}
signal(to);
if (to.length > 0) {
yield* all(
...to.map((filter, index) =>
filter.value(toValues[index], partialDuration, timingFunction),
),
);
}
}
Object.defineProperty(handler, 'reset', {
configurable: true,
value: signal.reset,
});
Object.defineProperty(handler, 'save', {
configurable: true,
value: signal.save,
});
Object.defineProperty(handler, 'raw', {
value: signal.raw,
});
for (const filter in FILTERS) {
const props = FILTERS[filter];
Object.defineProperty(handler, filter, {
value: (
newValue?: SignalValue<number>,
duration?: number,
timingFunction: TimingFunction = easeInOutCubic,
) => {
if (newValue === undefined) {
return (
signal()?.find(filter => filter.name === props.name) ??
props.default
);
}
let instance = signal()?.find(filter => filter.name === props.name);
if (!instance) {
instance = new Filter(props);
signal([...signal(), instance]);
}
if (duration === undefined) {
instance.value(newValue);
return node;
}
return instance.value(newValue, duration, timingFunction);
},
});
}
return <FiltersProperty>(<unknown>handler);
}
export function filtersProperty<T>(): PropertyDecorator {
return (target: any, key) => {
const meta = getPropertyMetaOrCreate<T>(target, key);
addInitializer(target, (instance: any, context: any) => {
instance[key] = createFiltersProperty(
instance,
<string>key,
context.defaults[key] ?? meta.default,
);
});
};
}
function areFiltersCompatible(a: Filter[], b: Filter[]) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i].name !== b[i].name) {
return false;
}
}
return true;
}

View File

@@ -0,0 +1,132 @@
import {getPropertyMetaOrCreate} from './signal';
import {Filter, FilterName, FILTERS} from '../partials';
import {
deepLerp,
easeInOutCubic,
TimingFunction,
} from '@motion-canvas/core/lib/tweening';
import {ThreadGenerator} from '@motion-canvas/core/lib/threading';
import {all} from '@motion-canvas/core/lib/flow';
import {addInitializer} from './initializers';
import {
isReactive,
Signal,
SignalContext,
SignalValue,
SimpleSignal,
} from '@motion-canvas/core/lib/signals';
export type FiltersSignal<TOwner> = Signal<
Filter[],
Filter[],
TOwner,
FiltersSignalContext<TOwner>
> & {
[K in FilterName]: SimpleSignal<number, TOwner>;
};
export class FiltersSignalContext<TOwner> extends SignalContext<
Filter[],
Filter[],
TOwner
> {
public constructor(initial: Filter[], owner: TOwner) {
super(initial, deepLerp, owner);
for (const filter in FILTERS) {
const props = FILTERS[filter];
Object.defineProperty(this.invokable, filter, {
value: (
newValue?: SignalValue<number>,
duration?: number,
timingFunction: TimingFunction = easeInOutCubic,
) => {
if (newValue === undefined) {
return (
this.get()?.find(filter => filter.name === props.name) ??
props.default
);
}
let instance = this.get()?.find(filter => filter.name === props.name);
if (!instance) {
instance = new Filter(props);
this.set([...this.get(), instance]);
}
if (duration === undefined) {
instance.value(newValue);
return this.owner;
}
return instance.value(newValue, duration, timingFunction);
},
});
}
}
public override *doTween(
value: SignalValue<Filter[]>,
duration: number,
timingFunction: TimingFunction,
): ThreadGenerator {
const from = this.get();
const to = isReactive(value) ? value() : value;
if (areFiltersCompatible(from, to)) {
yield* all(
...from.map((filter, i) =>
filter.value(to[i].value(), duration, timingFunction),
),
);
this.set(to);
return;
}
for (const filter of to) {
filter.value(filter.default);
}
const toValues = to.map(filter => filter.value.context.raw());
const partialDuration =
from.length > 0 && to.length > 0 ? duration / 2 : duration;
if (from.length > 0) {
yield* all(
...from.map(filter =>
filter.value(filter.default, partialDuration, timingFunction),
),
);
}
this.set(to);
if (to.length > 0) {
yield* all(
...to.map((filter, index) =>
filter.value(toValues[index]!, partialDuration, timingFunction),
),
);
}
}
}
export function filtersSignal<T>(): PropertyDecorator {
return (target: any, key) => {
const meta = getPropertyMetaOrCreate<T>(target, key);
addInitializer(target, (instance: any, context: any) => {
instance[key] = new FiltersSignalContext(
context.defaults[key] ?? meta.default ?? [],
instance,
).toSignal();
});
};
}
function areFiltersCompatible(a: Filter[], b: Filter[]) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i].name !== b[i].name) {
return false;
}
}
return true;
}

View File

@@ -1,8 +1,8 @@
export * from './canvasStyleProperty';
export * from './colorProperty';
export * from './canvasStyleSignal';
export * from './colorSignal';
export * from './compound';
export * from './computed';
export * from './filtersProperty';
export * from './filtersSignal';
export * from './initializers';
export * from './property';
export * from './vector2Property';
export * from './signal';
export * from './vector2Signal';

View File

@@ -1,25 +1,11 @@
import {
deepLerp,
easeInOutCubic,
TimingFunction,
tween,
InterpolationFunction,
} from '@motion-canvas/core/lib/tweening';
import {addInitializer} from './initializers';
import {
SignalValue,
isReactive,
createSignal,
SignalGetter,
SignalSetter,
SignalTween,
SignalUtils,
useLogger,
SignalGenerator,
capitalize,
} from '@motion-canvas/core/lib/utils';
import {decorate, threadable} from '@motion-canvas/core/lib/decorators';
import {ThreadGenerator} from '@motion-canvas/core/lib/threading';
import {useLogger} from '@motion-canvas/core/lib/utils';
import {patchSignal} from '../utils/patchSignal';
import {SignalContext} from '@motion-canvas/core/lib/signals';
export interface PropertyMetadata<T> {
default?: T;
@@ -32,176 +18,6 @@ export interface PropertyMetadata<T> {
compoundEntries: [string, string][];
}
export interface Property<
TSetterValue,
TGetterValue extends TSetterValue,
TOwner,
> extends SignalGetter<TGetterValue>,
SignalSetter<TSetterValue>,
SignalTween<TSetterValue>,
SignalUtils<TSetterValue, TOwner> {}
export type PropertyOwner<TGetterValue, TSetterValue> = {
[key: `get${Capitalize<string>}`]: SignalGetter<TGetterValue> | undefined;
[key: `set${Capitalize<string>}`]: SignalSetter<TSetterValue> | undefined;
[key: `tween${Capitalize<string>}`]: SignalTween<TGetterValue> | undefined;
};
export function createProperty<
TSetterValue,
TGetterValue extends TSetterValue,
TNode extends PropertyOwner<TGetterValue, TSetterValue>,
TProperty extends string & keyof TNode,
>(
node: TNode,
property: TProperty,
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>;
if (!originalGetter !== !originalSetter) {
useLogger().warn(
`The "${property}" property needs to provide either both the setter and getter or none of them`,
);
}
let wrap: (value: SignalValue<TSetterValue>) => SignalValue<TGetterValue>;
let unwrap: (value: SignalValue<TSetterValue>) => TGetterValue;
if (parser) {
wrap = value => (isReactive(value) ? () => parser(value()) : parser(value));
unwrap = value => parser(isReactive(value) ? value() : value);
} else {
wrap = value => <SignalValue<TGetterValue>>value;
unwrap = value => <TGetterValue>(isReactive(value) ? value() : value);
}
let signal: Property<TSetterValue, TGetterValue, TNode> | null = null;
if (!originalGetter || !originalSetter) {
signal = <Property<TSetterValue, TGetterValue, TNode>>(
(<unknown>(
createSignal(
wrap(<SignalValue<TSetterValue>>initial),
defaultInterpolation,
node,
)
))
);
if (!tweener && !parser) {
return signal;
}
getter = signal;
setter = signal;
} else {
getter = originalGetter.bind(node);
setter = (...args) => {
originalSetter.apply(node, args);
return node;
};
}
const handler = <Property<TSetterValue, TGetterValue, TNode>>(
function (
newValue?: SignalValue<TSetterValue>,
duration?: number,
timingFunction: TimingFunction = easeInOutCubic,
interpolationFunction: InterpolationFunction<TGetterValue> = defaultInterpolation,
) {
if (newValue === undefined) {
return getter();
}
if (duration === undefined) {
return setter(wrap(newValue));
}
return makeAnimate(timingFunction, interpolationFunction)(
<TGetterValue>newValue,
duration,
);
}
);
function makeAnimate(
defaultTimingFunction: TimingFunction,
defaultInterpolationFunction: InterpolationFunction<TGetterValue>,
before?: ThreadGenerator,
) {
function animate(
value: SignalValue<TGetterValue>,
duration: number,
timingFunction = defaultTimingFunction,
interpolationFunction = defaultInterpolationFunction,
) {
const task = <SignalGenerator<TGetterValue>>(
makeTask(value, duration, timingFunction, interpolationFunction, before)
);
task.to = makeAnimate(timingFunction, interpolationFunction, task);
return task;
}
return <SignalTween<TGetterValue>>animate;
}
decorate(<any>makeTask, threadable());
function* makeTask(
value: SignalValue<TGetterValue>,
duration: number,
timingFunction: TimingFunction,
interpolationFunction: InterpolationFunction<TGetterValue>,
before?: ThreadGenerator,
) {
if (before) {
yield* before;
}
if (tweener) {
yield* tweener.call(
node,
wrap(value),
duration,
timingFunction,
interpolationFunction,
);
} else {
const from = getter();
yield* tween(
duration,
v => {
setter(interpolationFunction(from, unwrap(value), timingFunction(v)));
},
() => setter(wrap(value)),
);
}
}
Object.defineProperty(handler, 'reset', {
configurable: true,
value: signal
? signal.reset
: initial !== undefined
? () => setter(wrap(initial))
: () => node,
});
Object.defineProperty(handler, 'save', {
configurable: true,
value: () => setter(getter()),
});
Object.defineProperty(handler, 'raw', {
value: signal?.raw ?? getter,
});
return handler;
}
const PROPERTIES = Symbol.for('@motion-canvas/2d/decorators/properties');
export function getPropertyMeta<T>(
@@ -250,7 +66,7 @@ export function getPropertiesOf(
}
/**
* Create a signal property decorator.
* Create a signal decorator.
*
* @remarks
* This decorator turns the given property into a signal.
@@ -260,8 +76,6 @@ export function getPropertiesOf(
* - `get[PropertyName]` - A property setter.
* - `tween[PropertyName]` - A tween provider.
*
* See the {@link PropertyOwner} type for more detailed method signatures.
*
* @example
* ```ts
* class Example {
@@ -270,31 +84,28 @@ export function getPropertiesOf(
* }
* ```
*/
export function property<T>(): PropertyDecorator {
export function signal<T>(): PropertyDecorator {
return (target: any, key) => {
const meta = getPropertyMetaOrCreate<T>(target, key);
addInitializer(target, (instance: any, context: any) => {
instance[key] = createProperty(
instance,
<string>key,
const signal = new SignalContext<T, T, any>(
context.defaults[key] ?? meta.default,
meta.interpolationFunction ?? deepLerp,
meta.parser,
instance[`get${capitalize(<string>key)}`],
instance[`set${capitalize(<string>key)}`],
instance[`tween${capitalize(<string>key)}`],
instance,
);
patchSignal(signal, meta.parser, instance, <string>key);
instance[key] = signal.toSignal();
});
};
}
/**
* Create an initial property value decorator.
* Create an initial signal value decorator.
*
* @remarks
* This decorator specifies the initial value of a property.
*
* Must be specified before the {@link property} decorator.
* Must be specified before the {@link signal} decorator.
*
* @example
* ```ts
@@ -319,13 +130,13 @@ export function initial<T>(value: T): PropertyDecorator {
}
/**
* Create a property interpolation function decorator.
* Create a signal interpolation function decorator.
*
* @remarks
* This decorator specifies the interpolation function of a property.
* The interpolation function is used when tweening between different values.
*
* Must be specified before the {@link property} decorator.
* Must be specified before the {@link signal} decorator.
*
* @example
* ```ts
@@ -352,7 +163,7 @@ export function interpolation<T>(
}
/**
* Create a property parser decorator.
* Create a signal parser decorator.
*
* @remarks
* This decorator specifies the parser of a property.
@@ -362,7 +173,7 @@ export function interpolation<T>(
* If the wrapper class has a method called `lerp` it will be set as the
* default interpolation function for the property.
*
* Must be specified before the {@link property} decorator.
* Must be specified before the {@link signal} decorator.
*
* @example
* ```ts
@@ -387,7 +198,7 @@ export function parser<T>(value: (value: any) => T): PropertyDecorator {
}
/**
* Create a property wrapper decorator.
* Create a signal wrapper decorator.
*
* @remarks
* This is a shortcut decorator for setting both the {@link parser} and
@@ -396,7 +207,7 @@ export function parser<T>(value: (value: any) => T): PropertyDecorator {
* 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.
* Must be specified before the {@link signal} decorator.
*
* @example
* ```ts
@@ -440,7 +251,7 @@ export function wrapper<T>(
*
* By default, any property is cloneable.
*
* Must be specified before the {@link property} decorator.
* Must be specified before the {@link signal} decorator.
*
* @example
* ```ts
@@ -473,7 +284,7 @@ export function cloneable<T>(value = true): PropertyDecorator {
*
* By default, any property is inspectable.
*
* Must be specified before the {@link property} decorator.
* Must be specified before the {@link signal} decorator.
*
* @example
* ```ts

View File

@@ -1,23 +0,0 @@
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);
};
}

View File

@@ -0,0 +1,15 @@
import {Spacing} from '@motion-canvas/core/lib/types';
import {compound} from './compound';
import {wrapper} from './signal';
export function spacingSignal(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);
};
}

View File

@@ -1,29 +0,0 @@
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);
};
}

View File

@@ -0,0 +1,30 @@
import {PossibleVector2, Vector2} from '@motion-canvas/core/lib/types/Vector';
import {compound} from './compound';
import type {Length} from '../partials';
import {wrapper} from './signal';
import {Signal} from '@motion-canvas/core/lib/signals';
export type Vector2LengthSignal<TOwner> = Signal<
PossibleVector2<Length>,
Vector2,
TOwner
> & {
x: Signal<Length, number, TOwner>;
y: Signal<Length, number, TOwner>;
};
export function vector2Signal(
prefix?: string | Record<string, string>,
): PropertyDecorator {
return (target, key) => {
compound(
typeof prefix === 'object'
? prefix
: {
x: prefix ? `${prefix}X` : 'x',
y: prefix ? `${prefix}Y` : 'y',
},
)(target, key);
wrapper(Vector2)(target, key);
};
}

View File

@@ -1,4 +1,8 @@
import {createSignal, Signal, SignalValue} from '@motion-canvas/core/lib/utils';
import {
createSignal,
SignalValue,
SimpleSignal,
} from '@motion-canvas/core/lib/signals';
import {map} from '@motion-canvas/core/lib/tweening';
import {transformScalar} from '@motion-canvas/core/lib/types';
@@ -78,7 +82,7 @@ export class Filter {
return this.props.default;
}
public readonly value: Signal<number, Filter>;
public readonly value: SimpleSignal<number, Filter>;
private readonly props: FilterProps;
public constructor(props: Partial<FilterProps>) {

View File

@@ -1,13 +1,14 @@
import {initial, signal} from '../decorators/signal';
import {vector2Signal} from '../decorators/vector2Signal';
import {computed} from '../decorators/computed';
import {initialize} from '../decorators/initializers';
import {
computed,
initial,
initialize,
property,
Vector2Property,
vector2Property,
} from '../decorators';
import {Color, PossibleColor, Vector2} from '@motion-canvas/core/lib/types';
import {Signal} from '@motion-canvas/core/lib/utils';
Color,
PossibleColor,
Vector2,
Vector2Signal,
} from '@motion-canvas/core/lib/types';
import {SimpleSignal} from '@motion-canvas/core/lib/signals';
export type GradientType = 'linear' | 'conic' | 'radial';
@@ -32,27 +33,27 @@ export interface GradientProps {
export class Gradient {
@initial('linear')
@property()
public declare readonly type: Signal<GradientType, this>;
@signal()
public declare readonly type: SimpleSignal<GradientType, this>;
@vector2Property('from')
public declare readonly from: Vector2Property<this>;
@vector2Signal('from')
public declare readonly from: Vector2Signal<this>;
@vector2Property('to')
public declare readonly to: Vector2Property<this>;
@vector2Signal('to')
public declare readonly to: Vector2Signal<this>;
@initial(0)
@property()
public declare readonly angle: Signal<number, this>;
@signal()
public declare readonly angle: SimpleSignal<number, this>;
@initial(0)
@property()
public declare readonly fromRadius: Signal<number, this>;
@signal()
public declare readonly fromRadius: SimpleSignal<number, this>;
@initial(0)
@property()
public declare readonly toRadius: Signal<number, this>;
@signal()
public declare readonly toRadius: SimpleSignal<number, this>;
@initial([])
@property()
public declare readonly stops: Signal<GradientStop[], this>;
@signal()
public declare readonly stops: SimpleSignal<GradientStop[], this>;
public constructor(props: GradientProps) {
initialize(this, {defaults: props});

View File

@@ -1,5 +1,7 @@
import {computed, initial, initialize, property} from '../decorators';
import {Signal} from '@motion-canvas/core/lib/utils';
import {initial, signal} from '../decorators/signal';
import {computed} from '../decorators/computed';
import {initialize} from '../decorators/initializers';
import {SimpleSignal} from '@motion-canvas/core/lib/signals';
export type CanvasRepetition =
| null
@@ -15,11 +17,11 @@ export interface PatternProps {
}
export class Pattern {
@property()
public declare readonly image: Signal<CanvasImageSource, this>;
@signal()
public declare readonly image: SimpleSignal<CanvasImageSource, this>;
@initial(null)
@property()
public declare readonly repetition: Signal<CanvasRepetition, this>;
@signal()
public declare readonly repetition: SimpleSignal<CanvasRepetition, this>;
public constructor(props: PatternProps) {
initialize(this, {defaults: props});

View File

@@ -8,19 +8,11 @@ import {
SceneRenderEvent,
ThreadGeneratorFactory,
} from '@motion-canvas/core/lib/scenes';
import {endScene, startScene, useScene} from '@motion-canvas/core/lib/utils';
import {endScene, startScene} from '@motion-canvas/core/lib/utils';
import {Vector2} from '@motion-canvas/core/lib/types';
import {Node, View2D} from '../components';
import {Meta} from '@motion-canvas/core';
export function useScene2D(): Scene2D | null {
const scene = useScene();
if (scene instanceof Scene2D) {
return scene;
}
return null;
}
export class Scene2D extends GeneratorScene<View2D> implements Inspectable {
private readonly view: View2D;
private registeredNodes: Record<string, Node> = {};

View File

@@ -1,2 +1,3 @@
export * from './makeScene2D';
export * from './Scene2D';
export * from './useScene2D';

View File

@@ -0,0 +1,6 @@
import {useScene} from '@motion-canvas/core/lib/utils';
import type {Scene2D} from './Scene2D';
export function useScene2D(): Scene2D {
return <Scene2D>useScene();
}

View File

@@ -0,0 +1,28 @@
import {capitalize} from '@motion-canvas/core/lib/utils';
import {SignalContext} from '@motion-canvas/core/lib/signals';
export function patchSignal<TSetterValue, TValue extends TSetterValue>(
signal: SignalContext<TSetterValue, TValue>,
parser?: (value: TSetterValue) => TValue,
owner?: any,
name?: string,
) {
if (parser) {
signal.setParser(parser);
}
if (name && owner) {
const setter = owner?.[`set${capitalize(name)}`];
if (setter) {
signal.set = setter.bind(owner);
}
const getter = owner?.[`get${capitalize(name)}`];
if (getter) {
signal.get = getter.bind(owner);
}
const tweener = owner?.[`tween${capitalize(name)}`];
if (tweener) {
signal.doTween = tweener.bind(owner);
}
}
}

View File

@@ -3,8 +3,9 @@ import {Meta, Metadata} from './Meta';
import {EventDispatcher, ValueDispatcher} from './events';
import {CanvasColorSpace, CanvasOutputMimeType, Vector2} from './types';
import {AudioManager} from './media';
import {createSignal, getContext} from './utils';
import {getContext} from './utils';
import {Logger} from './Logger';
import {createSignal} from './signals';
const EXPORT_FRAME_LIMIT = 256;
const EXPORT_RETRY_DELAY = 1000;

View File

@@ -10,13 +10,14 @@ import {TimeEvents} from './TimeEvents';
import {EventDispatcher, ValueDispatcher} from '../events';
import {Project} from '../Project';
import {decorate, threadable} from '../decorators';
import {consumePromises, endScene, setProject, startScene} from '../utils';
import {endScene, setProject, startScene} from '../utils';
import {CachedSceneData, Scene, SceneMetadata, SceneRenderEvent} from './Scene';
import {LifecycleEvents} from './LifecycleEvents';
import {Threadable} from './Threadable';
import {Rect, Vector2} from '../types';
import {SceneState} from './SceneState';
import {Random} from './Random';
import {DependencyContext} from '../signals';
export interface ThreadGeneratorFactory<T> {
(view: T): ThreadGenerator;
@@ -133,7 +134,7 @@ export abstract class GeneratorScene<T>
}
public async render(context: CanvasRenderingContext2D): Promise<void> {
let promises = consumePromises();
let promises = DependencyContext.consumePromises();
let iterations = 0;
do {
iterations++;
@@ -144,7 +145,7 @@ export abstract class GeneratorScene<T>
this.draw(context);
context.restore();
promises = consumePromises();
promises = DependencyContext.consumePromises();
} while (promises.length > 0 && iterations < 10);
if (iterations > 1) {
@@ -224,7 +225,7 @@ export abstract class GeneratorScene<T>
}
endScene(this);
const promises = consumePromises();
const promises = DependencyContext.consumePromises();
if (promises.length > 0) {
await Promise.all(promises.map(handle => handle.promise));
this.project.logger.error({

View File

@@ -0,0 +1,103 @@
import {InterpolationFunction, map} from '../tweening';
import {Signal, SignalContext} from './SignalContext';
import {SignalValue} from './types';
import {isReactive} from './isReactive';
export type CompoundSignal<
TSetterValue,
TValue extends TSetterValue,
TKeys extends keyof TValue,
TOwner,
TContext = CompoundSignalContext<TSetterValue, TValue, TKeys, TOwner>,
> = Signal<TSetterValue, TValue, TOwner, TContext> & {
[K in TKeys]: Signal<
TValue[K],
TValue[K],
TOwner extends void
? CompoundSignal<TSetterValue, TValue, TKeys, TOwner, TContext>
: TOwner
>;
};
export class CompoundSignalContext<
TSetterValue,
TValue extends TSetterValue,
TKeys extends keyof TValue,
TOwner = void,
> extends SignalContext<TSetterValue, TValue, TOwner> {
public readonly signals: [keyof TValue, Signal<any, any, TOwner>][] = [];
public constructor(
private readonly entries: TKeys[],
parser: (value: TSetterValue) => TValue,
initial: SignalValue<TSetterValue>,
interpolation: InterpolationFunction<TValue>,
owner: TOwner = <TOwner>(<unknown>undefined),
) {
super(undefined, interpolation, owner);
this.parser = parser;
for (const key of entries) {
const signal = new SignalContext(
isReactive(initial)
? () => parser(initial())[key]
: parser(initial)[key],
<any>map,
owner ?? this.invokable,
).toSignal();
this.signals.push([key, signal]);
Object.defineProperty(this.invokable, key, {value: signal});
}
}
public override toSignal(): CompoundSignal<
TSetterValue,
TValue,
TKeys,
TOwner
> {
return this.invokable;
}
public override parse(value: TSetterValue): TValue {
return this.parser(value);
}
public override get(): TValue {
return this.parse(
<TSetterValue>(
Object.fromEntries(
this.signals.map(([key, property]) => [key, property()]),
)
),
);
}
public override set(value: SignalValue<TValue>): TOwner {
if (isReactive(value)) {
for (const [key, property] of this.signals) {
property(() => value()[key]);
}
} else {
for (const [key, property] of this.signals) {
property(value[key]);
}
}
return this.owner;
}
public override reset(): TOwner {
for (const [, signal] of this.signals) {
signal.reset();
}
return this.owner;
}
public override save(): TOwner {
for (const [, signal] of this.signals) {
signal.save();
}
return this.owner;
}
}

View File

@@ -0,0 +1,45 @@
import {useLogger} from '../utils';
import {DependencyContext} from './DependencyContext';
export interface Computed<TValue> {
(...args: any[]): TValue;
context: ComputedContext<TValue>;
}
export class ComputedContext<TValue> extends DependencyContext<any> {
private last: TValue | undefined;
public constructor(
private readonly factory: (...args: any[]) => TValue,
owner?: any,
) {
super(owner);
this.markDirty();
}
public toSignal(): Computed<TValue> {
return this.invokable;
}
protected override invoke(...args: any[]): TValue {
if (this.event.isRaised()) {
this.dependencies.forEach(dep => dep.unsubscribe(this.markDirty));
this.dependencies.clear();
this.startCollecting();
try {
this.last = this.factory(...args);
} catch (e: any) {
useLogger().error({
message: e.message,
stack: e.stack,
inspect: this.owner?.key,
});
}
this.finishCollecting();
}
this.event.reset();
this.collect();
return this.last!;
}
}

View File

@@ -0,0 +1,82 @@
import {FlagDispatcher, Subscribable} from '../events';
export interface PromiseHandle<T> {
promise: Promise<T>;
value: T;
stack?: string;
owner?: any;
}
export class DependencyContext<TOwner = void> {
protected static collectionStack: DependencyContext<any>[] = [];
protected static promises: PromiseHandle<any>[] = [];
public static collectPromise<T>(promise: Promise<T>): PromiseHandle<T | null>;
public static collectPromise<T>(
promise: Promise<T>,
initialValue: T,
): PromiseHandle<T>;
public static collectPromise<T>(
promise: Promise<T>,
initialValue: T | null = null,
): PromiseHandle<T | null> {
const handle: PromiseHandle<T | null> = {
promise,
value: initialValue,
stack: this.collectionStack[0]?.stack,
};
const context = this.collectionStack.at(-2);
if (context) {
handle.owner = context.owner;
}
promise.then(value => {
handle.value = value;
context?.markDirty();
});
this.promises.push(handle);
return handle;
}
public static consumePromises(): PromiseHandle<any>[] {
const result = this.promises;
this.promises = [];
return result;
}
protected readonly invokable: any;
protected dependencies = new Set<Subscribable<void>>();
protected event = new FlagDispatcher();
protected stack: string | undefined;
protected markDirty = () => this.event.raise();
public constructor(protected readonly owner: TOwner) {
this.invokable = this.invoke.bind(this);
}
protected invoke() {
// do nothing
}
protected startCollecting() {
this.stack = new Error().stack;
DependencyContext.collectionStack.push(this);
}
protected finishCollecting() {
this.stack = undefined;
if (DependencyContext.collectionStack.pop() !== this) {
throw new Error('collectStart/collectEnd was called out of order');
}
}
protected collect() {
const signal = DependencyContext.collectionStack.at(-1);
if (signal) {
signal.dependencies.add(this.event.subscribable);
this.event.subscribe(signal.markDirty);
}
}
}

View File

@@ -0,0 +1,290 @@
import {
easeInOutCubic,
InterpolationFunction,
TimingFunction,
tween,
} from '../tweening';
import {useLogger} from '../utils';
import {ThreadGenerator} from '../threading';
import {run} from '../flow';
import {DependencyContext} from './DependencyContext';
import {
SignalGenerator,
SignalGetter,
SignalSetter,
SignalTween,
SignalValue,
} from './types';
import {isReactive} from './isReactive';
export type SimpleSignal<TValue, TReturn = void> = Signal<
TValue,
TValue,
TReturn
>;
export interface Signal<
TSetterValue,
TValue extends TSetterValue,
TOwner = void,
TContext = SignalContext<TSetterValue, TValue, TOwner>,
> extends SignalSetter<TSetterValue, TOwner>,
SignalGetter<TValue>,
SignalTween<TSetterValue, TValue> {
/**
* {@inheritDoc SignalContext.reset}
*/
reset(): TOwner;
/**
* {@inheritDoc SignalContext.save}
*/
save(): TOwner;
context: TContext;
}
export class SignalContext<
TSetterValue,
TValue extends TSetterValue = TSetterValue,
TOwner = void,
> extends DependencyContext<TOwner> {
protected current: SignalValue<TSetterValue> | undefined;
protected last: TValue | undefined;
protected parser: (value: TSetterValue) => TValue = value => <TValue>value;
public constructor(
private initial: SignalValue<TSetterValue> | undefined,
private readonly interpolation: InterpolationFunction<TValue>,
owner: TOwner = <TOwner>(<unknown>undefined),
) {
super(owner);
Object.defineProperty(this.invokable, 'reset', {
value: this.reset.bind(this),
});
Object.defineProperty(this.invokable, 'save', {
value: this.save.bind(this),
});
Object.defineProperty(this.invokable, 'context', {
value: this,
});
if (this.initial !== undefined) {
this.current = this.initial;
this.markDirty();
if (!isReactive(this.initial)) {
this.last = this.parse(this.initial);
}
}
}
public toSignal(): Signal<TSetterValue, TValue, TOwner> {
return this.invokable;
}
public parse(value: TSetterValue): TValue {
return this.parser(value);
}
protected wrap(value: SignalValue<TSetterValue>): SignalValue<TValue> {
return isReactive(value) ? () => this.parse(value()) : this.parse(value);
}
public setInitial(value: SignalValue<TSetterValue>) {
this.initial = value;
}
public setParser(value: (value: TSetterValue) => TValue) {
this.parser = value;
if (this.current !== undefined && !isReactive(this.current)) {
this.last = this.parse(this.current);
}
this.markDirty();
}
public set(value: SignalValue<TSetterValue>): TOwner {
if (this.current === value) {
return this.owner;
}
this.current = value;
this.markDirty();
if (this.dependencies.size > 0) {
this.dependencies.forEach(dep => dep.unsubscribe(this.markDirty));
this.dependencies.clear();
}
if (!isReactive(value)) {
this.last = this.parse(value);
}
return this.owner;
}
public get(): TValue {
if (this.event.isRaised() && isReactive(this.current)) {
this.dependencies.forEach(dep => dep.unsubscribe(this.markDirty));
this.dependencies.clear();
this.startCollecting();
try {
this.last = this.parse(this.current());
} catch (e: any) {
useLogger().error({
message: e.message,
stack: e.stack,
inspect: (<any>this.owner)?.key,
});
}
this.finishCollecting();
}
this.event.reset();
this.collect();
return this.last!;
}
protected override invoke(
value?: SignalValue<TSetterValue>,
duration?: number,
timingFunction: TimingFunction = easeInOutCubic,
interpolationFunction: InterpolationFunction<TValue> = this.interpolation,
) {
if (value === undefined) {
return this.get();
}
if (duration === undefined) {
return this.set(value);
}
return this.makeAnimate(timingFunction, interpolationFunction)(
value,
duration,
);
}
protected makeAnimate(
defaultTimingFunction: TimingFunction,
defaultInterpolationFunction: InterpolationFunction<TValue>,
before?: ThreadGenerator,
) {
const animate = (
value: SignalValue<TSetterValue>,
duration: number,
timingFunction = defaultTimingFunction,
interpolationFunction = defaultInterpolationFunction,
) => {
const tween = this.tween(
value,
duration,
timingFunction,
interpolationFunction,
) as SignalGenerator<TSetterValue, TValue>;
let task = tween;
if (before) {
task = run(function* () {
yield* before;
yield* tween;
}) as SignalGenerator<TSetterValue, TValue>;
}
task.to = this.makeAnimate(timingFunction, interpolationFunction, task);
return task;
};
return <SignalTween<TSetterValue, TValue>>animate;
}
protected *tween(
value: SignalValue<TSetterValue>,
duration: number,
timingFunction: TimingFunction,
interpolationFunction: InterpolationFunction<TValue>,
): ThreadGenerator {
yield* this.doTween(
this.parse(isReactive(value) ? value() : value),
duration,
timingFunction,
interpolationFunction,
);
this.set(value);
}
public *doTween(
value: TValue,
duration: number,
timingFunction: TimingFunction,
interpolationFunction: InterpolationFunction<TValue>,
): ThreadGenerator {
const from = this.get();
yield* tween(duration, v => {
this.set(interpolationFunction(from, value, timingFunction(v)));
});
}
/**
* Reset the signal to its initial value (if one has been set).
*
* @example
* ```ts
* const signal = createSignal(7);
*
* signal.reset();
* // same as:
* signal(7);
* ```
*/
public reset() {
if (this.initial !== undefined) {
this.set(this.initial);
}
return this.owner;
}
/**
* 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());
* ```
*/
public save() {
return this.set(this.get());
}
/**
* Get the raw value of this signal.
*
* @remarks
* If the signal was provided with a factory function, the function itself
* will be returned, without invoking it.
*
* This method can be used to create copies of signals.
*
* @example
* ```ts
* const a = createSignal(2);
* const b = createSignal(() => a);
* // b() == 2
*
* const bClone = createSignal(b.raw());
* // bClone() == 2
*
* a(4);
* // b() == 4
* // bClone() == 4
* ```
*/
public raw() {
return this.current;
}
}

View File

@@ -0,0 +1,8 @@
import {Computed, ComputedContext} from '../signals';
export function createComputed<TValue>(
factory: (...args: any[]) => TValue,
owner?: any,
): Computed<TValue> {
return new ComputedContext<TValue>(factory, owner).toSignal();
}

View File

@@ -1,5 +1,5 @@
import {Computed, createComputed} from './createComputed';
import {collectPromise} from './createSignal';
import {createComputed} from './createComputed';
import {Computed, ComputedContext} from '../signals';
export function createComputedAsync<T>(
factory: () => Promise<T>,
@@ -12,6 +12,8 @@ export function createComputedAsync<T>(
factory: () => Promise<T>,
initial: T | null = null,
): Computed<T | null> {
const handle = createComputed(() => collectPromise(factory(), initial));
const handle = createComputed(() =>
ComputedContext.collectPromise(factory(), initial),
);
return createComputed(() => handle().value);
}

View File

@@ -0,0 +1,14 @@
import {deepLerp, InterpolationFunction} from '../tweening';
import {SignalContext, SimpleSignal, SignalValue} from '../signals';
export function createSignal<TValue, TOwner = void>(
initial?: SignalValue<TValue>,
interpolation: InterpolationFunction<TValue> = deepLerp,
owner?: TOwner,
): SimpleSignal<TValue, TOwner> {
return new SignalContext<TValue, TValue, TOwner>(
initial,
interpolation,
owner,
).toSignal();
}

View File

@@ -0,0 +1,15 @@
/**
* Value wrappers for easy dependency tracking and cache invalidation.
*
* @packageDocumentation
*/
export * from './CompoundSignalContext';
export * from './ComputedContext';
export * from './createComputed';
export * from './createComputedAsync';
export * from './createSignal';
export * from './isReactive';
export * from './SignalContext';
export * from './DependencyContext';
export * from './types';

View File

@@ -0,0 +1,5 @@
import {SignalValue} from './types';
export function isReactive<T>(value: SignalValue<T>): value is () => T {
return typeof value === 'function';
}

View File

@@ -0,0 +1,27 @@
import type {InterpolationFunction, TimingFunction} from '../tweening';
import type {ThreadGenerator} from '../threading';
export type SignalValue<TValue> = TValue | (() => TValue);
export type SignalGenerator<
TSetterValue,
TValue extends TSetterValue,
> = ThreadGenerator & {
to: SignalTween<TSetterValue, TValue>;
};
export interface SignalSetter<TValue, TOwner = void> {
(value: SignalValue<TValue>): TOwner;
}
export interface SignalGetter<TValue> {
(): TValue;
}
export interface SignalTween<TSetterValue, TValue extends TSetterValue> {
(
value: SignalValue<TSetterValue>,
time: number,
timingFunction?: TimingFunction,
interpolationFunction?: InterpolationFunction<TValue>,
): SignalGenerator<TSetterValue, TValue>;
}

View File

@@ -1,12 +1,7 @@
import {GeneratorHelper} from '../helpers';
import {ThreadGenerator} from './ThreadGenerator';
import {
createSignal,
endThread,
startThread,
useLogger,
useProject,
} from '../utils';
import {endThread, startThread, useLogger, useProject} from '../utils';
import {createSignal} from '../signals';
/**
* A class representing an individual thread.

View File

@@ -1,6 +1,7 @@
import {Color, ColorSpace, InterpolationMode, mix} from 'chroma-js';
import type {Type} from './Type';
import type {InterpolationFunction} from '../tweening';
import {Signal, SignalContext, SignalValue} from '../signals';
export type SerializedColor = string;
@@ -10,6 +11,8 @@ export type PossibleColor =
| Color
| {r: number; g: number; b: number; a: number};
export type ColorSignal<T> = Signal<PossibleColor, Color, T>;
declare module 'chroma-js' {
interface Color extends Type {
serialize(): string;
@@ -30,6 +33,10 @@ declare module 'chroma-js' {
colorSpace?: ColorSpace,
): ColorInterface;
createLerp(colorSpace: ColorSpace): InterpolationFunction<ColorInterface>;
createSignal(
initial?: SignalValue<PossibleColor>,
interpolation?: InterpolationFunction<ColorInterface>,
): ColorSignal<void>;
}
interface ChromaStatic {
Color: ColorStatic & (new (color: PossibleColor) => ColorInterface);
@@ -54,6 +61,15 @@ Color.createLerp = Color.prototype.createLerp =
(from: Color | string, to: Color | string, value: number) =>
mix(from, to, value, colorSpace);
Color.createSignal = (
initial?: SignalValue<PossibleColor>,
interpolation: InterpolationFunction<Color> = Color.lerp,
): ColorSignal<void> => {
const context = new SignalContext(initial, interpolation);
context.setParser(value => new Color(value));
return context.toSignal();
};
Color.prototype.toSymbol = () => {
return Color.symbol;
};

View File

@@ -0,0 +1,73 @@
import {describe, expect, test} from 'vitest';
import {Rect, Vector2} from '../types';
import {createSignal} from '../signals';
describe('Rect', () => {
test('Correctly parses values', () => {
const fromUndefined = new Rect();
const fromProperties = new Rect(10, 20, 200, 100);
const fromArray = new Rect([10, 20, 200, 100]);
const fromVectors = new Rect(new Vector2(10, 20), new Vector2(200, 100));
const fromObject = new Rect({x: 10, y: 20, width: 200, height: 100});
expect(fromUndefined).toMatchObject({
x: 0,
y: 0,
width: 0,
height: 0,
});
expect(fromProperties).toMatchObject({
x: 10,
y: 20,
width: 200,
height: 100,
});
expect(fromArray).toMatchObject({
x: 10,
y: 20,
width: 200,
height: 100,
});
expect(fromVectors).toMatchObject({
x: 10,
y: 20,
width: 200,
height: 100,
});
expect(fromObject).toMatchObject({
x: 10,
y: 20,
width: 200,
height: 100,
});
});
test('Interpolates between rectangles', () => {
const a = new Rect(10, 20, 200, 100);
const b = new Rect(20, 40, 400, 200);
const result = Rect.lerp(a, b, 0.5);
expect(result).toMatchObject({
x: 15,
y: 30,
width: 300,
height: 150,
});
});
test('Creates a compound signal', () => {
const width = createSignal(200);
const rect = Rect.createSignal(() => [10, 20, width(), 100]);
expect(rect()).toMatchObject({x: 10, y: 20, width: 200, height: 100});
expect(rect.x()).toBe(10);
expect(rect.y()).toBe(20);
expect(rect.width()).toBe(200);
expect(rect.height()).toBe(100);
width(400);
expect(rect()).toMatchObject({x: 10, y: 20, width: 400, height: 100});
expect(rect.width()).toBe(400);
});
});

View File

@@ -1,7 +1,8 @@
import {Vector2} from './Vector';
import {arcLerp, map} from '../tweening';
import {arcLerp, InterpolationFunction, map} from '../tweening';
import {Type} from './Type';
import {Spacing} from './Spacing';
import {CompoundSignal, CompoundSignalContext, SignalValue} from '../signals';
export type SerializedRect = {
x: number;
@@ -13,7 +14,15 @@ export type SerializedRect = {
export type PossibleRect =
| SerializedRect
| [number, number, number, number]
| Vector2;
| Vector2
| undefined;
export type RectSignal<T> = CompoundSignal<
PossibleRect,
Rect,
'x' | 'y' | 'width' | 'height',
T
>;
export class Rect implements Type {
public static readonly symbol = Symbol.for('@motion-canvas/core/types/Rect');
@@ -23,6 +32,18 @@ export class Rect implements Type {
public width = 0;
public height = 0;
public static createSignal(
initial?: SignalValue<PossibleRect>,
interpolation: InterpolationFunction<Rect> = Rect.lerp,
): RectSignal<void> {
return new CompoundSignalContext(
['x', 'y', 'width', 'height'],
(value: PossibleRect) => new Rect(value),
initial,
interpolation,
).toSignal();
}
public static lerp(
from: Rect,
to: Rect,

View File

@@ -0,0 +1,96 @@
import {describe, expect, test} from 'vitest';
import {Spacing} from '../types';
import {createSignal} from '../signals';
describe('Spacing', () => {
test('Correctly parses values', () => {
const fromUndefined = new Spacing();
const fromArray = new Spacing([1, 2, 3, 4]);
const fromOne = new Spacing(1);
const fromTwo = new Spacing(1, 2);
const fromThree = new Spacing(1, 2, 3);
const fromObject = new Spacing({
top: 1,
right: 2,
bottom: 3,
left: 4,
});
expect(fromUndefined).toMatchObject({
top: 0,
right: 0,
bottom: 0,
left: 0,
});
expect(fromArray).toMatchObject({
top: 1,
right: 2,
bottom: 3,
left: 4,
});
expect(fromOne).toMatchObject({
top: 1,
right: 1,
bottom: 1,
left: 1,
});
expect(fromTwo).toMatchObject({
top: 1,
right: 2,
bottom: 1,
left: 2,
});
expect(fromThree).toMatchObject({
top: 1,
right: 2,
bottom: 3,
left: 2,
});
expect(fromObject).toMatchObject({
top: 1,
right: 2,
bottom: 3,
left: 4,
});
});
test('Interpolates between spacings', () => {
const a = new Spacing(1, 2, 3, 4);
const b = new Spacing(3, 4, 5, 6);
const result = Spacing.lerp(a, b, 0.5);
expect(result).toMatchObject({
top: 2,
right: 3,
bottom: 4,
left: 5,
});
});
test('Creates a compound signal', () => {
const horizontal = createSignal(2);
const spacing = Spacing.createSignal(() => [1, horizontal(), 3]);
expect(spacing()).toMatchObject({
top: 1,
right: 2,
bottom: 3,
left: 2,
});
expect(spacing.top()).toBe(1);
expect(spacing.right()).toBe(2);
expect(spacing.bottom()).toBe(3);
expect(spacing.left()).toBe(2);
horizontal(4);
expect(spacing()).toMatchObject({
top: 1,
right: 4,
bottom: 3,
left: 4,
});
expect(spacing.left()).toBe(4);
expect(spacing.right()).toBe(4);
});
});

View File

@@ -1,5 +1,6 @@
import {map} from '../tweening';
import {InterpolationFunction, map} from '../tweening';
import {Type} from './Type';
import {CompoundSignal, CompoundSignalContext, SignalValue} from '../signals';
export type SerializedSpacing = {
top: number;
@@ -13,7 +14,15 @@ export type PossibleSpacing =
| number
| [number, number]
| [number, number, number]
| [number, number, number, number];
| [number, number, number, number]
| undefined;
export type SpacingSignal<T> = CompoundSignal<
PossibleSpacing,
Spacing,
'top' | 'right' | 'bottom' | 'left',
T
>;
export class Spacing implements Type {
public static readonly symbol = Symbol.for(
@@ -25,6 +34,18 @@ export class Spacing implements Type {
public bottom = 0;
public left = 0;
public static createSignal(
initial?: SignalValue<PossibleSpacing>,
interpolation: InterpolationFunction<Spacing> = Spacing.lerp,
): SpacingSignal<void> {
return new CompoundSignalContext(
['top', 'right', 'bottom', 'left'],
(value: PossibleSpacing) => new Spacing(value),
initial,
interpolation,
).toSignal();
}
public static lerp(from: Spacing, to: Spacing, value: number): Spacing {
return new Spacing(
map(from.top, to.top, value),

View File

@@ -0,0 +1,43 @@
import {describe, expect, test} from 'vitest';
import {Vector2} from '../types';
import {createSignal} from '../signals';
describe('Vector2', () => {
test('Correctly parses values', () => {
const fromUndefined = new Vector2();
const fromScalar = new Vector2(3);
const fromProperties = new Vector2(2, 4);
const fromArray = new Vector2([2, -1]);
const fromVector = new Vector2(Vector2.up);
const fromObject = new Vector2({x: -1, y: 3});
expect(fromUndefined).toMatchObject({x: 0, y: 0});
expect(fromScalar).toMatchObject({x: 3, y: 3});
expect(fromProperties).toMatchObject({x: 2, y: 4});
expect(fromArray).toMatchObject({x: 2, y: -1});
expect(fromVector).toMatchObject({x: 0, y: 1});
expect(fromObject).toMatchObject({x: -1, y: 3});
});
test('Interpolates between vectors', () => {
const a = new Vector2(10, 24);
const b = new Vector2(-10, 12);
const result = Vector2.lerp(a, b, 0.5);
expect(result).toMatchObject({x: 0, y: 18});
});
test('Creates a compound signal', () => {
const x = createSignal(1);
const vector = Vector2.createSignal(() => [x(), 2]);
expect(vector()).toMatchObject({x: 1, y: 2});
expect(vector.x()).toBe(1);
expect(vector.y()).toBe(2);
x(3);
expect(vector()).toMatchObject({x: 3, y: 2});
expect(vector.x()).toBe(3);
});
});

View File

@@ -1,7 +1,8 @@
import {arcLerp} from '../tweening';
import {arcLerp, InterpolationFunction} from '../tweening';
import {map} from '../tweening/interpolationFunctions';
import {Direction, Origin} from './Origin';
import {Type} from './Type';
import {CompoundSignal, CompoundSignalContext, SignalValue} from '../signals';
export type SerializedVector2<T = number> = {
x: T;
@@ -12,7 +13,15 @@ export type PossibleVector2<T = number> =
| SerializedVector2<T>
| {width: T; height: T}
| T
| [T, T];
| [T, T]
| undefined;
export type Vector2Signal<T> = CompoundSignal<
PossibleVector2,
Vector2,
'x' | 'y',
T
>;
/**
* Represents a two-dimensional vector.
@@ -32,6 +41,20 @@ export class Vector2 implements Type {
public x = 0;
public y = 0;
public static createSignal(
initial?: SignalValue<PossibleVector2>,
interpolation: InterpolationFunction<Vector2> = Vector2.lerp,
owner?: any,
): Vector2Signal<void> {
return new CompoundSignalContext(
['x', 'y'],
(value: PossibleVector2) => new Vector2(value),
initial,
interpolation,
owner,
).toSignal();
}
public static lerp(from: Vector2, to: Vector2, value: number | Vector2) {
let valueX;
let valueY;
@@ -138,10 +161,10 @@ export class Vector2 implements Type {
public get ctg(): number {
return this.x / this.y;
}
public constructor();
public constructor(from: PossibleVector2);
public constructor(x: number, y: number);
public constructor(one?: PossibleVector2 | number, two?: number) {
if (one === undefined || one === null) {
return;

View File

@@ -1,49 +0,0 @@
import {FlagDispatcher, Subscribable} from '../events';
import {
collect,
DependencyContext,
finishCollecting,
startCollecting,
} from './createSignal';
import {useLogger} from './useProject';
export type Computed<TValue> = (...args: any[]) => TValue;
export function createComputed<TValue>(
factory: Computed<TValue>,
owner?: any,
): Computed<TValue> {
let last: TValue;
const event = new FlagDispatcher();
const context: DependencyContext = {
dependencies: new Set<Subscribable<void>>(),
handler: () => event.raise(),
owner,
};
const handler = <Computed<TValue>>function handler(...args: any[]) {
if (event.isRaised()) {
context.dependencies.forEach(dep => dep.unsubscribe(context.handler));
context.dependencies.clear();
context.stack = new Error().stack;
startCollecting(context);
try {
last = factory(...args);
} catch (e: any) {
useLogger().error({
message: e.message,
stack: e.stack,
inspect: context.owner?.key,
});
}
finishCollecting(context);
}
event.reset();
collect(event.subscribable);
return last;
};
context.handler();
return handler;
}

View File

@@ -1,320 +0,0 @@
import {EventHandler, FlagDispatcher, Subscribable} from '../events';
import {
deepLerp,
easeInOutCubic,
TimingFunction,
tween,
InterpolationFunction,
} from '../tweening';
import {ThreadGenerator} from '../threading';
import {useLogger} from './useProject';
import {decorate, threadable} from '../decorators';
export interface DependencyContext {
dependencies: Set<Subscribable<void>>;
handler: EventHandler<void>;
stack?: string;
owner?: any;
}
export type SignalValue<TValue> = TValue | (() => TValue);
export type SignalGenerator<TValue> = ThreadGenerator & {
to: SignalTween<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>,
): SignalGenerator<TValue>;
}
export interface SignalUtils<TValue, TReturn> {
/**
* 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;
/**
* Get the raw value of this signal.
*
* @remarks
* If the signal was provided with a factory function, the function itself
* will be returned, without invoking it.
*
* This method can be used to create copies of signals.
*
* @example
* ```ts
* const a = createSignal(2);
* const b = createSignal(() => a);
* // b() == 2
*
* const bClone = createSignal(b.raw());
* // bClone() == 2
*
* a(4);
* // b() == 4
* // bClone() == 4
* ```
*/
raw(): SignalValue<TValue>;
}
export interface Signal<TValue, TReturn = void>
extends SignalSetter<TValue, TReturn>,
SignalGetter<TValue>,
SignalTween<TValue>,
SignalUtils<TValue, TReturn> {}
const collectionStack: DependencyContext[] = [];
let promises: PromiseHandle<any>[] = [];
export function startCollecting(context: DependencyContext) {
collectionStack.push(context);
}
export function finishCollecting(context: DependencyContext) {
if (collectionStack.pop() !== context) {
throw new Error('collectStart/collectEnd was called out of order');
}
}
export function collect(subscribable: Subscribable<void>) {
const context = collectionStack.at(-1);
if (context) {
context.dependencies.add(subscribable);
subscribable.subscribe(context.handler);
}
}
export interface PromiseHandle<T> {
promise: Promise<T>;
value: T;
stack?: string;
owner?: any;
}
export function collectPromise<T>(promise: Promise<T>): PromiseHandle<T | null>;
export function collectPromise<T>(
promise: Promise<T>,
initialValue: T,
): PromiseHandle<T>;
export function collectPromise<T>(
promise: Promise<T>,
initialValue: T | null = null,
): PromiseHandle<T | null> {
const handle: PromiseHandle<T | null> = {
promise,
value: initialValue,
stack: collectionStack[0]?.stack,
};
const context = collectionStack.at(-2);
if (context) {
handle.owner = context.owner;
}
promise.then(value => {
handle.value = value;
context?.handler();
});
promises.push(handle);
return handle;
}
export function consumePromises(): PromiseHandle<any>[] {
const result = promises;
promises = [];
return result;
}
export function isReactive<T>(value: SignalValue<T>): value is () => T {
return typeof value === 'function';
}
export function createSignal<TValue, TReturn = void>(
initial?: SignalValue<TValue>,
defaultInterpolation: InterpolationFunction<TValue> = deepLerp,
setterReturn?: TReturn,
): Signal<TValue, TReturn> {
let current: SignalValue<TValue>;
let last: TValue;
const event = new FlagDispatcher();
const context: DependencyContext = {
dependencies: new Set<Subscribable<void>>(),
handler: () => event.raise(),
owner: setterReturn,
};
function set(value: SignalValue<TValue>) {
if (current === value) {
return setterReturn;
}
current = value;
markDirty();
if (context.dependencies.size > 0) {
context.dependencies.forEach(dep => dep.unsubscribe(markDirty));
context.dependencies.clear();
}
if (!isReactive(value)) {
last = value;
}
return setterReturn;
}
function get(): TValue {
if (event.isRaised() && isReactive(current)) {
context.dependencies.forEach(dep => dep.unsubscribe(markDirty));
context.dependencies.clear();
context.stack = new Error().stack;
startCollecting(context);
try {
last = current();
} catch (e: any) {
useLogger().error({
message: e.message,
stack: e.stack,
inspect: context.owner?.key,
});
}
finishCollecting(context);
}
event.reset();
collect(event.subscribable);
return last;
}
function markDirty() {
event.raise();
}
const handler = <Signal<TValue, TReturn>>(
function handler(
value?: SignalValue<TValue>,
duration?: number,
timingFunction: TimingFunction = easeInOutCubic,
interpolationFunction: InterpolationFunction<TValue> = defaultInterpolation,
) {
if (value === undefined) {
return get();
}
if (duration === undefined) {
return set(value);
}
return makeAnimate(timingFunction, interpolationFunction)(
value,
duration,
);
}
);
function makeAnimate(
defaultTimingFunction: TimingFunction,
defaultInterpolationFunction: InterpolationFunction<TValue>,
before?: ThreadGenerator,
) {
function animate(
value: SignalValue<TValue>,
duration: number,
timingFunction = defaultTimingFunction,
interpolationFunction = defaultInterpolationFunction,
) {
const task = <SignalGenerator<TValue>>(
makeTask(value, duration, timingFunction, interpolationFunction, before)
);
task.to = makeAnimate(timingFunction, interpolationFunction, task);
return task;
}
return <SignalTween<TValue>>animate;
}
decorate(makeTask, threadable());
function* makeTask(
value: SignalValue<TValue>,
duration: number,
timingFunction: TimingFunction,
interpolationFunction: InterpolationFunction<TValue>,
before?: ThreadGenerator,
) {
if (before) {
yield* before;
}
const from = get();
yield* tween(
duration,
v => {
set(
interpolationFunction(
from,
isReactive(value) ? value() : value,
timingFunction(v),
),
);
},
() => set(value),
);
}
Object.defineProperty(handler, 'reset', {
value: initial !== undefined ? () => set(initial) : () => setterReturn,
});
Object.defineProperty(handler, 'save', {
value: () => set(get()),
});
Object.defineProperty(handler, 'raw', {
value: () => current,
});
if (initial !== undefined) {
set(initial);
}
return handler;
}

View File

@@ -1,7 +1,4 @@
export * from './capitalize';
export * from './createComputed';
export * from './createComputedAsync';
export * from './createSignal';
export * from './getContext';
export * from './range';
export * from './useProject';

View File

@@ -10,7 +10,7 @@ export function useProject() {
}
export function useLogger() {
return currentProject.logger;
return currentProject?.logger ?? console;
}
export function setProject(project: Project) {