Skip to content

Commit

Permalink
feat(2d): add default computed values for signals (#259)
Browse files Browse the repository at this point in the history
  • Loading branch information
exdeejay committed Feb 15, 2023
1 parent 42b3369 commit 18f61a6
Show file tree
Hide file tree
Showing 13 changed files with 155 additions and 61 deletions.
6 changes: 3 additions & 3 deletions packages/2d/src/components/Image.ts
Expand Up @@ -120,9 +120,9 @@ export class Image extends Rect {
protected override applyFlex() {
super.applyFlex();
const image = this.image();
this.element.style.aspectRatio = this.parseValue(
this.ratio() ?? image.naturalWidth / image.naturalHeight,
);
this.element.style.aspectRatio = (
this.ratio() ?? image.naturalWidth / image.naturalHeight
).toString();
}

/**
Expand Down
101 changes: 62 additions & 39 deletions packages/2d/src/components/Layout.ts
@@ -1,6 +1,7 @@
import {
cloneable,
computed,
defaultStyle,
initial,
inspectable,
signal,
Expand Down Expand Up @@ -31,6 +32,7 @@ import {
FlexWrap,
LayoutMode,
Length,
LetterSpacing,
TextWrap,
} from '../partials';
import {threadable} from '@motion-canvas/core/lib/decorators';
Expand Down Expand Up @@ -154,25 +156,33 @@ export class Layout extends Node {
@signal()
public declare readonly columnGap: SimpleSignal<Length, this>;

@initial(null)
@defaultStyle('font-family')
@signal()
public declare readonly fontFamily: SimpleSignal<string | null, this>;
@initial(null)
public declare readonly fontFamily: SimpleSignal<string, this>;
@defaultStyle('font-size', parseFloat)
@signal()
public declare readonly fontSize: SimpleSignal<number | null, this>;
@initial(null)
public declare readonly fontSize: SimpleSignal<number, this>;
@defaultStyle('font-style')
@signal()
public declare readonly fontStyle: SimpleSignal<string | null, this>;
@initial(null)
public declare readonly fontStyle: SimpleSignal<string, this>;
@defaultStyle('font-weight', parseInt)
@signal()
public declare readonly fontWeight: SimpleSignal<number | null, this>;
@initial(null)
public declare readonly fontWeight: SimpleSignal<number, this>;
@initial('120%')
@signal()
public declare readonly lineHeight: SimpleSignal<number | null, this>;
@initial(null)
public declare readonly lineHeight: SimpleSignal<Length, this>;
@defaultStyle('letter-spacing', i => {
if (i === 'normal') {
return 'normal';
}
return parseFloat(i);
})
@signal()
public declare readonly letterSpacing: SimpleSignal<number | null, this>;
@initial(null)
public declare readonly letterSpacing: SimpleSignal<LetterSpacing, this>;

@defaultStyle('white-space', i => {
return i === 'pre' ? 'pre' : i === 'normal';
})
@signal()
public declare readonly textWrap: SimpleSignal<TextWrap, this>;

Expand Down Expand Up @@ -674,15 +684,11 @@ export class Layout extends Node {
this.position(this.position().add(newOffset).sub(oldOffset));
}

protected parseValue(value: number | string | null): string {
return value === null ? '' : value.toString();
}

protected parsePixels(value: number | null): string {
return value === null ? '' : `${value}px`;
}

protected parseLength(value: null | number | string): string {
protected parseLength(value: number | string | null): string {
if (value === null) {
return '';
}
Expand All @@ -702,8 +708,9 @@ export class Layout extends Node {
this.element.style.maxWidth = this.parseLength(this.maxWidth());
this.element.style.minWidth = this.parseLength(this.minWidth());
this.element.style.maxHeight = this.parseLength(this.maxHeight());
this.element.style.minWidth = this.parseLength(this.minWidth());
this.element.style.aspectRatio = this.parseValue(this.ratio());
this.element.style.minHeight = this.parseLength(this.minHeight()!);
this.element.style.aspectRatio =
this.ratio() === null ? '' : this.ratio()!.toString();

this.element.style.marginTop = this.parsePixels(this.margin.top());
this.element.style.marginBottom = this.parsePixels(this.margin.bottom());
Expand All @@ -716,7 +723,7 @@ export class Layout extends Node {
this.element.style.paddingRight = this.parsePixels(this.padding.right());

this.element.style.flexDirection = this.direction();
this.element.style.flexBasis = this.parseLength(this.basis());
this.element.style.flexBasis = this.parseLength(this.basis()!);
this.element.style.flexWrap = this.wrap();

this.element.style.justifyContent = this.justifyContent();
Expand All @@ -729,29 +736,45 @@ export class Layout extends Node {
this.element.style.flexGrow = '0';
this.element.style.flexShrink = '0';
} else {
this.element.style.flexGrow = this.parseValue(this.grow());
this.element.style.flexShrink = this.parseValue(this.shrink());
this.element.style.flexGrow = this.grow().toString();
this.element.style.flexShrink = this.shrink().toString();
}
}

@computed()
protected applyFont() {
this.element.style.fontFamily = this.parseValue(this.fontFamily());
this.element.style.fontSize = this.parsePixels(this.fontSize());
this.element.style.fontStyle = this.parseValue(this.fontStyle());
this.element.style.lineHeight = this.parsePixels(this.lineHeight());
this.element.style.fontWeight = this.parseValue(this.fontWeight());
this.element.style.letterSpacing = this.parsePixels(this.letterSpacing());

const wrap = this.textWrap();
this.element.style.whiteSpace =
wrap === null
? ''
: typeof wrap === 'boolean'
? wrap
? 'normal'
: 'nowrap'
: wrap;
this.element.style.fontFamily = this.fontFamily.isInitial()
? ''
: this.fontFamily();
this.element.style.fontSize = this.fontSize.isInitial()
? ''
: `${this.fontSize()}px`;
this.element.style.fontStyle = this.fontStyle.isInitial()
? ''
: this.fontStyle();
this.element.style.lineHeight =
typeof this.lineHeight() === 'string'
? (parseFloat(this.lineHeight() as string) / 100).toString()
: `${this.lineHeight()}px`;
this.element.style.fontWeight = this.fontWeight.isInitial()
? ''
: this.fontWeight().toString();
this.element.style.letterSpacing = this.letterSpacing.isInitial()
? ''
: this.letterSpacing() === 'normal'
? 'normal'
: `${this.letterSpacing()}px`;

if (this.textWrap.isInitial()) {
this.element.style.whiteSpace = '';
} else {
const wrap = this.textWrap();
if (typeof wrap === 'boolean') {
this.element.style.whiteSpace = wrap ? 'normal' : 'nowrap';
} else {
this.element.style.whiteSpace = wrap;
}
}
}

public override hit(position: Vector2): Node | null {
Expand Down
12 changes: 11 additions & 1 deletion packages/2d/src/components/Node.ts
Expand Up @@ -388,8 +388,18 @@ export class Node implements Promisable<Node> {
this.view2D = scene.getView();
this.creationStack = new Error().stack;
initialize(this, {defaults: rest});
for (const {signal} of this) {
for (const {key, signal, meta} of this) {
signal.reset();
if ((<any>rest)[key] !== undefined) {
signal((<any>rest)[key]);
}
if (meta.compoundEntries !== undefined) {
for (const [key, property] of meta.compoundEntries) {
if (property in rest) {
(<any>signal)[key]((<any>rest)[property]);
}
}
}
}
this.add(children);
if (spawner) {
Expand Down
6 changes: 3 additions & 3 deletions packages/2d/src/components/Video.ts
Expand Up @@ -182,9 +182,9 @@ export class Video extends Rect {
protected override applyFlex() {
super.applyFlex();
const video = this.video();
this.element.style.aspectRatio = this.parseValue(
this.ratio() ?? video.videoWidth / video.videoHeight,
);
this.element.style.aspectRatio = (
this.ratio() ?? video.videoWidth / video.videoHeight
).toString();
}

protected setCurrentTime(value: number) {
Expand Down
7 changes: 2 additions & 5 deletions packages/2d/src/decorators/compound.ts
Expand Up @@ -35,7 +35,7 @@ export function compound(entries: Record<string, string>): PropertyDecorator {
meta.compound = true;
meta.compoundEntries = Object.entries(entries);

addInitializer(target, (instance: any, context: any) => {
addInitializer(target, (instance: any) => {
if (!meta.parser) {
useLogger().error(`Missing parser decorator for "${key.toString()}"`);
return;
Expand All @@ -44,7 +44,7 @@ export function compound(entries: Record<string, string>): PropertyDecorator {
const signalContext = new CompoundSignalContext(
Object.keys(entries),
meta.parser,
context.defaults[key] ?? meta.default,
meta.default,
meta.interpolationFunction ?? deepLerp,
instance,
);
Expand All @@ -53,9 +53,6 @@ export function compound(entries: Record<string, string>): PropertyDecorator {

for (const [key, property] of meta.compoundEntries) {
patchSignal(signal[key].context, undefined, instance, property);
if (property in context.defaults) {
signal[key].context.setInitial(context.defaults[property]);
}
}

instance[key] = signal;
Expand Down
18 changes: 18 additions & 0 deletions packages/2d/src/decorators/defaultStyle.ts
@@ -0,0 +1,18 @@
import {capitalize} from '@motion-canvas/core/lib/utils';
import {Layout} from 'components';

export function defaultStyle<T>(
styleName: string,
parse: (value: string) => T = value => value as T,
): PropertyDecorator {
return (target: any, key) => {
target[`getDefault${capitalize(<string>key)}`] = function (this: Layout) {
this.requestLayoutUpdate();
const old = (<any>this.element.style)[styleName];
(<any>this.element.style)[styleName] = '';
const ret = parse.call(this, this.styles.getPropertyValue(styleName));
(<any>this.element.style)[styleName] = old;
return ret;
};
};
}
1 change: 1 addition & 0 deletions packages/2d/src/decorators/index.ts
@@ -1,4 +1,5 @@
export * from './canvasStyleSignal';
export * from './defaultStyle';
export * from './colorSignal';
export * from './compound';
export * from './computed';
Expand Down
8 changes: 5 additions & 3 deletions packages/2d/src/decorators/signal.ts
Expand Up @@ -3,7 +3,7 @@ import {
InterpolationFunction,
} from '@motion-canvas/core/lib/tweening';
import {addInitializer} from './initializers';
import {useLogger} from '@motion-canvas/core/lib/utils';
import {capitalize, useLogger} from '@motion-canvas/core/lib/utils';
import {patchSignal} from '../utils/patchSignal';
import {SignalContext} from '@motion-canvas/core/lib/signals';

Expand Down Expand Up @@ -87,9 +87,11 @@ export function getPropertiesOf(
export function signal<T>(): PropertyDecorator {
return (target: any, key) => {
const meta = getPropertyMetaOrCreate<T>(target, key);
addInitializer(target, (instance: any, context: any) => {
addInitializer(target, (instance: any) => {
const getDefault =
instance[`getDefault${capitalize(key as string)}`]?.bind(instance);
const signal = new SignalContext<T, T, any>(
context.defaults[key] ?? meta.default,
getDefault ?? meta.default,
meta.interpolationFunction ?? deepLerp,
instance,
);
Expand Down
4 changes: 3 additions & 1 deletion packages/2d/src/partials/types.ts
Expand Up @@ -32,7 +32,9 @@ export type FlexAlign =
| 'stretch'
| 'baseline';

export type TextWrap = boolean | 'pre' | null;
export type LetterSpacing = number | 'normal';

export type TextWrap = boolean | 'pre';

export type LayoutMode = boolean | null;

Expand Down

0 comments on commit 18f61a6

Please sign in to comment.