mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-11 14:57:56 -05:00
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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
14
packages/2d/src/decorators/canvasStyleSignal.ts
Normal file
14
packages/2d/src/decorators/canvasStyleSignal.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
9
packages/2d/src/decorators/colorSignal.ts
Normal file
9
packages/2d/src/decorators/colorSignal.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
132
packages/2d/src/decorators/filtersSignal.ts
Normal file
132
packages/2d/src/decorators/filtersSignal.ts
Normal 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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
15
packages/2d/src/decorators/spacingSignal.ts
Normal file
15
packages/2d/src/decorators/spacingSignal.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
30
packages/2d/src/decorators/vector2Signal.ts
Normal file
30
packages/2d/src/decorators/vector2Signal.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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> = {};
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './makeScene2D';
|
||||
export * from './Scene2D';
|
||||
export * from './useScene2D';
|
||||
|
||||
6
packages/2d/src/scenes/useScene2D.ts
Normal file
6
packages/2d/src/scenes/useScene2D.ts
Normal 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();
|
||||
}
|
||||
28
packages/2d/src/utils/patchSignal.ts
Normal file
28
packages/2d/src/utils/patchSignal.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
103
packages/core/src/signals/CompoundSignalContext.ts
Normal file
103
packages/core/src/signals/CompoundSignalContext.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
45
packages/core/src/signals/ComputedContext.ts
Normal file
45
packages/core/src/signals/ComputedContext.ts
Normal 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!;
|
||||
}
|
||||
}
|
||||
82
packages/core/src/signals/DependencyContext.ts
Normal file
82
packages/core/src/signals/DependencyContext.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
290
packages/core/src/signals/SignalContext.ts
Normal file
290
packages/core/src/signals/SignalContext.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
8
packages/core/src/signals/createComputed.ts
Normal file
8
packages/core/src/signals/createComputed.ts
Normal 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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
14
packages/core/src/signals/createSignal.ts
Normal file
14
packages/core/src/signals/createSignal.ts
Normal 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();
|
||||
}
|
||||
15
packages/core/src/signals/index.ts
Normal file
15
packages/core/src/signals/index.ts
Normal 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';
|
||||
5
packages/core/src/signals/isReactive.ts
Normal file
5
packages/core/src/signals/isReactive.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import {SignalValue} from './types';
|
||||
|
||||
export function isReactive<T>(value: SignalValue<T>): value is () => T {
|
||||
return typeof value === 'function';
|
||||
}
|
||||
27
packages/core/src/signals/types.ts
Normal file
27
packages/core/src/signals/types.ts
Normal 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>;
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
73
packages/core/src/types/Rect.test.ts
Normal file
73
packages/core/src/types/Rect.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
96
packages/core/src/types/Spacing.test.ts
Normal file
96
packages/core/src/types/Spacing.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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),
|
||||
|
||||
43
packages/core/src/types/Vector.test.ts
Normal file
43
packages/core/src/types/Vector.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -10,7 +10,7 @@ export function useProject() {
|
||||
}
|
||||
|
||||
export function useLogger() {
|
||||
return currentProject.logger;
|
||||
return currentProject?.logger ?? console;
|
||||
}
|
||||
|
||||
export function setProject(project: Project) {
|
||||
|
||||
Reference in New Issue
Block a user