feat: unify core types (#71)

Unifies all types currently available in core.
Each of them is now represented by a separate class.
This commit is contained in:
Jacob
2022-09-30 14:46:37 +02:00
committed by GitHub
parent 85c7dcdb4f
commit 9c5853d8bc
42 changed files with 754 additions and 460 deletions

View File

@@ -11,6 +11,8 @@
"core:test": "npm run test -w packages/core",
"2d:build": "npm run build -w packages/2d",
"2d:watch": "npm run watch -w packages/2d",
"legacy:build": "npm run build -w packages/legacy",
"legacy:watch": "npm run watch -w packages/legacy",
"ui:build": "npm run build -w packages/ui",
"ui:dev": "npm run dev -w packages/ui",
"template:serve": "npm run serve -w packages/template",

View File

@@ -7,13 +7,10 @@ import {
} from '../decorators';
import {
Vector2,
transformPoint,
transformAngle,
Rect,
Size,
rect,
transformScalar,
transformVector,
} from '@motion-canvas/core/lib/types';
import {
createSignal,
@@ -23,8 +20,6 @@ import {
SignalValue,
} from '@motion-canvas/core/lib/utils';
import {
vector2dLerp,
sizeLerp,
InterpolationFunction,
TimingFunction,
tween,
@@ -190,8 +185,8 @@ export class Node<TProps extends NodeProps = NodeProps>
this.layout.releaseSize();
}
@compound(['width', 'height'])
@property(undefined, sizeLerp)
@compound(['width', 'height'], Size)
@property(undefined, Size.lerp)
public declare readonly size: Property<
{width: Length; height: Length},
Size,
@@ -251,8 +246,8 @@ export class Node<TProps extends NodeProps = NodeProps>
@property(0)
public declare readonly offsetY: Signal<number, this>;
@compound({x: 'offsetX', y: 'offsetY'})
@property(undefined, vector2dLerp)
@compound({x: 'offsetX', y: 'offsetY'}, Vector2)
@property(undefined, Vector2.lerp)
public declare readonly offset: Signal<Vector2, this>;
@property(1)
@@ -261,8 +256,8 @@ export class Node<TProps extends NodeProps = NodeProps>
@property(1)
public declare readonly scaleY: Signal<number, this>;
@compound({x: 'scaleX', y: 'scaleY'})
@property(undefined, vector2dLerp)
@compound({x: 'scaleX', y: 'scaleY'}, Vector2)
@property(undefined, Vector2.lerp)
public declare readonly scale: Signal<Vector2, this>;
@property(false)
@@ -318,8 +313,8 @@ export class Node<TProps extends NodeProps = NodeProps>
@property(0)
public declare readonly shadowOffsetY: Signal<number, this>;
@compound({x: 'shadowOffsetX', y: 'shadowOffsetY'})
@property(undefined, vector2dLerp)
@compound({x: 'shadowOffsetX', y: 'shadowOffsetY'}, Vector2)
@property(undefined, Vector2.lerp)
public declare readonly shadowOffset: Signal<Vector2, this>;
@computed()
@@ -386,23 +381,23 @@ export class Node<TProps extends NodeProps = NodeProps>
return filters;
}
@compound(['x', 'y'])
@property(undefined, vector2dLerp)
@compound(['x', 'y'], Vector2)
@property(undefined, Vector2.lerp)
public declare readonly position: Signal<Vector2, this>;
@property(undefined, vector2dLerp)
@property(undefined, Vector2.lerp)
public declare readonly absolutePosition: Signal<Vector2, this>;
protected getAbsolutePosition() {
protected getAbsolutePosition(): Vector2 {
const matrix = this.localToWorld();
return {x: matrix.m41, y: matrix.m42};
return new Vector2(matrix.m41, matrix.m42);
}
protected setAbsolutePosition(value: SignalValue<Vector2>) {
if (isReactive(value)) {
this.position(() => transformPoint(value(), this.worldToParent()));
this.position(() => value().transformAsPoint(this.worldToParent()));
} else {
this.position(transformPoint(value, this.worldToParent()));
this.position(value.transformAsPoint(this.worldToParent()));
}
}
@@ -552,20 +547,17 @@ export class Node<TProps extends NodeProps = NodeProps>
protected computedPosition(): Vector2 {
const mode = this.mode();
if (mode !== 'enabled') {
return {
x: this.customX(),
y: this.customY(),
};
return new Vector2(this.customX(), this.customY());
}
this.requestLayoutUpdate();
this.requestFontUpdate();
const rect = this.layout.getComputedLayout();
const position = {
x: rect.x + (rect.width / 2) * this.offsetX(),
y: rect.y + (rect.height / 2) * this.offsetY(),
};
const position = new Vector2(
rect.x + (rect.width / 2) * this.offsetX(),
rect.y + (rect.height / 2) * this.offsetY(),
);
const parent = this.parent();
if (parent) {
@@ -583,10 +575,7 @@ export class Node<TProps extends NodeProps = NodeProps>
this.requestFontUpdate();
const rect = this.layout.getComputedLayout();
return {
width: rect.width,
height: rect.height,
};
return new Size(rect);
}
/**
@@ -737,13 +726,8 @@ export class Node<TProps extends NodeProps = NodeProps>
* The returned rectangle should be in local space.
*/
protected getCacheRect(): Rect {
const {width, height} = this.computedSize();
return {
x: width / -2,
y: height / -2,
width,
height,
};
const size = this.computedSize();
return new Rect(size.vector.scale(-0.5), size);
}
/**
@@ -755,13 +739,10 @@ export class Node<TProps extends NodeProps = NodeProps>
*/
protected getFullCacheRect() {
const matrix = this.compositeToLocal();
const shadowOffset = transformVector(this.shadowOffset(), matrix);
const shadowOffset = this.shadowOffset().transform(matrix);
const shadowBlur = transformScalar(this.shadowBlur(), matrix);
const result = rect.expand(
this.getCacheRect(),
this.blur() * 2 + shadowBlur,
);
const result = this.getCacheRect().expand(this.blur() * 2 + shadowBlur);
if (shadowOffset.x < 0) {
result.x += shadowOffset.x;
@@ -789,31 +770,24 @@ export class Node<TProps extends NodeProps = NodeProps>
const cache = this.getCacheRect();
const children = this.children();
if (!this.overflow() || children.length === 0) {
return cache;
return cache.pixelPerfect;
}
const points: Vector2[] = rect.corners(cache);
const points: Vector2[] = cache.corners;
for (const child of children) {
const childCache = child.fullCacheRect();
const childMatrix = child.localToParent();
points.push(
...rect.corners(childCache).map(r => transformPoint(r, childMatrix)),
...childCache.corners.map(r => r.transformAsPoint(childMatrix)),
);
}
const result = rect.fromPoints(...points);
return {
x: Math.floor(result.x),
y: Math.floor(result.y),
width: Math.ceil(result.width + 1),
height: Math.ceil(result.height + 1),
};
return Rect.fromPoints(...points).pixelPerfect;
}
@computed()
protected fullCacheRect(): Rect {
return rect.fromRects(this.cacheRect(), this.getFullCacheRect());
return Rect.fromRects(this.cacheRect(), this.getFullCacheRect());
}
/**
@@ -836,7 +810,7 @@ export class Node<TProps extends NodeProps = NodeProps>
}
if (this.hasShadow()) {
const matrix = this.compositeToWorld();
const offset = transformVector(this.shadowOffset(), matrix);
const offset = this.shadowOffset().transform(matrix);
const blur = transformScalar(this.shadowBlur(), matrix);
context.shadowColor = this.shadowColor();

View File

@@ -2,7 +2,7 @@ import {Node, NodeProps} from './Node';
import {Gradient, Pattern} from '../partials';
import {property} from '../decorators';
import {Signal} from '@motion-canvas/core/lib/utils';
import {Rect, rect} from '@motion-canvas/core/lib/types';
import {Rect} from '@motion-canvas/core/lib/types';
export type CanvasStyle = null | string | Gradient | Pattern;
@@ -81,7 +81,7 @@ export abstract class Shape<T extends ShapeProps = ShapeProps> extends Node<T> {
}
protected override getCacheRect(): Rect {
return rect.expand(super.getCacheRect(), this.lineWidth() / 2);
return super.getCacheRect().expand(this.lineWidth() / 2);
}
protected getPath(): Path2D {

View File

@@ -2,7 +2,7 @@ import {property} from '../decorators';
import {Signal} from '@motion-canvas/core/lib/utils';
import {textLerp} from '@motion-canvas/core/lib/tweening';
import {Shape, ShapeProps} from './Shape';
import {rect, Rect} from '@motion-canvas/core/lib/types';
import {Rect} from '@motion-canvas/core/lib/types';
export interface TextProps extends ShapeProps {
children?: string;
@@ -42,7 +42,7 @@ export class Text extends Shape<TextProps> {
const {width, height} = this.computedSize();
const range = document.createRange();
let line = '';
const lineRect = rect();
const lineRect = new Rect();
for (const childNode of this.layout.element.childNodes) {
if (!childNode.textContent) {
continue;

View File

@@ -40,9 +40,12 @@ 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)
@@ -51,9 +54,10 @@ export function compound(
target.constructor.prototype[`get${capitalize(key.toString())}`] =
function () {
return Object.fromEntries(
const object = Object.fromEntries(
entries.map(([key, property]) => [key, this[property]()]),
);
return klass ? new klass(object) : object;
};
target.constructor.prototype[`set${capitalize(key.toString())}`] =

View File

@@ -1,7 +1,6 @@
import {compound, computed, initialize, property} from '../decorators';
import {Vector2} from '@motion-canvas/core/lib/types';
import {Signal} from '@motion-canvas/core/lib/utils';
import {vector2dLerp} from '@motion-canvas/core/lib/tweening';
export type GradientType = 'linear' | 'conic' | 'radial';
@@ -32,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'})
@property(undefined, vector2dLerp)
@compound({x: 'fromX', y: 'fromY'}, Vector2)
@property(undefined, Vector2.lerp)
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'})
@property(undefined, vector2dLerp)
@compound({x: 'toX', y: 'toY'}, Vector2)
@property(undefined, Vector2.lerp)
public declare readonly to: Signal<Vector2, this>;
@property(0)

View File

@@ -114,13 +114,7 @@ export class Layout {
}
public getComputedLayout(): Rect {
const rect = this.element.getBoundingClientRect();
return {
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y,
};
return new Rect(this.element.getBoundingClientRect());
}
public setWidth(width: Length): this {

View File

@@ -5,8 +5,8 @@ import {Size, CanvasColorSpace, CanvasOutputMimeType} from './types';
import {AudioManager} from './media';
import {ifHot} from './utils';
export const ProjectSize = {
FullHD: {width: 1920, height: 1080},
export const ProjectSize: Record<string, Size> = {
FullHD: new Size(1920, 1080),
};
export interface ProjectConfig {
@@ -116,10 +116,7 @@ export class Project {
}
public getSize(): Size {
return {
width: this.width,
height: this.height,
};
return new Size(this.width, this.height);
}
public readonly name: string;

View File

@@ -1,9 +1,9 @@
import {Size} from '../types';
import {SerializedSize} from '../types';
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
export type ImageDataSource = CanvasImageSource & Size;
export type ImageDataSource = CanvasImageSource & SerializedSize;
export function loadImage(source: string): Promise<HTMLImageElement> {
const image = new Image();

View File

@@ -1,4 +1,4 @@
import {Rect, Vector2} from '../types';
import {SerializedRect, SerializedVector2} from '../types';
/**
* Represents an element to inspect.
@@ -22,22 +22,22 @@ export interface InspectedSize {
/**
* Bounding box of the element (with padding).
*/
rect?: Rect;
rect?: SerializedRect;
/**
* Bounding box of the content of this element (without padding).
*/
contentRect?: Rect;
contentRect?: SerializedRect;
/**
* Bounding box of the element (with margin).
*/
marginRect?: Rect;
marginRect?: SerializedRect;
/**
* The absolute position of the object's origin.
*/
position?: Vector2;
position?: SerializedVector2;
}
/**

View File

@@ -1,13 +1,14 @@
import {Direction, originPosition} from '../types';
import {easeInOutCubic, tween, deepLerp} from '../tweening';
import {Direction, Vector2} from '../types';
import {easeInOutCubic, tween} from '../tweening';
import {useScene} from '../utils';
import {useTransition} from './';
import {useTransition} from './useTransition';
export function slideTransition(direction: Direction = Direction.Top) {
const size = useScene().getSize();
const position = originPosition(direction, size.width, size.height);
let ppos = {x: 0, y: 0};
let cpos = {x: 0, y: 0};
const position = size.getOriginOffset(direction).scale(2);
const inverse = position.scale(-1);
let ppos = new Vector2();
let cpos = new Vector2();
const endTransition = useTransition(
ctx => ctx.translate(cpos.x, cpos.y),
ctx => ctx.translate(ppos.x, ppos.y),
@@ -15,13 +16,8 @@ export function slideTransition(direction: Direction = Direction.Top) {
return tween(
0.6,
value => {
ppos = deepLerp(
{x: 0, y: 0},
{x: -position.x, y: -position.y},
easeInOutCubic(value),
);
cpos = deepLerp(position, {x: 0, y: 0}, easeInOutCubic(value));
ppos = Vector2.lerp(Vector2.zero, inverse, easeInOutCubic(value));
cpos = Vector2.lerp(position, Vector2.zero, easeInOutCubic(value));
},
endTransition,
);

View File

@@ -1,5 +1,5 @@
import {deepLerp, colorLerp, vector2dLerp} from './interpolationFunctions';
import type {Vector2} from '../types';
import {deepLerp, colorLerp} from './interpolationFunctions';
import {Vector2} from '../types';
describe('deepLerp', () => {
test('falls back to primitive tween for numbers', () => {
@@ -90,13 +90,13 @@ describe('deepLerp', () => {
expect(deepLerp({}, {foo: 5}, 1)).toEqual({foo: 5});
});
test('replaces vector2dLerp', () => {
test('invokes native interpolation function', () => {
const args: [Vector2, Vector2, number] = [
{x: 50, y: 65},
{x: 10, y: 100},
new Vector2(50, 65),
new Vector2(10, 100),
1 / 2,
];
expect(deepLerp(...args)).toEqual(vector2dLerp(...args));
expect(deepLerp(...args)).toEqual(Vector2.lerp(...args));
});
});

View File

@@ -91,6 +91,10 @@ export function deepLerp(from: any, to: any, value: number): any {
return textLerp(from, to, value);
}
if ('lerp' in from) {
return from.lerp(from, to, value);
}
if (from && to && typeof from === 'object' && typeof to === 'object') {
if (Array.isArray(from) && Array.isArray(to)) {
if (from.length === to.length) {
@@ -155,85 +159,6 @@ export function colorLerp(
return range(value).toString();
}
export function vector2dLerp(from: Vector2, to: Vector2, value: number) {
return {
x: map(from.x, to.x, value),
y: map(from.y, to.y, value),
};
}
export function sizeLerp(from: Size, to: Size, value: number) {
return {
width: map(from.width, to.width, value),
height: map(from.height, to.height, value),
};
}
export function spacingLerp(
from: Spacing,
to: Spacing,
value: number,
): PossibleSpacing {
return [
map(from.top, to.top, value),
map(from.right, to.right, value),
map(from.bottom, to.bottom, value),
map(from.left, to.left, value),
];
}
export function rectArcLerp(
from: Partial<Rect>,
to: Partial<Rect>,
value: number,
reverse?: boolean,
ratio?: number,
) {
ratio ??= calculateRatio(from, to);
let flip = reverse;
if (ratio > 1) {
ratio = 1 / ratio;
} else {
flip = !flip;
}
const normalized = flip ? Math.acos(1 - value) : Math.asin(value);
const radians = map(normalized, map(0, Math.PI / 2, value), ratio);
let xValue = Math.sin(radians);
let yValue = 1 - Math.cos(radians);
if (reverse) {
[xValue, yValue] = [yValue, xValue];
}
return {
x: map(from.x ?? 0, to.x ?? 0, xValue),
y: map(from.y ?? 0, to.y ?? 0, yValue),
width: map(from.width ?? 0, to.width ?? 0, xValue),
height: map(from.height ?? 0, to.height ?? 0, yValue),
};
}
export function calculateRatio(from: Partial<Rect>, to: Partial<Rect>): number {
let numberOfValues = 0;
let ratio = 0;
if (from.x) {
ratio += Math.abs((from.x - to.x) / (from.y - to.y));
numberOfValues++;
}
if (from.width) {
ratio += Math.abs((from.width - to.width) / (from.height - to.height));
numberOfValues++;
}
if (numberOfValues) {
ratio /= numberOfValues;
}
return isNaN(ratio) ? 1 : ratio;
}
export function map(from: number, to: number, value: number) {
return from + (to - from) * value;
}

View File

@@ -1,26 +1,9 @@
import {Vector2} from './Vector';
export function transformPoint(vector: Vector2, matrix: DOMMatrix) {
return {
x: vector.x * matrix.m11 + vector.y * matrix.m21 + matrix.m41,
y: vector.x * matrix.m12 + vector.y * matrix.m22 + matrix.m42,
};
}
export function transformVector(vector: Vector2, matrix: DOMMatrix) {
return {
x: vector.x * matrix.m11 + vector.y * matrix.m21,
y: vector.x * matrix.m12 + vector.y * matrix.m22,
};
}
export function transformAngle(angle: number, matrix: DOMMatrix) {
const radians = (angle / 180) * Math.PI;
const vector = transformVector(
{x: Math.cos(radians), y: Math.sin(radians)},
matrix,
);
return (Math.atan2(vector.y, vector.x) * 180) / Math.PI;
const vector = Vector2.fromRadians(radians).transform(matrix);
return (vector.radians * 180) / Math.PI;
}
export function transformScalar(scalar: number, matrix: DOMMatrix) {

View File

@@ -49,46 +49,3 @@ export function flipOrigin(
return origin;
}
export function originPosition(
origin: Origin | Direction,
width = 1,
height = 1,
): Vector2 {
const position: Vector2 = {x: 0, y: 0};
if (origin === Origin.Middle) {
return position;
}
if (origin & Direction.Left) {
position.x = -width;
} else if (origin & Direction.Right) {
position.x = width;
}
if (origin & Direction.Top) {
position.y = -height;
} else if (origin & Direction.Bottom) {
position.y = height;
}
return position;
}
export function getOriginOffset(size: Size, origin: Origin): Vector2 {
return originPosition(origin, size.width / 2, size.height / 2);
}
export function getOriginDelta(size: Size, from: Origin, to: Origin) {
const fromOffset = getOriginOffset(size, from);
if (to === Origin.Middle) {
return {x: -fromOffset.x, y: -fromOffset.y};
}
const toOffset = getOriginOffset(size, to);
return {
x: toOffset.x - fromOffset.x,
y: toOffset.y - fromOffset.y,
};
}

View File

@@ -1,119 +1,193 @@
import {Vector2} from './Vector';
import {transformPoint, transformVector} from './Matrix';
import {Size} from './Size';
import {map} from '../tweening';
export interface Rect {
export type SerializedRect = {
x: number;
y: number;
width: number;
height: number;
}
};
export function rect(x = 0, y = 0, width = 0, height = 0): Rect {
return {x, y, width, height};
}
export type PossibleRect =
| SerializedRect
| [number, number, number, number]
| Size
| Vector2;
export interface rect {
fromPoints: (...points: Vector2[]) => Rect;
fromRects: (...rects: Rect[]) => Rect;
topLeft: (rect: Rect) => Vector2;
topRight: (rect: Rect) => Vector2;
bottomLeft: (rect: Rect) => Vector2;
bottomRight: (rect: Rect) => Vector2;
corners: (rect: Rect) => Vector2[];
transform: (rect: Rect, matrix: DOMMatrix) => Rect;
expand: (rect: Rect, amount: number) => Rect;
}
export class Rect {
public x = 0;
public y = 0;
public width = 0;
public height = 0;
rect.fromPoints = (...points: Vector2[]) => {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const point of points) {
if (point.x > maxX) {
maxX = point.x;
}
if (point.x < minX) {
minX = point.x;
}
if (point.y > maxY) {
maxY = point.y;
}
if (point.y < minY) {
minY = point.y;
}
public static lerp(from: Rect, to: Rect, value: number): Rect {
return new Rect(
map(from.x, to.x, value),
map(from.y, to.y, value),
map(from.width, to.width, value),
map(from.height, to.height, value),
);
}
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
};
public static fromPoints(...points: Vector2[]): Rect {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
rect.fromRects = (...rects: Rect[]) => {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const point of points) {
if (point.x > maxX) {
maxX = point.x;
}
if (point.x < minX) {
minX = point.x;
}
if (point.y > maxY) {
maxY = point.y;
}
if (point.y < minY) {
minY = point.y;
}
}
for (const r of rects) {
const right = r.x + r.width;
if (right > maxX) {
maxX = right;
}
if (r.x < minX) {
minX = r.x;
}
const bottom = r.y + r.height;
if (bottom > maxY) {
maxY = bottom;
}
if (r.y < minY) {
minY = r.y;
}
return new Rect(minX, minY, maxX - minX, maxY - minY);
}
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
};
public static fromRects(...rects: Rect[]): Rect {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
rect.topLeft = (rect: Rect) => ({x: rect.x, y: rect.y});
rect.topRight = (rect: Rect) => ({x: rect.x + rect.width, y: rect.y});
rect.bottomLeft = (rect: Rect) => ({x: rect.x, y: rect.y + rect.height});
rect.bottomRight = (rect: Rect) => ({
x: rect.x + rect.width,
y: rect.y + rect.height,
});
rect.corners = (value: Rect) => [
rect.topLeft(value),
rect.topRight(value),
rect.bottomRight(value),
rect.bottomLeft(value),
];
for (const r of rects) {
const right = r.x + r.width;
if (right > maxX) {
maxX = right;
}
if (r.x < minX) {
minX = r.x;
}
const bottom = r.y + r.height;
if (bottom > maxY) {
maxY = bottom;
}
if (r.y < minY) {
minY = r.y;
}
}
rect.transform = (rect: Rect, matrix: DOMMatrix) => {
const position = transformPoint(rect, matrix);
const size = transformVector({x: rect.width, y: rect.height}, matrix);
return new Rect(minX, minY, maxX - minX, maxY - minY);
}
return {
...position,
width: size.x,
height: size.y,
};
};
public get position() {
return new Vector2(this.x, this.y);
}
rect.expand = (rect: Rect, amount: number) => {
return {
x: rect.x - amount,
y: rect.y - amount,
width: rect.width + amount * 2,
height: rect.height + amount * 2,
};
};
public get size() {
return new Size(this.width, this.height);
}
public get topLeft() {
return this.position;
}
public get topRight() {
return new Vector2(this.x + this.width, this.y);
}
public get bottomLeft() {
return new Vector2(this.x, this.y + this.height);
}
public get bottomRight() {
return new Vector2(this.x + this.width, this.y + this.height);
}
public get corners() {
return [this.topLeft, this.topRight, this.bottomRight, this.bottomLeft];
}
public get pixelPerfect() {
return new Rect(
Math.floor(this.x),
Math.floor(this.y),
Math.ceil(this.width + 1),
Math.ceil(this.height + 1),
);
}
public constructor();
public constructor(from: PossibleRect);
public constructor(position: Vector2, size: Size);
public constructor(from: Vector2, to: Vector2);
public constructor(x: number, y?: number, width?: number, height?: number);
public constructor(
one?: PossibleRect | number,
two: Vector2 | Size | number = 0,
three = 0,
four = 0,
) {
if (one === undefined || one === null) {
return;
}
if (typeof one === 'number') {
this.x = one;
this.y = <number>two;
this.width = three;
this.height = four;
return;
}
if (one instanceof Vector2) {
this.x = one.x;
this.y = one.y;
if (two instanceof Size) {
this.width = two.width;
this.height = two.height;
} else if (two instanceof Vector2) {
this.width = two.x - one.x;
this.height = two.y - one.y;
}
return;
}
if (one instanceof Size) {
this.width = one.width;
this.height = one.height;
return;
}
if (Array.isArray(one)) {
this.x = one[0];
this.y = one[1];
this.width = one[2];
this.height = one[3];
return;
}
this.x = one.x;
this.y = one.y;
this.width = one.width;
this.height = one.height;
}
public transform(matrix: DOMMatrix): Rect {
return new Rect(
this.position.transformAsPoint(matrix),
this.size.transform(matrix),
);
}
public expand(amount: number) {
return new Rect(
this.x - amount,
this.y - amount,
this.width + amount * 2,
this.height + amount * 2,
);
}
}

View File

@@ -1,4 +1,80 @@
export interface Size {
import {Vector2} from './Vector';
import {Rect} from './Rect';
import {Direction, Origin} from './Origin';
import {map} from '../tweening';
export type SerializedSize = {
width: number;
height: number;
};
export type PossibleSize = SerializedSize | [number, number] | Vector2 | Rect;
export class Size {
public width = 0;
public height = 0;
public static lerp(from: Size, to: Size, value: number) {
return new Size(
map(from.width, to.width, value),
map(from.height, to.height, value),
);
}
public get vector() {
return new Vector2(this);
}
public get flip() {
return new Size(this.height, this.width);
}
public constructor();
public constructor(from: PossibleSize);
public constructor(width: number, height?: number);
public constructor(one?: PossibleSize | number, two = 0) {
if (one === undefined || one === null) {
return;
}
if (typeof one === 'number') {
this.width = one;
this.height = two;
return;
}
if (one instanceof Vector2) {
this.width = one.x;
this.height = one.y;
return;
}
if (Array.isArray(one)) {
this.width = one[0];
this.height = one[1];
return;
}
this.width = one.width;
this.height = one.height;
}
public getOriginOffset(origin: Origin | Direction) {
const offset = Vector2.fromOrigin(origin);
offset.x *= this.width / 2;
offset.y *= this.height / 2;
return offset;
}
public scale(value: number) {
return new Size(this.width * value, this.height * value);
}
public transform(matrix: DOMMatrix) {
return new Size(
this.width * matrix.m11 + this.height * matrix.m21,
this.width * matrix.m12 + this.height * matrix.m22,
);
}
}

View File

@@ -1,27 +1,37 @@
import type {Size} from './Size';
import type {Rect} from './Rect';
import type {Vector2} from './Vector';
import {map} from '../tweening';
interface ISpacing {
export type SerializedSpacing = {
top: number;
right: number;
bottom: number;
left: number;
}
};
export type PossibleSpacing =
| ISpacing
| SerializedSpacing
| number
| [number, number]
| [number, number, number]
| [number, number, number, number];
export class Spacing implements ISpacing {
export class Spacing {
public top = 0;
public right = 0;
public bottom = 0;
public left = 0;
public static lerp(from: Spacing, to: Spacing, value: number): Spacing {
return new Spacing(
map(from.top, to.top, value),
map(from.right, to.right, value),
map(from.bottom, to.bottom, value),
map(from.left, to.left, value),
);
}
public get x(): number {
return this.left + this.right;
}
@@ -30,76 +40,40 @@ export class Spacing implements ISpacing {
return this.top + this.bottom;
}
public constructor(value?: PossibleSpacing) {
if (value !== undefined) {
this.set(value);
}
}
public set(value: PossibleSpacing): this {
if (Array.isArray(value)) {
switch (value.length) {
case 2:
this.top = this.bottom = value[0];
this.right = this.left = value[1];
break;
case 3:
this.top = value[0];
this.right = this.left = value[1];
this.bottom = value[2];
break;
case 4:
this.top = value[0];
this.right = value[1];
this.bottom = value[2];
this.left = value[3];
break;
}
} else if (typeof value === 'object') {
this.top = value.top ?? 0;
this.right = value.right ?? 0;
this.bottom = value.bottom ?? 0;
this.left = value.left ?? 0;
return this;
} else {
this.top = this.right = this.bottom = this.left = value;
public constructor();
public constructor(from: PossibleSpacing);
public constructor(all: number);
public constructor(vertical: number, horizontal: number);
public constructor(top: number, horizontal: number, bottom: number);
public constructor(top: number, right: number, bottom: number, left: number);
public constructor(
one: PossibleSpacing = 0,
two?: number,
three?: number,
four?: number,
) {
if (one === undefined || one === null) {
return;
}
return this;
}
public expand<T extends Size | Rect>(value: T): T {
const result = {...value};
result.width += this.x;
result.height += this.y;
if ('x' in result) {
result.x -= this.left;
result.y -= this.top;
if (Array.isArray(one)) {
four = one[3];
three = one[2];
two = one[1];
one = one[0];
}
return result;
}
public shrink<T extends Size | Rect>(value: T): T {
const result = {...value};
result.width -= this.x;
result.height -= this.y;
if ('x' in result) {
result.x += this.left;
result.y += this.top;
if (typeof one === 'number') {
this.top = one;
this.right = two !== undefined ? two : one;
this.bottom = three !== undefined ? three : one;
this.left = four !== undefined ? four : two !== undefined ? two : one;
return;
}
return result;
}
public scale(scale: Vector2): Spacing {
return new Spacing([
this.top * scale.y,
this.right * scale.x,
this.bottom * scale.y,
this.left * scale.x,
]);
this.top = one.top;
this.right = one.right;
this.bottom = one.bottom;
this.left = one.left;
}
}

View File

@@ -1,10 +1,104 @@
export interface Vector2 {
x: number;
y: number;
}
import {Size} from './Size';
import {Rect} from './Rect';
import {map} from '../tweening';
import {Direction, Origin} from './Origin';
export interface Vector3 {
export type SerializedVector2 = {
x: number;
y: number;
z: number;
};
export type PossibleVector2 =
| SerializedVector2
| [number, number]
| Size
| Rect;
export class Vector2 {
public x = 0;
public y = 0;
public static readonly zero = new Vector2();
public static lerp(from: Vector2, to: Vector2, value: number) {
return new Vector2(map(from.x, to.x, value), map(from.y, to.y, value));
}
public static fromOrigin(origin: Origin | Direction) {
const position = new Vector2();
if (origin === Origin.Middle) {
return position;
}
if (origin & Direction.Left) {
position.x = -1;
} else if (origin & Direction.Right) {
position.x = 1;
}
if (origin & Direction.Top) {
position.y = -1;
} else if (origin & Direction.Bottom) {
position.y = 1;
}
return position;
}
public static fromRadians(radians: number) {
return new Vector2(Math.cos(radians), Math.sin(radians));
}
public get radians() {
return Math.atan2(this.y, this.x);
}
public constructor();
public constructor(from: PossibleVector2);
public constructor(x: number, y?: number);
public constructor(one?: PossibleVector2 | number, two = 0) {
if (one === undefined || one === null) {
return;
}
if (typeof one === 'number') {
this.x = one;
this.y = two;
return;
}
if (one instanceof Size) {
this.x = one.width;
this.y = one.height;
return;
}
if (Array.isArray(one)) {
this.x = one[0];
this.y = one[0];
return;
}
this.x = one.x;
this.y = one.y;
}
public scale(value: number) {
return new Vector2(this.x * value, this.y * value);
}
public transformAsPoint(matrix: DOMMatrix) {
return new Vector2(
this.x * matrix.m11 + this.y * matrix.m21 + matrix.m41,
this.x * matrix.m12 + this.y * matrix.m22 + matrix.m42,
);
}
public transform(matrix: DOMMatrix) {
return new Vector2(
this.x * matrix.m11 + this.y * matrix.m21,
this.x * matrix.m12 + this.y * matrix.m22,
);
}
}

View File

@@ -4,5 +4,6 @@
* @module
*/
export * from './show';
export * from './interpolationFunctions';
export * from './surfaceFrom';
export * from './surfaceTransition';

View File

@@ -0,0 +1,81 @@
import {PossibleSpacing, Rect, Size, Spacing, Vector2} from '../types';
import {map} from '@motion-canvas/core/lib/tweening';
export function vector2dLerp(from: Vector2, to: Vector2, value: number) {
return {
x: map(from.x, to.x, value),
y: map(from.y, to.y, value),
};
}
export function sizeLerp(from: Size, to: Size, value: number) {
return {
width: map(from.width, to.width, value),
height: map(from.height, to.height, value),
};
}
export function spacingLerp(
from: Spacing,
to: Spacing,
value: number,
): PossibleSpacing {
return [
map(from.top, to.top, value),
map(from.right, to.right, value),
map(from.bottom, to.bottom, value),
map(from.left, to.left, value),
];
}
export function rectArcLerp(
from: Partial<Rect>,
to: Partial<Rect>,
value: number,
reverse?: boolean,
ratio?: number,
) {
ratio ??= calculateRatio(from, to);
let flip = reverse;
if (ratio > 1) {
ratio = 1 / ratio;
} else {
flip = !flip;
}
const normalized = flip ? Math.acos(1 - value) : Math.asin(value);
const radians = map(normalized, map(0, Math.PI / 2, value), ratio);
let xValue = Math.sin(radians);
let yValue = 1 - Math.cos(radians);
if (reverse) {
[xValue, yValue] = [yValue, xValue];
}
return {
x: map(from.x ?? 0, to.x ?? 0, xValue),
y: map(from.y ?? 0, to.y ?? 0, yValue),
width: map(from.width ?? 0, to.width ?? 0, xValue),
height: map(from.height ?? 0, to.height ?? 0, yValue),
};
}
export function calculateRatio(from: Partial<Rect>, to: Partial<Rect>): number {
let numberOfValues = 0;
let ratio = 0;
if (from.x) {
ratio += Math.abs((from.x - to.x) / (from.y - to.y));
numberOfValues++;
}
if (from.width) {
ratio += Math.abs((from.width - to.width) / (from.height - to.height));
numberOfValues++;
}
if (numberOfValues) {
ratio /= numberOfValues;
}
return isNaN(ratio) ? 1 : ratio;
}

View File

@@ -1,6 +1,7 @@
import type {Node} from 'konva/lib/Node';
import {Surface} from '../components';
import {Origin, originPosition} from '@motion-canvas/core/lib/types';
import {Origin} from '@motion-canvas/core/lib/types';
import {originPosition} from '../types';
import {all} from '@motion-canvas/core/lib/flow';
import {Vector2d} from 'konva/lib/types';
import {

View File

@@ -2,14 +2,13 @@ import type {Vector2d} from 'konva/lib/types';
import type {ThreadGenerator} from '@motion-canvas/core/lib/threading';
import type {Surface, SurfaceMask} from '../components';
import {
calculateRatio,
colorLerp,
easeInOutCubic,
easeInOutQuint,
rectArcLerp,
tween,
} from '@motion-canvas/core/lib/tweening';
import {decorate, threadable} from '@motion-canvas/core/lib/decorators';
import {calculateRatio, rectArcLerp} from './interpolationFunctions';
/**
* Configuration for {@link surfaceFrom}.

View File

@@ -1,15 +1,14 @@
import {Surface} from '../components';
import {
calculateRatio,
clampRemap,
colorLerp,
easeInOutCubic,
easeInOutQuint,
rectArcLerp,
tween,
} from '@motion-canvas/core/lib/tweening';
import {decorate, threadable} from '@motion-canvas/core/lib/decorators';
import {ThreadGenerator} from '@motion-canvas/core/lib/threading';
import {calculateRatio, rectArcLerp} from './interpolationFunctions';
/**
* Configuration for {@link surfaceTransition}.

View File

@@ -1,5 +1,6 @@
import type {Node} from 'konva/lib/Node';
import {getOriginDelta, Origin} from '@motion-canvas/core/lib/types';
import {Origin} from '@motion-canvas/core/lib/types';
import {getOriginDelta} from '../types';
import {useScene} from '@motion-canvas/core/lib/utils';
interface AlignConfig {

View File

@@ -1,4 +1,4 @@
import {Size} from '@motion-canvas/core/lib/types';
import {Size} from '../types';
import {Group} from 'konva/lib/Group';
export class LayeredLayout extends Group {

View File

@@ -1,12 +1,9 @@
import {Text, TextConfig} from 'konva/lib/shapes/Text';
import {GetSet, IRect, Vector2d} from 'konva/lib/types';
import {ShapeGetClientRectConfig} from 'konva/lib/Shape';
import {
Origin,
Size,
Spacing,
getOriginOffset,
} from '@motion-canvas/core/lib/types';
import {Origin} from '@motion-canvas/core/lib/types';
import {getOriginOffset} from '../types';
import {Size, Spacing} from '../types';
import {
Animator,
tween,

View File

@@ -1,12 +1,9 @@
import {Group} from 'konva/lib/Group';
import {GetSet} from 'konva/lib/types';
import {Shape} from 'konva/lib/Shape';
import {
Center,
getOriginOffset,
Origin,
Size,
} from '@motion-canvas/core/lib/types';
import {Center, Origin} from '@motion-canvas/core/lib/types';
import {getOriginOffset} from '../types';
import {Size} from '../types';
import {ContainerConfig} from 'konva/lib/Container';
import {KonvaNode, getset} from '../decorators';
import {Node} from 'konva/lib/Node';

View File

@@ -1,11 +1,7 @@
import {Group} from 'konva/lib/Group';
import {Container, ContainerConfig} from 'konva/lib/Container';
import {
Center,
flipOrigin,
getOriginDelta,
Origin,
} from '@motion-canvas/core/lib/types';
import {Center, flipOrigin, Origin} from '@motion-canvas/core/lib/types';
import {getOriginDelta} from '../types';
import {GetSet, IRect} from 'konva/lib/types';
import {KonvaNode, getset} from '../decorators';
import {Node} from 'konva/lib/Node';

View File

@@ -1,5 +1,6 @@
import {ContainerConfig} from 'konva/lib/Container';
import {getOriginDelta, Origin, Size} from '@motion-canvas/core/lib/types';
import {Origin} from '@motion-canvas/core/lib/types';
import {getOriginDelta, Size} from '../types';
import {CanvasHelper} from '../helpers';
import {easeOutExpo, linear, tween} from '@motion-canvas/core/lib/tweening';
import {GetSet} from 'konva/lib/types';

View File

@@ -1,4 +1,4 @@
import {PossibleSpacing, Size} from '@motion-canvas/core/lib/types';
import {PossibleSpacing, Size} from '../types';
import {Util} from 'konva/lib/Util';
import {Context} from 'konva/lib/Context';
import * as THREE from 'three';

View File

@@ -1,5 +1,5 @@
import type {Context} from 'konva/lib/Context';
import {PossibleSpacing, Spacing} from '@motion-canvas/core/lib/types';
import {PossibleSpacing, Spacing} from '../types';
export const CanvasHelper = {
roundRect<T extends CanvasRenderingContext2D | Context>(

View File

@@ -1,12 +1,7 @@
import type {Style} from '../styles';
import {Node, NodeConfig} from 'konva/lib/Node';
import {
Origin,
PossibleSpacing,
Size,
Spacing,
getOriginDelta,
} from '@motion-canvas/core/lib/types';
import {Origin} from '@motion-canvas/core/lib/types';
import {PossibleSpacing, Size, Spacing, getOriginDelta} from '../types';
import {GetSet, IRect, Vector2d} from 'konva/lib/types';
import {Factory} from 'konva/lib/Factory';
import {Container} from 'konva/lib/Container';

View File

@@ -18,6 +18,7 @@ import {Util} from 'konva/lib/Util';
import {Node} from 'konva/lib/Node';
import {Konva} from 'konva/lib/Global';
import {NODE_ID} from '@motion-canvas/core/lib';
import {Rect, Vector2} from '@motion-canvas/core/lib/types';
Konva.autoDrawEnabled = false;

View File

@@ -0,0 +1,46 @@
import type {Vector2} from './Vector';
import type {Size} from './Size';
import {Direction, Origin} from '@motion-canvas/core/lib/types';
export function originPosition(
origin: Origin | Direction,
width = 1,
height = 1,
): Vector2 {
const position: Vector2 = {x: 0, y: 0};
if (origin === Origin.Middle) {
return position;
}
if (origin & Direction.Left) {
position.x = -width;
} else if (origin & Direction.Right) {
position.x = width;
}
if (origin & Direction.Top) {
position.y = -height;
} else if (origin & Direction.Bottom) {
position.y = height;
}
return position;
}
export function getOriginOffset(size: Size, origin: Origin): Vector2 {
return originPosition(origin, size.width / 2, size.height / 2);
}
export function getOriginDelta(size: Size, from: Origin, to: Origin) {
const fromOffset = getOriginOffset(size, from);
if (to === Origin.Middle) {
return {x: -fromOffset.x, y: -fromOffset.y};
}
const toOffset = getOriginOffset(size, to);
return {
x: toOffset.x - fromOffset.x,
y: toOffset.y - fromOffset.y,
};
}

View File

@@ -0,0 +1,6 @@
export interface Rect {
x: number;
y: number;
width: number;
height: number;
}

View File

@@ -0,0 +1,4 @@
export interface Size {
width: number;
height: number;
}

View File

@@ -0,0 +1,105 @@
import type {Size} from './Size';
import type {Rect} from './Rect';
import type {Vector2} from './Vector';
interface ISpacing {
top: number;
right: number;
bottom: number;
left: number;
}
export type PossibleSpacing =
| ISpacing
| number
| [number, number]
| [number, number, number]
| [number, number, number, number];
export class Spacing implements ISpacing {
public top = 0;
public right = 0;
public bottom = 0;
public left = 0;
public get x(): number {
return this.left + this.right;
}
public get y(): number {
return this.top + this.bottom;
}
public constructor(value?: PossibleSpacing) {
if (value !== undefined) {
this.set(value);
}
}
public set(value: PossibleSpacing): this {
if (Array.isArray(value)) {
switch (value.length) {
case 2:
this.top = this.bottom = value[0];
this.right = this.left = value[1];
break;
case 3:
this.top = value[0];
this.right = this.left = value[1];
this.bottom = value[2];
break;
case 4:
this.top = value[0];
this.right = value[1];
this.bottom = value[2];
this.left = value[3];
break;
}
} else if (typeof value === 'object') {
this.top = value.top ?? 0;
this.right = value.right ?? 0;
this.bottom = value.bottom ?? 0;
this.left = value.left ?? 0;
return this;
} else {
this.top = this.right = this.bottom = this.left = value;
}
return this;
}
public expand<T extends Size | Rect>(value: T): T {
const result = {...value};
result.width += this.x;
result.height += this.y;
if ('x' in result) {
result.x -= this.left;
result.y -= this.top;
}
return result;
}
public shrink<T extends Size | Rect>(value: T): T {
const result = {...value};
result.width -= this.x;
result.height -= this.y;
if ('x' in result) {
result.x += this.left;
result.y += this.top;
}
return result;
}
public scale(scale: Vector2): Spacing {
return new Spacing([
this.top * scale.y,
this.right * scale.x,
this.bottom * scale.y,
this.left * scale.x,
]);
}
}

View File

@@ -0,0 +1,10 @@
export interface Vector2 {
x: number;
y: number;
}
export interface Vector3 {
x: number;
y: number;
z: number;
}

View File

@@ -0,0 +1,5 @@
export * from './Origin';
export * from './Rect';
export * from './Size';
export * from './Spacing';
export * from './Vector';

View File

@@ -1,5 +1,5 @@
import type {Container} from 'konva/lib/Container';
import type {Vector2} from '@motion-canvas/core/lib/types';
import type {Vector2} from '../types';
export function slide(container: Container, offset: Vector2): void;
export function slide(container: Container, x: number, y?: number): void;

View File

@@ -13,7 +13,7 @@
</head>
<body>
<script type="module">
import project from '@motion-canvas/template/dist/projectA';
import project from '@motion-canvas/template/dist/project';
import {editor} from '/src/main.tsx';
editor(project);
</script>