mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-11 06:48:12 -05:00
feat: turn Layout into node (#75)
This commit is contained in:
@@ -1,52 +1,49 @@
|
||||
import {Node, NodeProps} from './Node';
|
||||
import {Signal} from '@motion-canvas/core/lib/utils';
|
||||
import {computed, property} from '../decorators';
|
||||
import {Layout, LayoutProps} from './Layout';
|
||||
|
||||
export interface ImageProps extends NodeProps {
|
||||
export interface ImageProps extends LayoutProps {
|
||||
src?: string;
|
||||
}
|
||||
|
||||
export class Image extends Node<ImageProps> {
|
||||
export class Image extends Layout {
|
||||
@property()
|
||||
public declare readonly src: Signal<string, this>;
|
||||
protected readonly image: HTMLImageElement;
|
||||
|
||||
public constructor(props: ImageProps) {
|
||||
super({
|
||||
...props,
|
||||
layout: props.layout
|
||||
? {...props.layout, tagName: 'img'}
|
||||
: {tagName: 'img'},
|
||||
tagName: 'img',
|
||||
});
|
||||
this.image = <HTMLImageElement>this.element;
|
||||
}
|
||||
|
||||
protected override draw(context: CanvasRenderingContext2D) {
|
||||
const image = <HTMLImageElement>this.layout.element;
|
||||
const {width, height} = this.computedSize();
|
||||
|
||||
context.drawImage(image, width / -2, height / -2, width, height);
|
||||
context.drawImage(this.image, width / -2, height / -2, width, height);
|
||||
super.draw(context);
|
||||
}
|
||||
|
||||
protected override applyLayoutChanges() {
|
||||
protected override updateLayout() {
|
||||
this.applySrc();
|
||||
super.updateLayout();
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected applySrc() {
|
||||
const src = this.src();
|
||||
const image = <HTMLImageElement>this.layout.element;
|
||||
image.src = src;
|
||||
return image;
|
||||
this.image.src = this.src();
|
||||
}
|
||||
|
||||
protected override collectAsyncResources(deps: Promise<any>[]) {
|
||||
super.collectAsyncResources(deps);
|
||||
const image = this.applySrc();
|
||||
if (!image.complete) {
|
||||
this.applySrc();
|
||||
if (!this.image.complete) {
|
||||
deps.push(
|
||||
new Promise((resolve, reject) => {
|
||||
image.addEventListener('load', resolve);
|
||||
image.addEventListener('error', reject);
|
||||
this.image.addEventListener('load', resolve);
|
||||
this.image.addEventListener('error', reject);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
645
packages/2d/src/components/Layout.ts
Normal file
645
packages/2d/src/components/Layout.ts
Normal file
@@ -0,0 +1,645 @@
|
||||
import {compound, computed, Property, property} from '../decorators';
|
||||
import {
|
||||
Vector2,
|
||||
transformAngle,
|
||||
Rect,
|
||||
Size,
|
||||
PossibleSpacing,
|
||||
Spacing,
|
||||
} from '@motion-canvas/core/lib/types';
|
||||
import {isReactive, Signal, SignalValue} from '@motion-canvas/core/lib/utils';
|
||||
import {
|
||||
InterpolationFunction,
|
||||
TimingFunction,
|
||||
tween,
|
||||
} from '@motion-canvas/core/lib/tweening';
|
||||
import {
|
||||
FlexAlign,
|
||||
FlexBasis,
|
||||
FlexDirection,
|
||||
FlexJustify,
|
||||
FlexWrap,
|
||||
LayoutMode,
|
||||
Length,
|
||||
ResolvedLayoutMode,
|
||||
} from '../partials';
|
||||
import {threadable} from '@motion-canvas/core/lib/decorators';
|
||||
import {ThreadGenerator} from '@motion-canvas/core/lib/threading';
|
||||
import {Node, NodeProps} from './Node';
|
||||
import {TwoDView} from '../scenes';
|
||||
|
||||
export interface LayoutProps extends NodeProps {
|
||||
layout?: LayoutMode;
|
||||
tagName?: keyof HTMLElementTagNameMap;
|
||||
|
||||
width?: Length;
|
||||
height?: Length;
|
||||
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;
|
||||
basis?: FlexBasis;
|
||||
grow?: number;
|
||||
shrink?: number;
|
||||
wrap?: FlexWrap;
|
||||
|
||||
justifyContent?: FlexJustify;
|
||||
alignItems?: FlexAlign;
|
||||
rowGap?: Length;
|
||||
columnGap?: Length;
|
||||
gap?: Length;
|
||||
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
fontStyle?: string;
|
||||
fontWeight?: number;
|
||||
lineHeight?: number;
|
||||
letterSpacing?: number;
|
||||
textWrap?: boolean;
|
||||
|
||||
x?: number;
|
||||
y?: number;
|
||||
position?: Vector2;
|
||||
size?: Size;
|
||||
rotation?: number;
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
offset?: Vector2;
|
||||
scaleX?: number;
|
||||
scaleY?: number;
|
||||
scale?: Vector2;
|
||||
overflow?: boolean;
|
||||
}
|
||||
|
||||
export class Layout extends Node {
|
||||
@property(null)
|
||||
public declare readonly layout: 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)
|
||||
public declare readonly marginBottom: Signal<number, this>;
|
||||
@property(0)
|
||||
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>;
|
||||
@property(0)
|
||||
public declare readonly paddingBottom: Signal<number, this>;
|
||||
@property(0)
|
||||
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(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>;
|
||||
@property(null)
|
||||
public declare readonly fontSize: Signal<number | null, this>;
|
||||
@property(null)
|
||||
public declare readonly fontStyle: Signal<string | null, this>;
|
||||
@property(null)
|
||||
public declare readonly fontWeight: Signal<number | null, this>;
|
||||
@property(null)
|
||||
public declare readonly lineHeight: Signal<number | null, this>;
|
||||
@property(null)
|
||||
public declare readonly letterSpacing: Signal<number | null, this>;
|
||||
@property(null)
|
||||
public declare readonly textWrap: Signal<boolean | null, this>;
|
||||
|
||||
@property(0)
|
||||
protected declare readonly customX: Signal<number, this>;
|
||||
@property(0)
|
||||
public declare readonly x: Signal<number, this>;
|
||||
protected getX(): number {
|
||||
const mode = this.resolvedMode();
|
||||
if (mode !== 'enabled') {
|
||||
return this.customX();
|
||||
}
|
||||
|
||||
return this.computedPosition().x;
|
||||
}
|
||||
protected setX(value: SignalValue<number>) {
|
||||
this.customX(value);
|
||||
}
|
||||
|
||||
@property(0)
|
||||
protected declare readonly customY: Signal<number, this>;
|
||||
@property(0)
|
||||
public declare readonly y: Signal<number, this>;
|
||||
protected getY(): number {
|
||||
const mode = this.resolvedMode();
|
||||
if (mode !== 'enabled') {
|
||||
return this.customY();
|
||||
}
|
||||
|
||||
return this.computedPosition().y;
|
||||
}
|
||||
protected setY(value: SignalValue<number>) {
|
||||
this.customY(value);
|
||||
}
|
||||
|
||||
@property(null)
|
||||
protected declare readonly customWidth: Signal<Length, this>;
|
||||
@property(null)
|
||||
public declare readonly width: Property<Length, number, this>;
|
||||
protected getWidth(): number {
|
||||
return this.computedSize().width;
|
||||
}
|
||||
protected setWidth(value: SignalValue<Length>) {
|
||||
this.customWidth(value);
|
||||
}
|
||||
|
||||
@threadable()
|
||||
protected *tweenWidth(
|
||||
value: SignalValue<Length>,
|
||||
time: number,
|
||||
timingFunction: TimingFunction,
|
||||
interpolationFunction: InterpolationFunction<Length>,
|
||||
): ThreadGenerator {
|
||||
const width = this.customWidth();
|
||||
const lock = typeof width !== 'number' || typeof value !== 'number';
|
||||
let from: number;
|
||||
if (lock) {
|
||||
from = this.width();
|
||||
} else {
|
||||
from = width;
|
||||
}
|
||||
|
||||
let to: number;
|
||||
if (lock) {
|
||||
this.width(value);
|
||||
to = this.width();
|
||||
} else {
|
||||
to = value;
|
||||
}
|
||||
|
||||
this.width(from);
|
||||
lock && this.lockSize();
|
||||
yield* tween(time, value =>
|
||||
this.width(interpolationFunction(from, to, timingFunction(value))),
|
||||
);
|
||||
this.width(value);
|
||||
lock && this.releaseSize();
|
||||
}
|
||||
|
||||
@property(null)
|
||||
protected declare readonly customHeight: Signal<Length, this>;
|
||||
@property(null)
|
||||
public declare readonly height: Property<Length, number, this>;
|
||||
protected getHeight(): number {
|
||||
return this.computedSize().height;
|
||||
}
|
||||
protected setHeight(value: SignalValue<Length>) {
|
||||
this.customHeight(value);
|
||||
}
|
||||
|
||||
@threadable()
|
||||
protected *tweenHeight(
|
||||
value: SignalValue<Length>,
|
||||
time: number,
|
||||
timingFunction: TimingFunction,
|
||||
interpolationFunction: InterpolationFunction<Length>,
|
||||
): ThreadGenerator {
|
||||
const height = this.customHeight();
|
||||
const lock = typeof height !== 'number' || typeof value !== 'number';
|
||||
|
||||
let from: number;
|
||||
if (lock) {
|
||||
from = this.height();
|
||||
} else {
|
||||
from = height;
|
||||
}
|
||||
|
||||
let to: number;
|
||||
if (lock) {
|
||||
this.height(value);
|
||||
to = this.height();
|
||||
} else {
|
||||
to = value;
|
||||
}
|
||||
|
||||
this.height(from);
|
||||
lock && this.lockSize();
|
||||
yield* tween(time, value =>
|
||||
this.height(interpolationFunction(from, to, timingFunction(value))),
|
||||
);
|
||||
this.height(value);
|
||||
lock && this.releaseSize();
|
||||
}
|
||||
|
||||
@compound(['width', 'height'])
|
||||
@property(undefined, Size.lerp, Size)
|
||||
public declare readonly size: Property<
|
||||
{width: Length; height: Length},
|
||||
Size,
|
||||
this
|
||||
>;
|
||||
|
||||
@computed()
|
||||
protected customSize(): {width: Length; height: Length} {
|
||||
return {
|
||||
width: this.customWidth(),
|
||||
height: this.customHeight(),
|
||||
};
|
||||
}
|
||||
|
||||
@threadable()
|
||||
protected *tweenSize(
|
||||
value: SignalValue<{width: Length; height: Length}>,
|
||||
time: number,
|
||||
timingFunction: TimingFunction,
|
||||
interpolationFunction: InterpolationFunction<Size>,
|
||||
): ThreadGenerator {
|
||||
const size = this.customSize();
|
||||
let from: Size;
|
||||
if (typeof size.height !== 'number' || typeof size.width !== 'number') {
|
||||
from = this.size();
|
||||
} else {
|
||||
from = <Size>size;
|
||||
}
|
||||
|
||||
let to: Size;
|
||||
if (
|
||||
typeof value === 'object' &&
|
||||
typeof value.height === 'number' &&
|
||||
typeof value.width === 'number'
|
||||
) {
|
||||
to = <Size>value;
|
||||
} else {
|
||||
this.size(value);
|
||||
to = this.size();
|
||||
}
|
||||
|
||||
this.size(from);
|
||||
this.lockSize();
|
||||
yield* tween(time, value =>
|
||||
this.size(interpolationFunction(from, to, timingFunction(value))),
|
||||
);
|
||||
this.releaseSize();
|
||||
this.size(value);
|
||||
}
|
||||
|
||||
@property(0)
|
||||
public declare readonly rotation: Signal<number, this>;
|
||||
|
||||
@property(0)
|
||||
public declare readonly offsetX: Signal<number, this>;
|
||||
|
||||
@property(0)
|
||||
public declare readonly offsetY: Signal<number, this>;
|
||||
|
||||
@compound({x: 'offsetX', y: 'offsetY'})
|
||||
@property(undefined, Vector2.lerp, Vector2)
|
||||
public declare readonly offset: Signal<Vector2, this>;
|
||||
|
||||
@property(1)
|
||||
public declare readonly scaleX: Signal<number, this>;
|
||||
|
||||
@property(1)
|
||||
public declare readonly scaleY: Signal<number, this>;
|
||||
|
||||
@compound({x: 'scaleX', y: 'scaleY'})
|
||||
@property(undefined, Vector2.lerp, Vector2)
|
||||
public declare readonly scale: Signal<Vector2, this>;
|
||||
|
||||
@property(false)
|
||||
public declare readonly overflow: Signal<boolean, this>;
|
||||
|
||||
@compound(['x', 'y'])
|
||||
@property(undefined, Vector2.lerp, Vector2)
|
||||
public declare readonly position: Signal<Vector2, this>;
|
||||
|
||||
@property(undefined, Vector2.lerp, Vector2)
|
||||
public declare readonly absolutePosition: Signal<Vector2, this>;
|
||||
|
||||
protected getAbsolutePosition(): Vector2 {
|
||||
const matrix = this.localToWorld();
|
||||
return new Vector2(matrix.m41, matrix.m42);
|
||||
}
|
||||
|
||||
protected setAbsolutePosition(value: SignalValue<Vector2>) {
|
||||
if (isReactive(value)) {
|
||||
this.position(() => value().transformAsPoint(this.worldToParent()));
|
||||
} else {
|
||||
this.position(value.transformAsPoint(this.worldToParent()));
|
||||
}
|
||||
}
|
||||
|
||||
@property()
|
||||
public declare readonly absoluteRotation: Signal<Vector2, this>;
|
||||
|
||||
protected getAbsoluteRotation() {
|
||||
const matrix = this.localToWorld();
|
||||
return (Math.atan2(matrix.m12, matrix.m11) * 180) / Math.PI;
|
||||
}
|
||||
|
||||
protected setAbsoluteRotation(value: SignalValue<number>) {
|
||||
if (isReactive(value)) {
|
||||
this.rotation(() => transformAngle(value(), this.worldToParent()));
|
||||
} else {
|
||||
this.rotation(transformAngle(value, this.worldToParent()));
|
||||
}
|
||||
}
|
||||
|
||||
public readonly element: HTMLElement;
|
||||
public readonly styles: CSSStyleDeclaration;
|
||||
|
||||
@property(0)
|
||||
protected declare readonly sizeLockCounter: Signal<number, this>;
|
||||
|
||||
public constructor({tagName = 'div', ...props}: LayoutProps) {
|
||||
super(props);
|
||||
|
||||
this.element = TwoDView.document.createElement(tagName);
|
||||
this.element.style.display = 'flex';
|
||||
this.element.style.boxSizing = 'border-box';
|
||||
|
||||
this.styles = getComputedStyle(this.element);
|
||||
}
|
||||
|
||||
public lockSize() {
|
||||
this.sizeLockCounter(this.sizeLockCounter() + 1);
|
||||
}
|
||||
|
||||
public releaseSize() {
|
||||
this.sizeLockCounter(this.sizeLockCounter() - 1);
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected parentTransform(): Layout | null {
|
||||
let parent: Node | null = this.parent();
|
||||
while (parent) {
|
||||
if (parent instanceof Layout) {
|
||||
return parent;
|
||||
}
|
||||
parent = parent.parent();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolved layout mode of this node.
|
||||
*
|
||||
* @remarks
|
||||
* When the mode is `null`, its value will be inherited from the parent.
|
||||
*
|
||||
* Use {@link Layout.mode} to get the raw mode set for this node (without
|
||||
* inheritance).
|
||||
*/
|
||||
@computed()
|
||||
protected resolvedMode(): ResolvedLayoutMode {
|
||||
const parentMode = this.parentTransform()?.resolvedMode();
|
||||
let mode = this.layout();
|
||||
|
||||
if (mode === null) {
|
||||
if (!parentMode || parentMode === 'disabled') {
|
||||
mode = 'disabled';
|
||||
} else {
|
||||
mode = 'enabled';
|
||||
}
|
||||
}
|
||||
|
||||
return mode;
|
||||
}
|
||||
|
||||
protected override localToParent(): DOMMatrix {
|
||||
const matrix = new DOMMatrix();
|
||||
const size = this.computedSize();
|
||||
matrix.translateSelf(this.x(), this.y());
|
||||
matrix.rotateSelf(0, 0, this.rotation());
|
||||
matrix.scaleSelf(this.scaleX(), this.scaleY());
|
||||
matrix.translateSelf(
|
||||
(size.width / -2) * this.offsetX(),
|
||||
(size.height / -2) * this.offsetY(),
|
||||
);
|
||||
|
||||
return matrix;
|
||||
}
|
||||
|
||||
protected getComputedLayout(): Rect {
|
||||
return new Rect(this.element.getBoundingClientRect());
|
||||
}
|
||||
|
||||
@computed()
|
||||
public computedPosition(): Vector2 {
|
||||
this.requestLayoutUpdate();
|
||||
const rect = this.getComputedLayout();
|
||||
|
||||
const position = new Vector2(
|
||||
rect.x + (rect.width / 2) * this.offsetX(),
|
||||
rect.y + (rect.height / 2) * this.offsetY(),
|
||||
);
|
||||
|
||||
const parent = this.parentTransform();
|
||||
if (parent) {
|
||||
const parentRect = parent.getComputedLayout();
|
||||
position.x -= parentRect.x + (parentRect.width - rect.width) / 2;
|
||||
position.y -= parentRect.y + (parentRect.height - rect.height) / 2;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected computedSize(): Size {
|
||||
this.requestLayoutUpdate();
|
||||
return new Size(this.getComputedLayout());
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest layout root and apply any new layout changes.
|
||||
*/
|
||||
@computed()
|
||||
protected requestLayoutUpdate() {
|
||||
const mode = this.resolvedMode();
|
||||
const parent = this.parentTransform();
|
||||
if (mode === 'disabled' || mode === 'root' || !parent) {
|
||||
this.view()?.element.append(this.element);
|
||||
parent?.requestFontUpdate();
|
||||
this.updateLayout();
|
||||
} else {
|
||||
parent.requestLayoutUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply any new layout changes to this node and its children.
|
||||
*/
|
||||
@computed()
|
||||
protected updateLayout() {
|
||||
this.applyFont();
|
||||
this.applyFlex();
|
||||
this.syncDOM();
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected syncDOM() {
|
||||
this.element.innerText = '';
|
||||
const queue = [...this.children()];
|
||||
while (queue.length) {
|
||||
const child = queue.shift()!;
|
||||
if (child instanceof Layout) {
|
||||
this.element.append(child.element);
|
||||
child.updateLayout();
|
||||
} else {
|
||||
queue.push(...child.children());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply any new font changes to this node and all of its ancestors.
|
||||
*/
|
||||
@computed()
|
||||
protected requestFontUpdate() {
|
||||
this.parentTransform()?.requestFontUpdate();
|
||||
this.applyFont();
|
||||
}
|
||||
|
||||
protected override getCacheRect(): Rect {
|
||||
const size = this.computedSize();
|
||||
return new Rect(size.vector.scale(-0.5), size);
|
||||
}
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected applyFlex() {
|
||||
const mode = this.layout();
|
||||
this.element.style.position =
|
||||
mode === 'disabled' || mode === 'root' ? 'absolute' : 'relative';
|
||||
|
||||
this.element.style.width = this.parseLength(this.customWidth());
|
||||
this.element.style.height = this.parseLength(this.customHeight());
|
||||
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.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.rowGap = this.parseLength(this.rowGap());
|
||||
this.element.style.columnGap = this.parseLength(this.columnGap());
|
||||
this.element.style.gap = this.parseLength(this.gap());
|
||||
|
||||
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()
|
||||
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 ? '' : wrap ? 'normal' : 'nowrap';
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,13 @@
|
||||
import {
|
||||
compound,
|
||||
computed,
|
||||
initialize,
|
||||
Property,
|
||||
property,
|
||||
} from '../decorators';
|
||||
import {
|
||||
Vector2,
|
||||
transformAngle,
|
||||
Rect,
|
||||
Size,
|
||||
transformScalar,
|
||||
} from '@motion-canvas/core/lib/types';
|
||||
import {
|
||||
createSignal,
|
||||
isReactive,
|
||||
Reference,
|
||||
Signal,
|
||||
SignalValue,
|
||||
} from '@motion-canvas/core/lib/utils';
|
||||
import {
|
||||
InterpolationFunction,
|
||||
TimingFunction,
|
||||
tween,
|
||||
map,
|
||||
} from '@motion-canvas/core/lib/tweening';
|
||||
import {Layout, LayoutProps, Length, ResolvedLayoutMode} from '../partials';
|
||||
import {compound, computed, initialize, property} from '../decorators';
|
||||
import {Vector2, Rect, transformScalar} from '@motion-canvas/core/lib/types';
|
||||
import {createSignal, Reference, Signal} from '@motion-canvas/core/lib/utils';
|
||||
import {ComponentChild, ComponentChildren} from './types';
|
||||
import {threadable} from '@motion-canvas/core/lib/decorators';
|
||||
import {ThreadGenerator, Promisable} from '@motion-canvas/core/lib/threading';
|
||||
import {Promisable} from '@motion-canvas/core/lib/threading';
|
||||
import {TwoDView} from '../scenes';
|
||||
|
||||
export interface NodeProps {
|
||||
ref?: Reference<Node>;
|
||||
children?: ComponentChildren;
|
||||
x?: number;
|
||||
y?: number;
|
||||
position?: Vector2;
|
||||
width?: Length;
|
||||
height?: Length;
|
||||
size?: Size;
|
||||
rotation?: number;
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
offset?: Vector2;
|
||||
scaleX?: number;
|
||||
scaleY?: number;
|
||||
scale?: Vector2;
|
||||
layout?: LayoutProps;
|
||||
opacity?: number;
|
||||
blur?: number;
|
||||
brightness?: number;
|
||||
@@ -61,208 +22,13 @@ export interface NodeProps {
|
||||
shadowOffsetX?: number;
|
||||
shadowOffsetY?: number;
|
||||
shadowOffset?: Vector2;
|
||||
overflow?: boolean;
|
||||
cache?: boolean;
|
||||
composite?: boolean | Node;
|
||||
composite?: boolean;
|
||||
}
|
||||
|
||||
export class Node<TProps extends NodeProps = NodeProps>
|
||||
implements Promisable<Node>
|
||||
{
|
||||
export class Node implements Promisable<Node> {
|
||||
public declare isClass: boolean;
|
||||
|
||||
public readonly layout: Layout;
|
||||
|
||||
@property(0)
|
||||
public declare readonly x: Signal<number, this>;
|
||||
protected readonly customX = createSignal(0, map, this);
|
||||
protected getX(): number {
|
||||
return this.computedPosition().x;
|
||||
}
|
||||
protected setX(value: SignalValue<number>) {
|
||||
this.customX(value);
|
||||
}
|
||||
|
||||
@property(0)
|
||||
public declare readonly y: Signal<number, this>;
|
||||
protected readonly customY = createSignal(0, map, this);
|
||||
protected getY(): number {
|
||||
return this.computedPosition().y;
|
||||
}
|
||||
protected setY(value: SignalValue<number>) {
|
||||
this.customY(value);
|
||||
}
|
||||
|
||||
@property(null)
|
||||
public declare readonly width: Property<Length, number, this>;
|
||||
protected readonly customWidth = createSignal<Length, this>(
|
||||
undefined,
|
||||
undefined,
|
||||
this,
|
||||
);
|
||||
protected getWidth(): number {
|
||||
return this.computedSize().width;
|
||||
}
|
||||
protected setWidth(value: SignalValue<Length>) {
|
||||
this.customWidth(value);
|
||||
}
|
||||
|
||||
@threadable()
|
||||
protected *tweenWidth(
|
||||
value: SignalValue<Length>,
|
||||
time: number,
|
||||
timingFunction: TimingFunction,
|
||||
interpolationFunction: InterpolationFunction<Length>,
|
||||
): ThreadGenerator {
|
||||
const width = this.customWidth();
|
||||
let from: number;
|
||||
if (typeof width === 'number') {
|
||||
from = width;
|
||||
} else {
|
||||
from = this.width();
|
||||
}
|
||||
|
||||
let to: number;
|
||||
if (typeof value === 'number') {
|
||||
to = value;
|
||||
} else {
|
||||
this.width(value);
|
||||
to = this.width();
|
||||
}
|
||||
|
||||
this.width(from);
|
||||
this.layout.lockSize();
|
||||
yield* tween(time, value =>
|
||||
this.width(interpolationFunction(from, to, timingFunction(value))),
|
||||
);
|
||||
this.width(value);
|
||||
this.layout.releaseSize();
|
||||
}
|
||||
|
||||
@property(null)
|
||||
public declare readonly height: Property<Length, number, this>;
|
||||
protected readonly customHeight = createSignal<Length, this>(
|
||||
undefined,
|
||||
undefined,
|
||||
this,
|
||||
);
|
||||
protected getHeight(): number {
|
||||
return this.computedSize().height;
|
||||
}
|
||||
protected setHeight(value: SignalValue<Length>) {
|
||||
this.customHeight(value);
|
||||
}
|
||||
|
||||
@threadable()
|
||||
protected *tweenHeight(
|
||||
value: SignalValue<Length>,
|
||||
time: number,
|
||||
timingFunction: TimingFunction,
|
||||
interpolationFunction: InterpolationFunction<Length>,
|
||||
): ThreadGenerator {
|
||||
const height = this.customHeight();
|
||||
let from: number;
|
||||
if (typeof height === 'number') {
|
||||
from = height;
|
||||
} else {
|
||||
from = this.height();
|
||||
}
|
||||
|
||||
let to: number;
|
||||
if (typeof value === 'number') {
|
||||
to = value;
|
||||
} else {
|
||||
this.height(value);
|
||||
to = this.height();
|
||||
}
|
||||
|
||||
this.height(from);
|
||||
this.layout.lockSize();
|
||||
yield* tween(time, value =>
|
||||
this.height(interpolationFunction(from, to, timingFunction(value))),
|
||||
);
|
||||
this.height(value);
|
||||
this.layout.releaseSize();
|
||||
}
|
||||
|
||||
@compound(['width', 'height'])
|
||||
@property(undefined, Size.lerp, Size)
|
||||
public declare readonly size: Property<
|
||||
{width: Length; height: Length},
|
||||
Size,
|
||||
this
|
||||
>;
|
||||
|
||||
@computed()
|
||||
protected customSize(): {width: Length; height: Length} {
|
||||
return {
|
||||
width: this.customWidth(),
|
||||
height: this.customHeight(),
|
||||
};
|
||||
}
|
||||
|
||||
@threadable()
|
||||
protected *tweenSize(
|
||||
value: SignalValue<{width: Length; height: Length}>,
|
||||
time: number,
|
||||
timingFunction: TimingFunction,
|
||||
interpolationFunction: InterpolationFunction<Size>,
|
||||
): ThreadGenerator {
|
||||
const size = this.customSize();
|
||||
let from: Size;
|
||||
if (typeof size.height !== 'number' || typeof size.width !== 'number') {
|
||||
from = this.size();
|
||||
} else {
|
||||
from = <Size>size;
|
||||
}
|
||||
|
||||
let to: Size;
|
||||
if (
|
||||
typeof value === 'object' &&
|
||||
typeof value.height === 'number' &&
|
||||
typeof value.width === 'number'
|
||||
) {
|
||||
to = <Size>value;
|
||||
} else {
|
||||
this.size(value);
|
||||
to = this.size();
|
||||
}
|
||||
|
||||
this.size(from);
|
||||
this.layout.lockSize();
|
||||
yield* tween(time, value =>
|
||||
this.size(interpolationFunction(from, to, timingFunction(value))),
|
||||
);
|
||||
this.layout.releaseSize();
|
||||
this.size(value);
|
||||
}
|
||||
|
||||
@property(0)
|
||||
public declare readonly rotation: Signal<number, this>;
|
||||
|
||||
@property(0)
|
||||
public declare readonly offsetX: Signal<number, this>;
|
||||
|
||||
@property(0)
|
||||
public declare readonly offsetY: Signal<number, this>;
|
||||
|
||||
@compound({x: 'offsetX', y: 'offsetY'})
|
||||
@property(undefined, Vector2.lerp, Vector2)
|
||||
public declare readonly offset: Signal<Vector2, this>;
|
||||
|
||||
@property(1)
|
||||
public declare readonly scaleX: Signal<number, this>;
|
||||
|
||||
@property(1)
|
||||
public declare readonly scaleY: Signal<number, this>;
|
||||
|
||||
@compound({x: 'scaleX', y: 'scaleY'})
|
||||
@property(undefined, Vector2.lerp, Vector2)
|
||||
public declare readonly scale: Signal<Vector2, this>;
|
||||
|
||||
@property(false)
|
||||
public declare readonly overflow: Signal<boolean, this>;
|
||||
|
||||
@property(false)
|
||||
public declare readonly cache: Signal<boolean, this>;
|
||||
|
||||
@@ -345,9 +111,13 @@ export class Node<TProps extends NodeProps = NodeProps>
|
||||
protected filterString(): string {
|
||||
let filters = '';
|
||||
|
||||
const blur = this.blur();
|
||||
if (blur !== 0) {
|
||||
filters += ` blur(${transformScalar(blur, this.compositeToWorld())}px)`;
|
||||
const invert = this.invert();
|
||||
if (invert !== 0) {
|
||||
filters += ` invert(${invert * 100}%)`;
|
||||
}
|
||||
const sepia = this.sepia();
|
||||
if (sepia !== 0) {
|
||||
filters += ` sepia(${sepia * 100}%)`;
|
||||
}
|
||||
const brightness = this.brightness();
|
||||
if (brightness !== 1) {
|
||||
@@ -365,94 +135,26 @@ export class Node<TProps extends NodeProps = NodeProps>
|
||||
if (hue !== 0) {
|
||||
filters += ` hue-rotate(${hue}deg)`;
|
||||
}
|
||||
const invert = this.invert();
|
||||
if (invert !== 0) {
|
||||
filters += ` invert(${invert * 100}%)`;
|
||||
}
|
||||
const saturate = this.saturate();
|
||||
if (saturate !== 1) {
|
||||
filters += ` saturate(${saturate * 100}%)`;
|
||||
}
|
||||
const sepia = this.sepia();
|
||||
if (sepia !== 0) {
|
||||
filters += ` sepia(${sepia * 100}%)`;
|
||||
const blur = this.blur();
|
||||
if (blur !== 0) {
|
||||
filters += ` blur(${transformScalar(blur, this.compositeToWorld())}px)`;
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
@compound(['x', 'y'])
|
||||
@property(undefined, Vector2.lerp, Vector2)
|
||||
public declare readonly position: Signal<Vector2, this>;
|
||||
public readonly children = createSignal<Node[]>([]);
|
||||
public readonly parent = createSignal<Node | null>(null);
|
||||
|
||||
@property(undefined, Vector2.lerp, Vector2)
|
||||
public declare readonly absolutePosition: Signal<Vector2, this>;
|
||||
|
||||
protected getAbsolutePosition(): Vector2 {
|
||||
const matrix = this.localToWorld();
|
||||
return new Vector2(matrix.m41, matrix.m42);
|
||||
}
|
||||
|
||||
protected setAbsolutePosition(value: SignalValue<Vector2>) {
|
||||
if (isReactive(value)) {
|
||||
this.position(() => value().transformAsPoint(this.worldToParent()));
|
||||
} else {
|
||||
this.position(value.transformAsPoint(this.worldToParent()));
|
||||
}
|
||||
}
|
||||
|
||||
@property()
|
||||
public declare readonly absoluteRotation: Signal<Vector2, this>;
|
||||
|
||||
protected getAbsoluteRotation() {
|
||||
const matrix = this.localToWorld();
|
||||
return (Math.atan2(matrix.m12, matrix.m11) * 180) / Math.PI;
|
||||
}
|
||||
|
||||
protected setAbsoluteRotation(value: SignalValue<number>) {
|
||||
if (isReactive(value)) {
|
||||
this.rotation(() => transformAngle(value(), this.worldToParent()));
|
||||
} else {
|
||||
this.rotation(transformAngle(value, this.worldToParent()));
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly children = createSignal<Node[]>([]);
|
||||
protected readonly parent = createSignal<Node | null>(null);
|
||||
protected readonly quality = createSignal(false);
|
||||
|
||||
public constructor({children, layout, ...rest}: TProps) {
|
||||
this.layout = new Layout(layout ?? {});
|
||||
public constructor({children, ...rest}: NodeProps) {
|
||||
initialize(this, {defaults: rest});
|
||||
this.append(children);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolved layout mode of this node.
|
||||
*
|
||||
* @remarks
|
||||
* When the mode is `null`, its value will be inherited from the parent.
|
||||
*
|
||||
* Use {@link Layout.mode} to get the raw mode set for this node (without
|
||||
* inheritance).
|
||||
*/
|
||||
@computed()
|
||||
public mode(): ResolvedLayoutMode {
|
||||
const parent = this.parent();
|
||||
const parentMode = parent?.mode();
|
||||
let mode = this.layout.mode();
|
||||
|
||||
if (mode === null) {
|
||||
if (!parentMode || parentMode === 'disabled') {
|
||||
mode = 'disabled';
|
||||
} else {
|
||||
mode = 'enabled';
|
||||
}
|
||||
}
|
||||
|
||||
return mode;
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected localToWorld(): DOMMatrix {
|
||||
const parent = this.parent();
|
||||
@@ -473,18 +175,7 @@ export class Node<TProps extends NodeProps = NodeProps>
|
||||
|
||||
@computed()
|
||||
protected localToParent(): DOMMatrix {
|
||||
const matrix = new DOMMatrix();
|
||||
const size = this.computedSize();
|
||||
const position = this.computedPosition();
|
||||
matrix.translateSelf(position.x, position.y);
|
||||
matrix.rotateSelf(0, 0, this.rotation());
|
||||
matrix.scaleSelf(this.scaleX(), this.scaleY());
|
||||
matrix.translateSelf(
|
||||
(size.width / -2) * this.offsetX(),
|
||||
(size.height / -2) * this.offsetY(),
|
||||
);
|
||||
|
||||
return matrix;
|
||||
return new DOMMatrix();
|
||||
}
|
||||
|
||||
@computed()
|
||||
@@ -544,101 +235,8 @@ export class Node<TProps extends NodeProps = NodeProps>
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected computedPosition(): Vector2 {
|
||||
const mode = this.mode();
|
||||
if (mode !== 'enabled') {
|
||||
return new Vector2(this.customX(), this.customY());
|
||||
}
|
||||
|
||||
this.requestLayoutUpdate();
|
||||
this.requestFontUpdate();
|
||||
const rect = this.layout.getComputedLayout();
|
||||
|
||||
const position = new Vector2(
|
||||
rect.x + (rect.width / 2) * this.offsetX(),
|
||||
rect.y + (rect.height / 2) * this.offsetY(),
|
||||
);
|
||||
|
||||
const parent = this.parent();
|
||||
if (parent) {
|
||||
const parentRect = parent.layout.getComputedLayout();
|
||||
position.x -= parentRect.x + (parentRect.width - rect.width) / 2;
|
||||
position.y -= parentRect.y + (parentRect.height - rect.height) / 2;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected computedSize(): Size {
|
||||
this.requestLayoutUpdate();
|
||||
this.requestFontUpdate();
|
||||
const rect = this.layout.getComputedLayout();
|
||||
|
||||
return new Size(rect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest layout root and apply any new layout changes.
|
||||
*/
|
||||
@computed()
|
||||
protected requestLayoutUpdate() {
|
||||
const mode = this.layout.mode();
|
||||
const parent = this.parent();
|
||||
if (mode === 'disabled' || mode === 'root' || !parent) {
|
||||
this.updateLayout();
|
||||
parent?.requestFontUpdate();
|
||||
} else {
|
||||
parent.requestLayoutUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply any new layout changes to this node and its children.
|
||||
*/
|
||||
@computed()
|
||||
protected updateLayout() {
|
||||
this.updateFont();
|
||||
this.layout
|
||||
.setWidth(this.customWidth())
|
||||
.setHeight(this.customHeight())
|
||||
.applyFlex();
|
||||
this.applyLayoutChanges();
|
||||
for (const child of this.children()) {
|
||||
child.updateLayout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply any custom layout-related changes to this node.
|
||||
*/
|
||||
protected applyLayoutChanges() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply any new font changes to this node and all of its ancestors.
|
||||
*/
|
||||
@computed()
|
||||
protected requestFontUpdate() {
|
||||
this.parent()?.requestFontUpdate();
|
||||
this.updateFont();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply any new font changes to this node.
|
||||
*/
|
||||
@computed()
|
||||
protected updateFont() {
|
||||
this.layout.applyFont();
|
||||
this.applyFontChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply any custom font-related changes to this node.
|
||||
*/
|
||||
protected applyFontChanges() {
|
||||
// do nothing
|
||||
public view(): TwoDView | null {
|
||||
return this.parent()?.view() ?? null;
|
||||
}
|
||||
|
||||
public append(node: ComponentChildren): this {
|
||||
@@ -663,12 +261,10 @@ export class Node<TProps extends NodeProps = NodeProps>
|
||||
}
|
||||
|
||||
if (current) {
|
||||
current.layout.element.removeChild(this.layout.element);
|
||||
current.children(current.children().filter(child => child !== this));
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
parent.layout.element.append(this.layout.element);
|
||||
parent.children([...parent.children(), this]);
|
||||
}
|
||||
|
||||
@@ -726,8 +322,31 @@ export class Node<TProps extends NodeProps = NodeProps>
|
||||
* The returned rectangle should be in local space.
|
||||
*/
|
||||
protected getCacheRect(): Rect {
|
||||
const size = this.computedSize();
|
||||
return new Rect(size.vector.scale(-0.5), size);
|
||||
return new Rect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a rectangle encapsulating the contents rendered by this node as well
|
||||
* as its children.
|
||||
*/
|
||||
@computed()
|
||||
protected cacheRect(): Rect {
|
||||
const cache = this.getCacheRect();
|
||||
const children = this.children();
|
||||
if (children.length === 0) {
|
||||
return cache.pixelPerfect;
|
||||
}
|
||||
|
||||
const points: Vector2[] = cache.corners;
|
||||
for (const child of children) {
|
||||
const childCache = child.fullCacheRect();
|
||||
const childMatrix = child.localToParent();
|
||||
points.push(
|
||||
...childCache.corners.map(r => r.transformAsPoint(childMatrix)),
|
||||
);
|
||||
}
|
||||
|
||||
return Rect.fromPoints(...points).pixelPerfect;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -737,12 +356,13 @@ export class Node<TProps extends NodeProps = NodeProps>
|
||||
* @remarks
|
||||
* The returned rectangle should be in local space.
|
||||
*/
|
||||
protected getFullCacheRect() {
|
||||
@computed()
|
||||
protected fullCacheRect(): Rect {
|
||||
const matrix = this.compositeToLocal();
|
||||
const shadowOffset = this.shadowOffset().transform(matrix);
|
||||
const shadowBlur = transformScalar(this.shadowBlur(), matrix);
|
||||
|
||||
const result = this.getCacheRect().expand(this.blur() * 2 + shadowBlur);
|
||||
const result = this.cacheRect().expand(this.blur() * 2 + shadowBlur);
|
||||
|
||||
if (shadowOffset.x < 0) {
|
||||
result.x += shadowOffset.x;
|
||||
@@ -761,35 +381,6 @@ export class Node<TProps extends NodeProps = NodeProps>
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a rectangle encapsulating the contents rendered by this node as well
|
||||
* as its children.
|
||||
*/
|
||||
@computed()
|
||||
protected cacheRect(): Rect {
|
||||
const cache = this.getCacheRect();
|
||||
const children = this.children();
|
||||
if (!this.overflow() || children.length === 0) {
|
||||
return cache.pixelPerfect;
|
||||
}
|
||||
|
||||
const points: Vector2[] = cache.corners;
|
||||
for (const child of children) {
|
||||
const childCache = child.fullCacheRect();
|
||||
const childMatrix = child.localToParent();
|
||||
points.push(
|
||||
...childCache.corners.map(r => r.transformAsPoint(childMatrix)),
|
||||
);
|
||||
}
|
||||
|
||||
return Rect.fromPoints(...points).pixelPerfect;
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected fullCacheRect(): Rect {
|
||||
return Rect.fromRects(this.cacheRect(), this.getFullCacheRect());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the given context for drawing a cached node onto it.
|
||||
*
|
||||
|
||||
@@ -1,10 +1,37 @@
|
||||
import {Shape} from './Shape';
|
||||
import {Signal} from '@motion-canvas/core/lib/utils';
|
||||
import {Shape, ShapeProps} from './Shape';
|
||||
import {property} from '../decorators';
|
||||
|
||||
export interface RectProps extends ShapeProps {
|
||||
radius?: number;
|
||||
}
|
||||
|
||||
export class Rect extends Shape {
|
||||
@property(0)
|
||||
public declare readonly radius: Signal<number, this>;
|
||||
|
||||
public constructor(props: RectProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
protected override getPath(): Path2D {
|
||||
const path = new Path2D();
|
||||
const {width, height} = this.computedSize();
|
||||
path.rect(-width / 2, -height / 2, width, height);
|
||||
const radius = this.radius();
|
||||
const {width, height} = this.size();
|
||||
const x = width / -2;
|
||||
const y = height / -2;
|
||||
|
||||
if (radius > 0) {
|
||||
const maxRadius = Math.min(height / 2, width / 2, radius);
|
||||
path.moveTo(x + maxRadius, y);
|
||||
path.arcTo(x + width, y, x + width, y + height, maxRadius);
|
||||
path.arcTo(x + width, y + height, x, y + height, maxRadius);
|
||||
path.arcTo(x, y + height, x, y, maxRadius);
|
||||
path.arcTo(x, y, x + width, y, maxRadius);
|
||||
} else {
|
||||
path.rect(x, y, width, height);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {Node, NodeProps} from './Node';
|
||||
import {Gradient, Pattern} from '../partials';
|
||||
import {property} from '../decorators';
|
||||
import {Signal} from '@motion-canvas/core/lib/utils';
|
||||
import {Rect} from '@motion-canvas/core/lib/types';
|
||||
import {Layout, LayoutProps} from './Layout';
|
||||
|
||||
export type CanvasStyle = null | string | Gradient | Pattern;
|
||||
|
||||
export interface ShapeProps extends NodeProps {
|
||||
export interface ShapeProps extends LayoutProps {
|
||||
fill?: CanvasStyle;
|
||||
stroke?: CanvasStyle;
|
||||
strokeFirst?: boolean;
|
||||
@@ -17,7 +17,7 @@ export interface ShapeProps extends NodeProps {
|
||||
lineDashOffset?: number;
|
||||
}
|
||||
|
||||
export abstract class Shape<T extends ShapeProps = ShapeProps> extends Node<T> {
|
||||
export abstract class Shape extends Layout {
|
||||
@property(null)
|
||||
public declare readonly fill: Signal<CanvasStyle, this>;
|
||||
@property(null)
|
||||
@@ -35,6 +35,10 @@ export abstract class Shape<T extends ShapeProps = ShapeProps> extends Node<T> {
|
||||
@property(0)
|
||||
public declare readonly lineDashOffset: Signal<number, this>;
|
||||
|
||||
public constructor(props: ShapeProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
protected parseCanvasStyle(
|
||||
style: CanvasStyle,
|
||||
context: CanvasRenderingContext2D,
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface TextProps extends ShapeProps {
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export class Text extends Shape<TextProps> {
|
||||
export class Text extends Shape {
|
||||
protected static segmenter;
|
||||
|
||||
static {
|
||||
@@ -35,15 +35,14 @@ export class Text extends Shape<TextProps> {
|
||||
protected override draw(context: CanvasRenderingContext2D) {
|
||||
this.requestFontUpdate();
|
||||
this.applyStyle(context);
|
||||
context.font = this.layout.styles.font;
|
||||
context.textBaseline = 'middle';
|
||||
context.font = this.styles.font;
|
||||
|
||||
const parentRect = this.layout.element.getBoundingClientRect();
|
||||
const {width, height} = this.computedSize();
|
||||
const parentRect = this.element.getBoundingClientRect();
|
||||
const {width, height} = this.size();
|
||||
const range = document.createRange();
|
||||
let line = '';
|
||||
const lineRect = new Rect();
|
||||
for (const childNode of this.layout.element.childNodes) {
|
||||
for (const childNode of this.element.childNodes) {
|
||||
if (!childNode.textContent) {
|
||||
continue;
|
||||
}
|
||||
@@ -75,29 +74,36 @@ export class Text extends Shape<TextProps> {
|
||||
text: string,
|
||||
rect: Rect,
|
||||
) {
|
||||
const y =
|
||||
rect.y +
|
||||
rect.height / 2 +
|
||||
context.measureText(text).fontBoundingBoxDescent;
|
||||
|
||||
if (this.lineWidth() <= 0) {
|
||||
context.fillText(text, rect.x, rect.y + rect.height / 2);
|
||||
} else if (this.strokeFirst()) {
|
||||
context.strokeText(text, rect.x, rect.y + rect.height / 2);
|
||||
context.fillText(text, rect.x, rect.y + rect.height / 2);
|
||||
context.strokeText(text, rect.x, y);
|
||||
context.fillText(text, rect.x, y);
|
||||
} else {
|
||||
context.fillText(text, rect.x, rect.y + rect.height / 2);
|
||||
context.strokeText(text, rect.x, rect.y + rect.height / 2);
|
||||
context.fillText(text, rect.x, y);
|
||||
context.strokeText(text, rect.x, y);
|
||||
}
|
||||
}
|
||||
|
||||
protected override applyFontChanges() {
|
||||
super.applyFontChanges();
|
||||
const wrap = this.layout.styles.whiteSpace !== 'nowrap';
|
||||
protected override updateLayout() {
|
||||
this.applyFont();
|
||||
this.applyFlex();
|
||||
|
||||
const wrap = this.styles.whiteSpace !== 'nowrap';
|
||||
const text = this.text();
|
||||
|
||||
if (wrap && Text.segmenter) {
|
||||
this.layout.element.innerText = '';
|
||||
this.element.innerText = '';
|
||||
for (const word of Text.segmenter.segment(text)) {
|
||||
this.layout.element.appendChild(document.createTextNode(word.segment));
|
||||
this.element.appendChild(document.createTextNode(word.segment));
|
||||
}
|
||||
} else {
|
||||
this.layout.element.innerText = this.text();
|
||||
this.element.innerText = this.text();
|
||||
}
|
||||
|
||||
if (wrap && !Text.segmenter) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './Circle';
|
||||
export * from './Image';
|
||||
export * from './Layout';
|
||||
export * from './Node';
|
||||
export * from './Rect';
|
||||
export * from './Text';
|
||||
|
||||
@@ -14,15 +14,21 @@ export type ComponentChild =
|
||||
export type ComponentChildren = ComponentChild | ComponentChild[];
|
||||
export type NodeChildren = Node | Node[];
|
||||
|
||||
export type PropsOf<T> = T extends NodeConstructor<infer P>
|
||||
? P
|
||||
: T extends FunctionComponent<infer P>
|
||||
? P
|
||||
: never;
|
||||
|
||||
export interface JSXProps {
|
||||
children?: ComponentChildren;
|
||||
ref?: Reference<Node>;
|
||||
}
|
||||
|
||||
export interface FunctionComponent<T = any> {
|
||||
(props: T): Node<T> | null;
|
||||
(props: T): Node | null;
|
||||
}
|
||||
|
||||
export interface NodeConstructor<T = any> {
|
||||
new (props: T): Node<T>;
|
||||
new (props: T): Node;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import {createComputed} from '@motion-canvas/core/lib/utils/createComputed';
|
||||
*/
|
||||
export function computed(): MethodDecorator {
|
||||
return (target: any, key) => {
|
||||
const method = target[key];
|
||||
addInitializer(target, (instance: any) => {
|
||||
const method = Object.getPrototypeOf(instance)[key];
|
||||
instance[key] = createComputed(method.bind(instance));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
import {
|
||||
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,
|
||||
FlexWrap,
|
||||
FlexJustify,
|
||||
LayoutMode,
|
||||
Length,
|
||||
FlexBasis,
|
||||
} from './types';
|
||||
import {TwoDView} from '../scenes';
|
||||
|
||||
export interface LayoutProps {
|
||||
tagName?: keyof HTMLElementTagNameMap;
|
||||
mode?: LayoutMode;
|
||||
|
||||
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;
|
||||
basis?: FlexBasis;
|
||||
grow?: number;
|
||||
shrink?: number;
|
||||
wrap?: FlexWrap;
|
||||
|
||||
justifyContent?: FlexJustify;
|
||||
alignItems?: FlexAlign;
|
||||
rowGap?: Length;
|
||||
columnGap?: Length;
|
||||
gap?: Length;
|
||||
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
fontStyle?: string;
|
||||
fontWeight?: number;
|
||||
lineHeight?: number;
|
||||
letterSpacing?: number;
|
||||
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)
|
||||
public declare readonly marginBottom: Signal<number, this>;
|
||||
@property(0)
|
||||
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>;
|
||||
@property(0)
|
||||
public declare readonly paddingBottom: Signal<number, this>;
|
||||
@property(0)
|
||||
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(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>;
|
||||
@property(null)
|
||||
public declare readonly fontSize: Signal<number | null, this>;
|
||||
@property(null)
|
||||
public declare readonly fontStyle: Signal<string | null, this>;
|
||||
@property(null)
|
||||
public declare readonly fontWeight: Signal<number | null, this>;
|
||||
@property(null)
|
||||
public declare readonly lineHeight: Signal<number | null, this>;
|
||||
@property(null)
|
||||
public declare readonly letterSpacing: Signal<number | null, this>;
|
||||
@property(null)
|
||||
public declare readonly textWrap: Signal<boolean | null, this>;
|
||||
|
||||
public readonly element: HTMLElement;
|
||||
public readonly styles: CSSStyleDeclaration;
|
||||
private sizeLockCounter = createSignal(0);
|
||||
|
||||
public constructor({tagName = 'div', ...props}: LayoutProps) {
|
||||
const frame = document.querySelector<HTMLIFrameElement>(
|
||||
`#${TwoDView.frameID}`,
|
||||
);
|
||||
|
||||
this.element = (frame?.contentDocument ?? document).createElement(tagName);
|
||||
this.styles = getComputedStyle(this.element);
|
||||
|
||||
this.element.style.display = 'flex';
|
||||
this.element.style.boxSizing = 'border-box';
|
||||
|
||||
initialize(this, {defaults: props});
|
||||
}
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
public lockSize() {
|
||||
this.sizeLockCounter(this.sizeLockCounter() + 1);
|
||||
}
|
||||
|
||||
public releaseSize() {
|
||||
this.sizeLockCounter(this.sizeLockCounter() - 1);
|
||||
}
|
||||
|
||||
public getComputedLayout(): Rect {
|
||||
return new Rect(this.element.getBoundingClientRect());
|
||||
}
|
||||
|
||||
public setWidth(width: Length): this {
|
||||
this.element.style.width = this.parseLength(width);
|
||||
return this;
|
||||
}
|
||||
|
||||
public setHeight(height: Length): this {
|
||||
this.element.style.height = this.parseLength(height);
|
||||
return this;
|
||||
}
|
||||
|
||||
@computed()
|
||||
public applyFlex() {
|
||||
const mode = this.mode();
|
||||
this.element.style.position =
|
||||
mode === 'disabled' || mode === 'root' ? 'absolute' : 'relative';
|
||||
|
||||
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.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());
|
||||
|
||||
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.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';
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './Gradient';
|
||||
export * from './Layout';
|
||||
export * from './Pattern';
|
||||
export * from './types';
|
||||
|
||||
@@ -26,7 +26,7 @@ export class TwoDScene extends GeneratorScene<TwoDView> {
|
||||
}
|
||||
|
||||
public override reset(previousScene?: Scene): Promise<void> {
|
||||
this.view.removeChildren();
|
||||
this.view.reset();
|
||||
return super.reset(previousScene);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,10 @@
|
||||
import {Node} from '../components';
|
||||
import {Layout} from '../components/Layout';
|
||||
|
||||
export class TwoDView extends Node {
|
||||
export class TwoDView extends Layout {
|
||||
public static frameID = 'motion-canvas-2d-frame';
|
||||
public static document: Document;
|
||||
|
||||
public constructor() {
|
||||
super({
|
||||
// TODO Sync with the project size
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
composite: true,
|
||||
layout: {
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: 48,
|
||||
lineHeight: 64,
|
||||
textWrap: false,
|
||||
fontStyle: 'normal',
|
||||
},
|
||||
});
|
||||
|
||||
static {
|
||||
let frame = document.querySelector<HTMLIFrameElement>(
|
||||
`#${TwoDView.frameID}`,
|
||||
);
|
||||
@@ -33,10 +20,29 @@ export class TwoDView extends Node {
|
||||
|
||||
document.body.prepend(frame);
|
||||
}
|
||||
this.document = frame.contentDocument ?? document;
|
||||
}
|
||||
|
||||
if (frame.contentDocument) {
|
||||
frame.contentDocument.body.append(this.layout.element);
|
||||
}
|
||||
public constructor() {
|
||||
super({
|
||||
// TODO Sync with the project size
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
composite: true,
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: 48,
|
||||
lineHeight: 64,
|
||||
textWrap: false,
|
||||
fontStyle: 'normal',
|
||||
});
|
||||
|
||||
TwoDView.document.body.append(this.element);
|
||||
this.applyFlex();
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.removeChildren();
|
||||
this.element.innerText = '';
|
||||
}
|
||||
|
||||
public override render(context: CanvasRenderingContext2D) {
|
||||
@@ -75,4 +81,12 @@ export class TwoDView extends Node {
|
||||
protected override transformContext() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
protected override requestLayoutUpdate() {
|
||||
this.updateLayout();
|
||||
}
|
||||
|
||||
public override view(): TwoDView | null {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export type SerializedVector2 = {
|
||||
|
||||
export type PossibleVector2 =
|
||||
| SerializedVector2
|
||||
| number
|
||||
| [number, number]
|
||||
| Size
|
||||
| Rect;
|
||||
@@ -56,15 +57,15 @@ export class Vector2 {
|
||||
|
||||
public constructor();
|
||||
public constructor(from: PossibleVector2);
|
||||
public constructor(x: number, y?: number);
|
||||
public constructor(one?: PossibleVector2 | number, two = 0) {
|
||||
public constructor(x: number, y: number);
|
||||
public constructor(one?: PossibleVector2 | number, two?: number) {
|
||||
if (one === undefined || one === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof one === 'number') {
|
||||
this.x = one;
|
||||
this.y = two;
|
||||
this.y = two ?? one;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user