feat: add missing layout props (#72)

This commit is contained in:
Jacob
2022-10-01 13:48:47 +02:00
committed by GitHub
parent 9c5853d8bc
commit f808a562b1
7 changed files with 207 additions and 104 deletions

View File

@@ -185,8 +185,8 @@ export class Node<TProps extends NodeProps = NodeProps>
this.layout.releaseSize();
}
@compound(['width', 'height'], Size)
@property(undefined, Size.lerp)
@compound(['width', 'height'])
@property(undefined, Size.lerp, Size)
public declare readonly size: Property<
{width: Length; height: Length},
Size,
@@ -246,8 +246,8 @@ export class Node<TProps extends NodeProps = NodeProps>
@property(0)
public declare readonly offsetY: Signal<number, this>;
@compound({x: 'offsetX', y: 'offsetY'}, Vector2)
@property(undefined, Vector2.lerp)
@compound({x: 'offsetX', y: 'offsetY'})
@property(undefined, Vector2.lerp, Vector2)
public declare readonly offset: Signal<Vector2, this>;
@property(1)
@@ -256,8 +256,8 @@ export class Node<TProps extends NodeProps = NodeProps>
@property(1)
public declare readonly scaleY: Signal<number, this>;
@compound({x: 'scaleX', y: 'scaleY'}, Vector2)
@property(undefined, Vector2.lerp)
@compound({x: 'scaleX', y: 'scaleY'})
@property(undefined, Vector2.lerp, Vector2)
public declare readonly scale: Signal<Vector2, this>;
@property(false)
@@ -313,8 +313,8 @@ export class Node<TProps extends NodeProps = NodeProps>
@property(0)
public declare readonly shadowOffsetY: Signal<number, this>;
@compound({x: 'shadowOffsetX', y: 'shadowOffsetY'}, Vector2)
@property(undefined, Vector2.lerp)
@compound({x: 'shadowOffsetX', y: 'shadowOffsetY'})
@property(undefined, Vector2.lerp, Vector2)
public declare readonly shadowOffset: Signal<Vector2, this>;
@computed()
@@ -381,11 +381,11 @@ export class Node<TProps extends NodeProps = NodeProps>
return filters;
}
@compound(['x', 'y'], Vector2)
@property(undefined, Vector2.lerp)
@compound(['x', 'y'])
@property(undefined, Vector2.lerp, Vector2)
public declare readonly position: Signal<Vector2, this>;
@property(undefined, Vector2.lerp)
@property(undefined, Vector2.lerp, Vector2)
public declare readonly absolutePosition: Signal<Vector2, this>;
protected getAbsolutePosition(): Vector2 {

View File

@@ -40,12 +40,9 @@ import {addInitializer} from './initializers';
* @param mapping - An array of signals to turn into a compound property or a
* record mapping the property in the compound object to the
* corresponding signal.
*
* @param klass - A class used to instantiate the returned value.
*/
export function compound(
mapping: string[] | Record<string, string>,
klass?: new (from: any) => any,
): PropertyDecorator {
return (target: any, key) => {
const entries = Array.isArray(mapping)
@@ -54,10 +51,9 @@ export function compound(
target.constructor.prototype[`get${capitalize(key.toString())}`] =
function () {
const object = Object.fromEntries(
return Object.fromEntries(
entries.map(([key, property]) => [key, this[property]()]),
);
return klass ? new klass(object) : object;
};
target.constructor.prototype[`set${capitalize(key.toString())}`] =

View File

@@ -32,7 +32,7 @@ export interface Property<
export type PropertyOwner<TGetterValue, TSetterValue> = {
[key: `get${Capitalize<string>}`]: SignalGetter<TGetterValue> | undefined;
[key: `set${Capitalize<string>}`]: SignalSetter<TSetterValue> | undefined;
[key: `tween${Capitalize<string>}`]: SignalTween<TSetterValue> | undefined;
[key: `tween${Capitalize<string>}`]: SignalTween<TGetterValue> | undefined;
};
export function createProperty<
@@ -44,7 +44,8 @@ export function createProperty<
node: TNode,
property: TProperty,
initial?: TSetterValue,
defaultInterpolation: InterpolationFunction<TSetterValue> = deepLerp,
defaultInterpolation: InterpolationFunction<TGetterValue> = deepLerp,
klass?: new (value: TSetterValue) => TGetterValue,
): Property<TSetterValue, TGetterValue, TNode> {
let getter: SignalGetter<TGetterValue>;
let setter: SignalSetter<TSetterValue>;
@@ -59,10 +60,27 @@ export function createProperty<
);
}
let wrap: (value: SignalValue<TSetterValue>) => SignalValue<TGetterValue>;
let unwrap: (value: SignalValue<TSetterValue>) => TGetterValue;
if (klass) {
wrap = value =>
isReactive(value) ? () => new klass(value()) : new klass(value);
unwrap = value => new klass(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(initial, defaultInterpolation, node))
(<unknown>(
createSignal(
initial === undefined ? undefined : wrap(initial),
defaultInterpolation,
node,
)
))
);
if (!tweener) {
return signal;
@@ -83,20 +101,20 @@ export function createProperty<
newValue?: SignalValue<TSetterValue>,
duration?: number,
timingFunction: TimingFunction = easeInOutCubic,
interpolationFunction: InterpolationFunction<TSetterValue> = defaultInterpolation,
interpolationFunction: InterpolationFunction<TGetterValue> = defaultInterpolation,
) {
if (newValue === undefined) {
return getter();
}
if (duration === undefined) {
return setter(newValue);
return setter(wrap(newValue));
}
if (tweener) {
return tweener.call(
node,
newValue,
wrap(newValue),
duration,
timingFunction,
interpolationFunction,
@@ -106,11 +124,7 @@ export function createProperty<
const from = getter();
return tween(duration, value => {
setter(
interpolationFunction(
from,
isReactive(newValue) ? newValue() : newValue,
timingFunction(value),
),
interpolationFunction(from, unwrap(newValue), timingFunction(value)),
);
});
}
@@ -174,10 +188,12 @@ export function createProperty<
* @param initial - An option initial value of the property.
* @param interpolationFunction - The default function used to interpolate
* between values.
* @param klass - A class used to instantiate the returned value.
*/
export function property<T>(
initial?: T,
interpolationFunction?: InterpolationFunction<T>,
klass?: new (value: any) => T,
): PropertyDecorator {
return (target: any, key) => {
addInitializer(target, (instance: any, context: any) => {
@@ -186,6 +202,7 @@ export function property<T>(
<string>key,
context.defaults[key] ?? initial,
interpolationFunction ?? deepLerp,
klass,
);
});
};

View File

@@ -31,16 +31,16 @@ export class Gradient {
public declare readonly fromX: Signal<number, this>;
@property(0)
public declare readonly fromY: Signal<number, this>;
@compound({x: 'fromX', y: 'fromY'}, Vector2)
@property(undefined, Vector2.lerp)
@compound({x: 'fromX', y: 'fromY'})
@property(undefined, Vector2.lerp, Vector2)
public declare readonly from: Signal<Vector2, this>;
@property(0)
public declare readonly toX: Signal<number, this>;
@property(0)
public declare readonly toY: Signal<number, this>;
@compound({x: 'toX', y: 'toY'}, Vector2)
@property(undefined, Vector2.lerp)
@compound({x: 'toX', y: 'toY'})
@property(undefined, Vector2.lerp, Vector2)
public declare readonly to: Signal<Vector2, this>;
@property(0)

View File

@@ -1,32 +1,61 @@
import {computed, initialize, property} from '../decorators';
import {createSignal, Signal} from '@motion-canvas/core/lib/utils';
import {Rect} from '@motion-canvas/core/lib/types';
import {
AlignItems,
compound,
computed,
initialize,
Property,
property,
} from '../decorators';
import {createSignal, Signal} from '@motion-canvas/core/lib/utils';
import {
PossibleSpacing,
Rect,
Spacing,
Vector2,
} from '@motion-canvas/core/lib/types';
import {
FlexAlign,
FlexDirection,
JustifyContent,
FlexWrap,
FlexJustify,
LayoutMode,
Length,
FlexBasis,
} from './types';
import {TwoDView} from '../scenes';
export interface LayoutProps {
tagName?: keyof HTMLElementTagNameMap;
mode?: LayoutMode;
width?: number;
height?: number;
maxWidth?: Length;
maxHeight?: Length;
minWidth?: Length;
minHeight?: Length;
ratio?: number;
marginTop?: number;
marginBottom?: number;
marginLeft?: number;
marginRight?: number;
margin?: PossibleSpacing;
paddingTop?: number;
paddingBottom?: number;
paddingLeft?: number;
paddingRight?: number;
padding?: PossibleSpacing;
direction?: FlexDirection;
justifyContent?: JustifyContent;
alignItems?: AlignItems;
ratio?: string;
basis?: FlexBasis;
grow?: number;
shrink?: number;
wrap?: FlexWrap;
justifyContent?: FlexJustify;
alignItems?: FlexAlign;
rowGap?: Length;
columnGap?: Length;
gap?: Length;
fontFamily?: string;
fontSize?: number;
@@ -34,13 +63,24 @@ export interface LayoutProps {
fontWeight?: number;
lineHeight?: number;
letterSpacing?: number;
wrap?: boolean;
textWrap?: boolean;
}
export class Layout {
@property(null)
public declare readonly mode: Signal<LayoutMode, this>;
@property(null)
public declare readonly maxWidth: Signal<Length, this>;
@property(null)
public declare readonly maxHeight: Signal<Length, this>;
@property(null)
public declare readonly minWidth: Signal<Length, this>;
@property(null)
public declare readonly minHeight: Signal<Length, this>;
@property(null)
public declare readonly ratio: Signal<number | null, this>;
@property(0)
public declare readonly marginTop: Signal<number, this>;
@property(0)
@@ -49,6 +89,14 @@ export class Layout {
public declare readonly marginLeft: Signal<number, this>;
@property(0)
public declare readonly marginRight: Signal<number, this>;
@compound({
top: 'marginTop',
bottom: 'marginBottom',
left: 'marginLeft',
right: 'marginRight',
})
@property(undefined, Spacing.lerp, Spacing)
public declare readonly margin: Property<PossibleSpacing, Spacing, this>;
@property(0)
public declare readonly paddingTop: Signal<number, this>;
@@ -58,15 +106,36 @@ export class Layout {
public declare readonly paddingLeft: Signal<number, this>;
@property(0)
public declare readonly paddingRight: Signal<number, this>;
@compound({
top: 'paddingTop',
bottom: 'paddingBottom',
left: 'paddingLeft',
right: 'paddingRight',
})
@property(undefined, Spacing.lerp, Spacing)
public declare readonly padding: Property<PossibleSpacing, Spacing, this>;
@property('row')
public declare readonly direction: Signal<FlexDirection, this>;
@property('none')
public declare readonly ratio: Signal<string, this>;
@property('flex-start')
public declare readonly justifyContent: Signal<JustifyContent, this>;
@property('auto')
public declare readonly alignItems: Signal<AlignItems, this>;
@property(null)
public declare readonly basis: Signal<FlexBasis, this>;
@property(0)
public declare readonly grow: Signal<number, this>;
@property(1)
public declare readonly shrink: Signal<number, this>;
@property('nowrap')
public declare readonly wrap: Signal<FlexWrap, this>;
@property('normal')
public declare readonly justifyContent: Signal<FlexJustify, this>;
@property('normal')
public declare readonly alignItems: Signal<FlexAlign, this>;
@property(null)
public declare readonly gap: Signal<Length, this>;
@property(null)
public declare readonly rowGap: Signal<Length, this>;
@property(null)
public declare readonly columnGap: Signal<Length, this>;
@property(null)
public declare readonly fontFamily: Signal<string | null, this>;
@@ -81,7 +150,7 @@ export class Layout {
@property(null)
public declare readonly letterSpacing: Signal<number | null, this>;
@property(null)
public declare readonly wrap: Signal<boolean | null, this>;
public declare readonly textWrap: Signal<boolean | null, this>;
public readonly element: HTMLElement;
public readonly styles: CSSStyleDeclaration;
@@ -101,7 +170,21 @@ export class Layout {
initialize(this, {defaults: props});
}
public toPixels(value: number) {
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 {
if (value === null) {
return '';
}
if (typeof value === 'string') {
return value;
}
return `${value}px`;
}
@@ -118,26 +201,12 @@ export class Layout {
}
public setWidth(width: Length): this {
if (width === null) {
this.element.style.width = 'auto';
} else if (typeof width === 'string') {
this.element.style.width = width;
} else {
this.element.style.width = this.toPixels(width);
}
this.element.style.width = this.parseLength(width);
return this;
}
public setHeight(height: Length): this {
if (height === null) {
this.element.style.height = 'auto';
} else if (typeof height === 'string') {
this.element.style.height = height;
} else {
this.element.style.height = this.toPixels(height);
}
this.element.style.height = this.parseLength(height);
return this;
}
@@ -147,40 +216,51 @@ export class Layout {
this.element.style.position =
mode === 'disabled' || mode === 'root' ? 'absolute' : 'relative';
this.element.style.marginTop = this.toPixels(this.marginTop());
this.element.style.marginBottom = this.toPixels(this.marginBottom());
this.element.style.marginLeft = this.toPixels(this.marginLeft());
this.element.style.marginRight = this.toPixels(this.marginRight());
this.element.style.paddingTop = this.toPixels(this.paddingTop());
this.element.style.paddingBottom = this.toPixels(this.paddingBottom());
this.element.style.paddingLeft = this.toPixels(this.paddingLeft());
this.element.style.paddingRight = this.toPixels(this.paddingRight());
this.element.style.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.marginTop = this.parsePixels(this.marginTop());
this.element.style.marginBottom = this.parsePixels(this.marginBottom());
this.element.style.marginLeft = this.parsePixels(this.marginLeft());
this.element.style.marginRight = this.parsePixels(this.marginRight());
this.element.style.paddingTop = this.parsePixels(this.paddingTop());
this.element.style.paddingBottom = this.parsePixels(this.paddingBottom());
this.element.style.paddingLeft = this.parsePixels(this.paddingLeft());
this.element.style.paddingRight = this.parsePixels(this.paddingRight());
this.element.style.flexDirection = this.direction();
this.element.style.aspectRatio = this.ratio();
this.element.style.flexBasis = this.parseLength(this.basis());
this.element.style.flexWrap = this.wrap();
this.element.style.justifyContent = this.justifyContent();
this.element.style.alignItems = this.alignItems();
this.element.style.gap = this.parseLength(this.gap());
this.element.style.rowGap = this.parseLength(this.rowGap());
this.element.style.columnGap = this.parseLength(this.columnGap());
this.element.style.flexGrow = this.sizeLockCounter() > 0 ? '0' : '';
this.element.style.flexShrink = this.sizeLockCounter() > 0 ? '0' : '';
if (this.sizeLockCounter() > 0) {
this.element.style.flexGrow = '0';
this.element.style.flexShrink = '0';
} else {
this.element.style.flexGrow = this.parsePixels(this.grow());
this.element.style.flexShrink = this.parsePixels(this.shrink());
}
}
@computed()
public applyFont() {
this.element.style.fontFamily = this.fontFamily() ?? '';
const fontSize = this.fontSize();
this.element.style.fontSize = fontSize ? this.toPixels(fontSize) : '';
this.element.style.fontStyle = this.fontStyle() ?? '';
const lineHeight = this.lineHeight();
this.element.style.lineHeight =
lineHeight === null ? '' : this.toPixels(lineHeight);
const fontWeight = this.fontWeight();
this.element.style.fontWeight =
fontWeight === null ? '' : fontWeight.toString();
const letterSpacing = this.letterSpacing();
this.element.style.letterSpacing = letterSpacing
? this.toPixels(letterSpacing)
: '';
const wrap = this.wrap();
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 ? '' : wrap ? 'normal' : 'nowrap';
}

View File

@@ -1,22 +1,32 @@
export type FlexDirection = 'row' | 'row-reverse' | 'column' | 'column-reverse';
export type JustifyContent =
| 'flex-start'
export type FlexWrap = 'nowrap' | 'wrap' | 'wrap-reverse';
export type FlexBasis =
| Length
| 'content'
| 'max-content'
| 'min-content'
| 'fit-content'
| null;
export type FlexJustify =
| 'normal'
| 'center'
| 'flex-end'
| 'start'
| 'end'
| 'space-between'
| 'space-around'
| 'space-evenly';
| 'space-evenly'
| 'stretch';
export type AlignItems =
| 'auto'
| 'flex-start'
export type FlexAlign =
| 'normal'
| 'center'
| 'flex-end'
| 'start'
| 'end'
| 'stretch'
| 'baseline'
| 'space-between'
| 'space-around';
| 'baseline';
export type ResolvedLayoutMode = 'disabled' | 'enabled' | 'root' | 'pop';
export type LayoutMode = ResolvedLayoutMode | null;

View File

@@ -13,7 +13,7 @@ export class TwoDView extends Node {
fontFamily: 'Roboto',
fontSize: 48,
lineHeight: 64,
wrap: false,
textWrap: false,
fontStyle: 'normal',
},
});