refactor: remove legacy package

BREAKING CHANGE: remove legacy package
This commit is contained in:
aarthificial
2022-11-17 16:54:35 +01:00
committed by Jacob
parent 842079a654
commit 6a84120d94
89 changed files with 67 additions and 4783 deletions

95
package-lock.json generated
View File

@@ -4919,10 +4919,6 @@
"resolved": "packages/docs",
"link": true
},
"node_modules/@motion-canvas/legacy": {
"resolved": "packages/legacy",
"link": true
},
"node_modules/@motion-canvas/template": {
"resolved": "packages/template",
"link": true
@@ -6168,11 +6164,6 @@
"integrity": "sha512-fOwvpvQYStpb/zHMx0Cauwywu9yLDmzWiiQBC7gJyq5tYLUXFZvDG7VK1B7WBxxjBJNKFOZ0zLoOQn8vmATbhw==",
"dev": true
},
"node_modules/@types/prismjs": {
"version": "1.26.0",
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.0.tgz",
"integrity": "sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ=="
},
"node_modules/@types/prop-types": {
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
@@ -6276,14 +6267,6 @@
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
"dev": true
},
"node_modules/@types/three": {
"version": "0.141.0",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.141.0.tgz",
"integrity": "sha512-OJdKDgTPVBUgc+s74DYoy4aLznbFFC38Xm4ElmU1YwGNgR7GGFVvFCX7lpVgOsT6S1zSJtGdajTsOYE8/xY9nA==",
"dependencies": {
"@types/webxr": "*"
}
},
"node_modules/@types/tough-cookie": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
@@ -6295,11 +6278,6 @@
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
},
"node_modules/@types/webxr": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.0.tgz",
"integrity": "sha512-IUMDPSXnYIbEO2IereEFcgcqfDREOgmbGqtrMpVPpACTU6pltYLwHgVkrnYv0XhWEcjio9sYEfIEzgn3c7nDqA=="
},
"node_modules/@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@@ -14290,25 +14268,6 @@
"integrity": "sha512-lxpCM3HTvquGxKGzHeknB/sUjuVoUElLlfYnXZT73K8geR9jQbroGlSCFBax9/0mpGoD3kzcMLnOlGQPJJNyqQ==",
"dev": true
},
"node_modules/konva": {
"version": "8.3.10",
"resolved": "https://registry.npmjs.org/konva/-/konva-8.3.10.tgz",
"integrity": "sha512-5zOynjWBG9wWgpA634SDH+764eyoISpmHLTOCfQ3GFN8OBVd83Genk6H0R4D3hXV0kEGIFAv7RDcSVDtQpPOMw==",
"funding": [
{
"type": "patreon",
"url": "https://www.patreon.com/lavrton"
},
{
"type": "opencollective",
"url": "https://opencollective.com/konva"
},
{
"type": "github",
"url": "https://github.com/sponsors/lavrton"
}
]
},
"node_modules/latest-version": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
@@ -20514,11 +20473,6 @@
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
},
"node_modules/three": {
"version": "0.141.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.141.0.tgz",
"integrity": "sha512-JaSDAPWuk4RTzG5BYRQm8YZbERUxTfTDVouWgHMisS2to4E5fotMS9F2zPFNOIJyEFTTQDDKPpsgZVThKU3pXA=="
},
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@@ -22544,6 +22498,7 @@
"packages/legacy": {
"name": "@motion-canvas/legacy",
"version": "12.0.0-alpha.2",
"extraneous": true,
"license": "MIT",
"dependencies": {
"@types/prismjs": "^1.26.0",
@@ -22568,8 +22523,8 @@
"version": "0.0.0",
"license": "MIT",
"dependencies": {
"@motion-canvas/core": "*",
"@motion-canvas/legacy": "*"
"@motion-canvas/2d": "*",
"@motion-canvas/core": "*"
},
"devDependencies": {
"@motion-canvas/ui": "*",
@@ -26396,25 +26351,11 @@
"typescript": "^4.7.4"
}
},
"@motion-canvas/legacy": {
"version": "file:packages/legacy",
"requires": {
"@motion-canvas/core": "^12.0.0-alpha.2",
"@types/prismjs": "^1.26.0",
"@types/three": "^0.141.0",
"colorjs.io": "^0.3.0",
"konva": "^8.3.9",
"prismjs": "^1.28.0",
"three": "^0.141.0",
"typescript": "^4.7.4",
"vite": "^3.0.9"
}
},
"@motion-canvas/template": {
"version": "file:packages/template",
"requires": {
"@motion-canvas/2d": "*",
"@motion-canvas/core": "*",
"@motion-canvas/legacy": "*",
"@motion-canvas/ui": "*",
"@motion-canvas/vite-plugin": "*",
"vite": "^3.0.9"
@@ -27416,11 +27357,6 @@
"integrity": "sha512-fOwvpvQYStpb/zHMx0Cauwywu9yLDmzWiiQBC7gJyq5tYLUXFZvDG7VK1B7WBxxjBJNKFOZ0zLoOQn8vmATbhw==",
"dev": true
},
"@types/prismjs": {
"version": "1.26.0",
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.0.tgz",
"integrity": "sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ=="
},
"@types/prop-types": {
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
@@ -27524,14 +27460,6 @@
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
"dev": true
},
"@types/three": {
"version": "0.141.0",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.141.0.tgz",
"integrity": "sha512-OJdKDgTPVBUgc+s74DYoy4aLznbFFC38Xm4ElmU1YwGNgR7GGFVvFCX7lpVgOsT6S1zSJtGdajTsOYE8/xY9nA==",
"requires": {
"@types/webxr": "*"
}
},
"@types/tough-cookie": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
@@ -27543,11 +27471,6 @@
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
},
"@types/webxr": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.0.tgz",
"integrity": "sha512-IUMDPSXnYIbEO2IereEFcgcqfDREOgmbGqtrMpVPpACTU6pltYLwHgVkrnYv0XhWEcjio9sYEfIEzgn3c7nDqA=="
},
"@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@@ -33379,11 +33302,6 @@
"integrity": "sha512-lxpCM3HTvquGxKGzHeknB/sUjuVoUElLlfYnXZT73K8geR9jQbroGlSCFBax9/0mpGoD3kzcMLnOlGQPJJNyqQ==",
"dev": true
},
"konva": {
"version": "8.3.10",
"resolved": "https://registry.npmjs.org/konva/-/konva-8.3.10.tgz",
"integrity": "sha512-5zOynjWBG9wWgpA634SDH+764eyoISpmHLTOCfQ3GFN8OBVd83Genk6H0R4D3hXV0kEGIFAv7RDcSVDtQpPOMw=="
},
"latest-version": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
@@ -37987,11 +37905,6 @@
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
},
"three": {
"version": "0.141.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.141.0.tgz",
"integrity": "sha512-JaSDAPWuk4RTzG5BYRQm8YZbERUxTfTDVouWgHMisS2to4E5fotMS9F2zPFNOIJyEFTTQDDKPpsgZVThKU3pXA=="
},
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",

View File

@@ -26,7 +26,7 @@ import {
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';
import {View2D} from '../scenes';
export interface LayoutProps extends NodeProps {
layout?: LayoutMode;
@@ -444,7 +444,7 @@ export class Layout extends Node {
public constructor({tagName = 'div', ...props}: LayoutProps) {
super(props);
this.element = TwoDView.document.createElement(tagName);
this.element = View2D.document.createElement(tagName);
this.element.style.display = 'flex';
this.element.style.boxSizing = 'border-box';

View File

@@ -9,7 +9,7 @@ import {
} from '@motion-canvas/core/lib/utils';
import {ComponentChild, ComponentChildren} from './types';
import {Promisable} from '@motion-canvas/core/lib/threading';
import {TwoDView, use2DView} from '../scenes';
import {View2D, use2DView} from '../scenes';
import {TimingFunction} from '@motion-canvas/core/lib/tweening';
import {threadable} from '@motion-canvas/core/lib/decorators';
@@ -187,7 +187,7 @@ export class Node implements Promisable<Node> {
public constructor({children, ...rest}: NodeProps) {
initialize(this, {defaults: rest});
this.append(children);
this.add(children);
use2DView()?.registerNode(this);
}
@@ -274,11 +274,11 @@ export class Node implements Promisable<Node> {
}
@computed()
public view(): TwoDView | null {
public view(): View2D | null {
return this.parent()?.view() ?? null;
}
public append(node: ComponentChildren): this {
public add(node: ComponentChildren): this {
const nodes: ComponentChild[] = Array.isArray(node) ? node : [node];
for (const node of nodes) {
if (node instanceof Node) {

View File

@@ -3,21 +3,21 @@ import {
Scene,
SceneRenderEvent,
} from '@motion-canvas/core/lib/scenes';
import {TwoDView} from './TwoDView';
import {View2D} from './View2D';
import {useScene} from '@motion-canvas/core/lib/utils';
export function use2DView(): TwoDView | null {
export function use2DView(): View2D | null {
const scene = useScene();
if (scene instanceof TwoDScene) {
if (scene instanceof Scene2D) {
return scene.getView();
}
return null;
}
export class TwoDScene extends GeneratorScene<TwoDView> {
private readonly view = new TwoDView();
export class Scene2D extends GeneratorScene<View2D> {
private readonly view = new View2D();
public getView(): TwoDView {
public getView(): View2D {
return this.view;
}

View File

@@ -1,16 +1,14 @@
import {Layout, Node} from '../components';
export class TwoDView extends Layout {
export class View2D extends Layout {
public static frameID = 'motion-canvas-2d-frame';
public static document: Document;
static {
let frame = document.querySelector<HTMLIFrameElement>(
`#${TwoDView.frameID}`,
);
let frame = document.querySelector<HTMLIFrameElement>(`#${View2D.frameID}`);
if (!frame) {
frame = document.createElement('iframe');
frame.id = TwoDView.frameID;
frame.id = View2D.frameID;
frame.style.position = 'absolute';
frame.style.pointerEvents = 'none';
frame.style.top = '0';
@@ -38,7 +36,7 @@ export class TwoDView extends Layout {
fontStyle: 'normal',
});
TwoDView.document.body.append(this.element);
View2D.document.body.append(this.element);
this.applyFlex();
}
@@ -92,7 +90,7 @@ export class TwoDView extends Layout {
this.updateLayout();
}
public override view(): TwoDView | null {
public override view(): View2D | null {
return this;
}

View File

@@ -1,3 +1,3 @@
export * from './make2DScene';
export * from './TwoDScene';
export * from './TwoDView';
export * from './makeScene2D';
export * from './Scene2D';
export * from './View2D';

View File

@@ -1,15 +0,0 @@
import {
DescriptionOf,
ThreadGeneratorFactory,
} from '@motion-canvas/core/lib/scenes';
import {TwoDView} from './TwoDView';
import {TwoDScene} from './TwoDScene';
export function make2DScene(
runner: ThreadGeneratorFactory<TwoDView>,
): DescriptionOf<TwoDScene> {
return {
klass: TwoDScene,
config: runner,
};
}

View File

@@ -0,0 +1,15 @@
import {
DescriptionOf,
ThreadGeneratorFactory,
} from '@motion-canvas/core/lib/scenes';
import {View2D} from './View2D';
import {Scene2D} from './Scene2D';
export function makeScene2D(
runner: ThreadGeneratorFactory<View2D>,
): DescriptionOf<Scene2D> {
return {
klass: Scene2D,
config: runner,
};
}

View File

@@ -81,7 +81,7 @@ const MANIFEST = JSON.parse(
const templateDir = path.resolve(
fileURLToPath(import.meta.url),
'..',
`template-konva-${response.language}`,
`template-2d-${response.language}`,
);
copyDirectory(templateDir, response.path);

View File

@@ -8,7 +8,7 @@
},
"dependencies": {
"@motion-canvas/core": "*",
"@motion-canvas/legacy": "*"
"@motion-canvas/2d": "*"
},
"devDependencies": {
"@motion-canvas/ui": "*",

View File

@@ -1,7 +1,7 @@
import {makeKonvaScene} from '@motion-canvas/legacy/lib/scenes';
import {makeScene2D} from '@motion-canvas/2d/lib/scenes';
import {waitFor} from '@motion-canvas/core/lib/flow';
export default makeKonvaScene(function* (view) {
export default makeScene2D(function* (view) {
// Create your animations here
yield* waitFor(5);

View File

@@ -0,0 +1,6 @@
import {defineConfig} from 'vite';
import motionCanvas from '@motion-canvas/vite-plugin';
export default defineConfig({
plugins: [motionCanvas()],
});

View File

@@ -8,7 +8,7 @@
},
"dependencies": {
"@motion-canvas/core": "*",
"@motion-canvas/legacy": "*"
"@motion-canvas/2d": "*"
},
"devDependencies": {
"@motion-canvas/ui": "*",

View File

@@ -0,0 +1 @@
/// <reference types="@motion-canvas/core/project" />

View File

@@ -1,7 +1,7 @@
import {makeKonvaScene} from '@motion-canvas/legacy/lib/scenes';
import {makeScene2D} from '@motion-canvas/2d/lib/scenes';
import {waitFor} from '@motion-canvas/core/lib/flow';
export default makeKonvaScene(function* (view) {
export default makeScene2D(function* (view) {
// Create your animations here
yield* waitFor(5);

View File

@@ -1,5 +1,5 @@
{
"extends": "@motion-canvas/legacy/tsconfig.project.json",
"extends": "@motion-canvas/2d/tsconfig.project.json",
"compilerOptions": {
"baseUrl": "src"
},

View File

@@ -1,7 +1,6 @@
import {defineConfig} from 'vite';
import motionCanvas from '@motion-canvas/vite-plugin';
import legacyRenderer from '@motion-canvas/legacy/vite';
export default defineConfig({
plugins: [motionCanvas(), legacyRenderer()],
plugins: [motionCanvas()],
});

View File

@@ -1,12 +0,0 @@
import {defineConfig} from 'vite';
import motionCanvas from '@motion-canvas/vite-plugin';
import legacyRenderer from '@motion-canvas/legacy/vite';
export default defineConfig({
plugins: [
motionCanvas({
project: './src/project.js',
}),
legacyRenderer(),
],
});

View File

@@ -1 +0,0 @@
/// <reference types="@motion-canvas/legacy/project" />

View File

@@ -1,52 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [12.0.0-alpha.2](https://github.com/motion-canvas/motion-canvas/compare/v12.0.0-alpha.1...v12.0.0-alpha.2) (2022-09-07)
### Features
* better naming conventions ([#62](https://github.com/motion-canvas/motion-canvas/issues/62)) ([a9d764f](https://github.com/motion-canvas/motion-canvas/commit/a9d764fbceb639497ef45f44c90f9b6e408213d3))
### BREAKING CHANGES
* change names of timing and interpolation functions
`TweenFunction` is now called `InterpolationFunction`.
Individual functions are now called `[type]Lerp` instead of `[type]Tween`.
For instance: `colorTween` is now `colorLerp`.
`InterpolationFunction` is now called `TimingFunction`.
This name is better aligned with the CSS spec.
# [12.0.0-alpha.1](https://github.com/motion-canvas/motion-canvas/compare/v12.0.0-alpha.0...v12.0.0-alpha.1) (2022-08-31)
### Bug Fixes
* **legacy:** add missing files ([#61](https://github.com/motion-canvas/motion-canvas/issues/61)) ([fad87d5](https://github.com/motion-canvas/motion-canvas/commit/fad87d5aa5500e7c63cb914fc51044db6225502e))
# [12.0.0-alpha.0](https://github.com/motion-canvas/motion-canvas/compare/v11.1.0...v12.0.0-alpha.0) (2022-08-31)
### Features
* extract konva to separate package ([#60](https://github.com/motion-canvas/motion-canvas/issues/60)) ([4ecad3c](https://github.com/motion-canvas/motion-canvas/commit/4ecad3ca2732bd5147af670c230f8f959129a707))
### BREAKING CHANGES
* change to import paths
See [the migration guide](https://motion-canvas.github.io/guides/migration/12.0.0) for more info.

View File

@@ -1,42 +0,0 @@
{
"name": "@motion-canvas/legacy",
"version": "12.0.0-alpha.2",
"description": "The legacy Motion Canvas renderer based on the Konva library",
"main": "lib/scenes/index.js",
"author": "motion-canvas",
"license": "MIT",
"scripts": {
"build": "tsc",
"watch": "tsc -w"
},
"publishConfig": {
"registry": "https://npm.pkg.github.com/motion-canvas"
},
"repository": {
"type": "git",
"url": "git+https://github.com/motion-canvas/motion-canvas.git"
},
"files": [
"lib",
"vite",
"project.d.ts",
"tsconfig.project.json"
],
"peerDependencies": {
"@motion-canvas/core": "*",
"vite": "3.x"
},
"dependencies": {
"@types/prismjs": "^1.26.0",
"@types/three": "^0.141.0",
"colorjs.io": "^0.3.0",
"konva": "^8.3.9",
"prismjs": "^1.28.0",
"three": "^0.141.0"
},
"devDependencies": {
"@motion-canvas/core": "^12.0.0-alpha.2",
"typescript": "^4.7.4",
"vite": "^3.0.9"
}
}

View File

@@ -1,13 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars,@typescript-eslint/no-namespace */
/// <reference types="@motion-canvas/core/project" />
/// <reference types="@motion-canvas/legacy/lib/patches/Factory" />
/// <reference types="@motion-canvas/legacy/lib/patches/Node" />
/// <reference types="@motion-canvas/legacy/lib/patches/Shape" />
/// <reference types="@motion-canvas/legacy/lib/patches/Container" />
declare namespace JSX {
type ElementClass = import('konva/lib/Node').Node;
interface ElementChildrenAttribute {
children: unknown;
}
}

View File

@@ -1,9 +0,0 @@
/**
* Animation utilities.
*
* @module
*/
export * from './show';
export * from './interpolationFunctions';
export * from './surfaceFrom';
export * from './surfaceTransition';

View File

@@ -1,81 +0,0 @@
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,89 +0,0 @@
import type {Node} from 'konva/lib/Node';
import {Surface} from '../components';
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 {
easeInOutCubic,
easeOutCubic,
easeOutExpo,
map,
tween,
} from '@motion-canvas/core/lib/tweening';
import {decorate, threadable} from '@motion-canvas/core/lib/decorators';
import {ThreadGenerator} from '@motion-canvas/core/lib/threading';
decorate(showTop, threadable());
/**
* Show the given node by sliding it up.
*
* @param node - The node to animate.
*/
export function* showTop(node: Node): ThreadGenerator {
const to = node.offsetY();
const from = to - 40;
node.show();
node.cache();
node.offsetY(from);
node.opacity(0);
yield* all(
node.opacity(1, 0.5, easeOutExpo),
node.offsetY(to, 0.5, easeOutExpo),
);
node.clearCache();
}
decorate(showSurfaceVertically, threadable());
/**
* Show the given surface by expanding its mask vertically.
*
* @param surface - The surface to animate.
*/
export function* showSurfaceVertically(surface: Surface): ThreadGenerator {
const mask = surface.getMask();
surface.show();
surface.setMask({...mask, height: 0});
yield* tween(0.5, value => {
surface.setMask({
...mask,
height: map(0, mask.height, easeOutCubic(value)),
});
});
surface.setMask(null);
}
decorate(showCircle, threadable());
/**
* Show the given surface using a circle mask.
*
* @param surface - The surface to animate.
* @param duration - The duration of the animation.
* @param origin - The center of the circle mask.
*/
export function* showCircle(
surface: Surface,
duration = 0.6,
origin?: Origin | Vector2d,
): ThreadGenerator {
const position =
typeof origin === 'object'
? origin
: originPosition(origin ?? surface.getOrigin());
const mask = surface.getAbsoluteCircleMask({
circleMask: {
...position,
radius: 1,
},
});
surface.show();
surface.setCircleMask(mask);
const target = mask.radius;
mask.radius = 0;
yield* tween(duration, value => {
mask.radius = easeInOutCubic(value, 0, target);
});
surface.setCircleMask(null);
}

View File

@@ -1,96 +0,0 @@
import type {Vector2d} from 'konva/lib/types';
import type {ThreadGenerator} from '@motion-canvas/core/lib/threading';
import type {Surface, SurfaceMask} from '../components';
import {
colorLerp,
easeInOutCubic,
easeInOutQuint,
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}.
*/
export interface SurfaceFromConfig {
/**
* Whether the transition arc should be reversed.
*
* @remarks
* See {@link rectArcLerp} for more detail.
*/
reverse?: boolean;
/**
* A function called when the initial surface is updated.
*
* @param surface - The initial surface.
* @param value - Completion of the entire transition.
*
* @returns `true` if the default changes made by {@link surfaceFrom}
* should be prevented.
*/
onUpdate?: (surface: Surface, value: number) => boolean | void;
duration?: number;
}
decorate(surfaceFrom, threadable());
/**
* Animate the mask of the surface from the initial state to its current state.
*
* @param surface - The surface to animate.
* @param mask - The initial mask.
* @param position - The initial position.
* @param config - Additional configuration.
*/
export function* surfaceFrom(
surface: Surface,
mask: Partial<SurfaceMask> = {width: 0, height: 0},
position?: Vector2d,
config: SurfaceFromConfig = {},
): ThreadGenerator {
const toMask = surface.getMask();
const fromMask = {...toMask, ...mask};
const toPosition = surface.getPosition();
if (position) {
surface.position(position);
}
surface.show().setMask(fromMask);
const ratio =
(calculateRatio(surface.getPosition(), toPosition) +
calculateRatio(fromMask, toMask)) /
2;
yield* tween(config.duration ?? 0.6, value => {
surface.setMask({
...fromMask,
...rectArcLerp(
fromMask,
toMask,
easeInOutQuint(value),
config.reverse,
ratio,
),
radius: easeInOutCubic(value, fromMask.radius, toMask.radius),
color: colorLerp(fromMask.color, toMask.color, easeInOutQuint(value)),
});
if (position) {
surface.setPosition(
rectArcLerp(
position,
toPosition,
easeInOutQuint(value),
config.reverse,
ratio,
),
);
}
if (!config.onUpdate?.(surface, value)) {
surface.getChild().opacity(value);
}
});
surface.setMask(null);
}

View File

@@ -1,185 +0,0 @@
import {Surface} from '../components';
import {
clampRemap,
colorLerp,
easeInOutCubic,
easeInOutQuint,
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}.
*
* @remarks
* For {@link SurfaceTransitionConfig.onInitialSurfaceUpdate | `onInitialSurfaceUpdate`}
* and {@link SurfaceTransitionConfig.onTargetSurfaceUpdate | `onTargetSurfaceUpdate`}
* callbacks, the `value` and `relativeValue` arguments represent the absolute
* and relative completion of the transition:
* ```text
*
* start onSurfaceChange end
* │ │ │
* │ 0 <──────────┼─────────────────> 1 │ <- value
* │ 0 <──────> 1 │ 0 <─────────────> 1 │ <- relativeValue
* ──────┴──── from ────┴──────── to ─────────┴───────────────────────>
* time
* ```
*/
export interface SurfaceTransitionConfig {
/**
* Whether the transition arc should be reversed.
*
* @remarks
* See {@link rectArcLerp} for more detail.
*/
reverse?: boolean;
/**
* A function called when the currently displayed surface changes.
*
* @param surface - The new surface.
*/
onSurfaceChange?: (surface: Surface) => void;
/**
* A function called when the initial surface is updated.
*
* @param surface - The initial surface.
* @param value - Completion of the entire transition.
* @param relativeValue - Relative completion of the transition.
*
* @returns `true` if the default changes made by {@link surfaceTransition}
* should be prevented
*/
onInitialSurfaceUpdate?: (
surface: Surface,
value: number,
relativeValue: number,
) => true | void;
/**
* A function called when the target surface is updated.
*
* @param surface - The target surface.
* @param value - Completion of the entire transition.
* @param relativeValue - Relative completion of the transition.
*
* @returns `true` if the default changes made by {@link surfaceTransition}
* should be prevented
*/
onTargetSurfaceUpdate?: (
surface: Surface,
value: number,
relativeValue: number,
) => true | void;
/**
* Duration at which the surfaces are swapped.
*/
transitionTime?: number;
duration?: number;
}
decorate(surfaceTransition, threadable());
/**
* Morph one surface into another.
*
* @param initial - The initial surface.
* @param target - The target surface.
* @param config - Additional configuration.
*/
export function* surfaceTransition(
initial: Surface,
target: Surface,
config: SurfaceTransitionConfig = {},
): ThreadGenerator {
const from = initial.getMask();
const transitionTime = config.transitionTime ?? 1 / 3;
const to = target.getMask();
const toPos = target.getPosition();
const fromPos = initial.getPosition();
const fromDelta = initial.getOriginDelta(target.getOrigin());
const fromNewPos = {
x: fromPos.x + fromDelta.x,
y: fromPos.y + fromDelta.y,
};
const toDelta = target.getOriginDelta(initial.getOrigin());
const toNewPos = {
x: toPos.x + toDelta.x,
y: toPos.y + toDelta.y,
};
const ratio =
(calculateRatio(fromNewPos, toPos) + calculateRatio(from, to)) / 2;
target.hide();
config.onSurfaceChange?.(initial);
let check = true;
let relativeValue = 0;
yield* tween(config.duration ?? 0.6, value => {
if (value > transitionTime) {
relativeValue = clampRemap(transitionTime, 1, 0, 1, value);
if (check) {
target.show();
initial.hide();
}
target.setMask({
...from,
...rectArcLerp(from, to, easeInOutQuint(value), config.reverse, ratio),
radius: easeInOutCubic(value, from.radius, target.radius()),
color: colorLerp(
from.color,
target.background(),
easeInOutQuint(value),
),
});
target.setPosition(
rectArcLerp(
fromNewPos,
toPos,
easeInOutQuint(value),
config.reverse,
ratio,
),
);
if (!config.onTargetSurfaceUpdate?.(target, value, relativeValue)) {
target.getChild().opacity(relativeValue);
}
if (check) {
config.onSurfaceChange?.(target);
check = false;
}
} else {
relativeValue = clampRemap(0, transitionTime, 1, 0, value);
initial.setMask({
...from,
...rectArcLerp(from, to, easeInOutQuint(value), config.reverse, ratio),
radius: easeInOutCubic(value, from.radius, target.radius()),
color: colorLerp(
from.color,
target.background(),
easeInOutQuint(value),
),
});
initial.setPosition(
rectArcLerp(
fromPos,
toNewPos,
easeInOutQuint(value),
config.reverse,
ratio,
),
);
if (!config.onInitialSurfaceUpdate?.(target, value, relativeValue)) {
initial.getChild().opacity(relativeValue);
}
}
});
initial.position(fromPos).setMask(null);
target.position(toPos).setMask(null);
}

View File

@@ -1,32 +0,0 @@
import type {Node} from 'konva/lib/Node';
import {Origin} from '@motion-canvas/core/lib/types';
import {getOriginDelta} from '../types';
import {useScene} from '@motion-canvas/core/lib/utils';
interface AlignConfig {
origin?: Origin;
children: Node | Node[];
}
export function Align(config: AlignConfig) {
const origin = config.origin ?? Origin.Middle;
if (origin === Origin.Middle) {
console.warn(
'<Align> with origin set to Middle does nothing and can be omitted',
);
return <>{config.children}</>;
}
const scene = useScene();
const position = getOriginDelta(scene.getSize(), Origin.Middle, origin);
if (Array.isArray(config.children)) {
for (const child of config.children) {
child.move(position);
}
} else {
config.children.move(position);
}
return <>{config.children}</>;
}

View File

@@ -1,94 +0,0 @@
import {KonvaNode, getset} from '../decorators';
import {Sprite} from './Sprite';
import {GetSet} from 'konva/lib/types';
import {LinearLayout} from './LinearLayout';
import {Center} from '@motion-canvas/core/lib/types';
import {Surface, SurfaceConfig} from './Surface';
import {getStyle, Style} from '../styles';
import {ImageDataSource} from '@motion-canvas/core/lib/media';
export interface AnimationClipConfig extends SurfaceConfig {
animation: ImageDataSource[];
skin?: ImageDataSource;
frame?: number;
style?: Partial<Style>;
}
@KonvaNode()
export class AnimationClip extends Surface {
@getset(null, AnimationClip.prototype.update)
public animation: GetSet<AnimationClipConfig['animation'], this>;
@getset(null, AnimationClip.prototype.update)
public skin: GetSet<AnimationClipConfig['skin'], this>;
@getset(0, AnimationClip.prototype.updateStyle)
public frame: GetSet<AnimationClipConfig['frame'], this>;
@getset(null, AnimationClip.prototype.updateStyle)
public style: GetSet<AnimationClipConfig['style'], this>;
public constructor(config?: AnimationClipConfig) {
super({
direction: Center.Horizontal,
...config,
});
this.setChild(new LinearLayout({direction: Center.Horizontal, padd: 5}));
this.update();
}
private update() {
const animation = this.animation();
const layout = this.getChild<LinearLayout>();
if (!animation || !layout) return;
const skin = this.skin();
for (let i = 0; i < animation.length; i++) {
let sprite: Sprite;
let surface: Surface;
if (layout.children.length > i) {
surface = <Surface>layout.children[i];
sprite = surface.getChild<Sprite>();
sprite.animation(animation);
sprite.skin(skin);
} else {
surface = new Surface({
padd: 22,
margin: 5,
});
sprite = new Sprite({
animation,
skin,
width: 96,
height: 96,
});
surface.setChild(sprite);
layout.add(surface);
}
}
for (let i = layout.children.length - 1; i >= animation.length; i--) {
layout.children[i].destroy();
}
this.updateStyle();
}
private updateStyle() {
const style = getStyle(this);
const layout = this.getChild<LinearLayout>();
const animation = this.animation();
if (!animation || !layout) return;
const children = this.getChild<LinearLayout>().children;
const frame = this.frame() % animation.length;
this.background(style.background);
for (let i = 0; i < children.length; i++) {
const surface = <Surface>children[i];
surface.background(
frame === i ? style.backgroundLight : 'rgba(0, 0, 0, 0)',
);
surface.getChild<Sprite>().frame(i);
}
}
}

View File

@@ -1,357 +0,0 @@
import {Node} from 'konva/lib/Node';
import {Shape, ShapeConfig} from 'konva/lib/Shape';
import {Context} from 'konva/lib/Context';
import {GetSet, Vector2d} from 'konva/lib/types';
import {clamp} from 'three/src/math/MathUtils';
import {KonvaNode, getset} from '../decorators';
export interface ArrowConfig extends ShapeConfig {
radius?: number;
points?: number[];
startArrow?: boolean;
start?: number;
endArrow?: boolean;
arrowSize?: number;
end?: number;
}
abstract class Segment {
public abstract draw(
context: Context,
start: number,
end: number,
move: boolean,
): [Vector2d, Vector2d, Vector2d, Vector2d];
public abstract get arcLength(): number;
}
class LineSegment extends Segment {
private readonly length: number;
private readonly vector: Vector2d;
private readonly tangent: Vector2d;
public constructor(private from: Vector2d, private to: Vector2d) {
super();
this.vector = {
x: this.to.x - this.from.x,
y: this.to.y - this.from.y,
};
this.length = Math.sqrt(
this.vector.x * this.vector.x + this.vector.y * this.vector.y,
);
this.tangent = {
x: -this.vector.y / this.length,
y: this.vector.x / this.length,
};
}
public get arcLength(): number {
return this.length;
}
public draw(
context: Context,
start = 0,
end = 1,
move = false,
): [Vector2d, Vector2d, Vector2d, Vector2d] {
const from = {
x: this.from.x + this.vector.x * start,
y: this.from.y + this.vector.y * start,
};
const to = {
x: this.from.x + this.vector.x * end,
y: this.from.y + this.vector.y * end,
};
if (move) context.moveTo(from.x, from.y);
context.lineTo(to.x, to.y);
return [
from,
{
x: -this.tangent.x,
y: -this.tangent.y,
},
to,
this.tangent,
];
}
}
class CircleSegment extends Segment {
private readonly length: number;
private readonly delta: number;
public constructor(
private center: Vector2d,
private radius: number,
private startAngle: number,
private deltaAngle: number,
private counter: boolean,
) {
super();
this.delta = this.counter ? this.deltaAngle : this.deltaAngle + Math.PI * 2;
this.length = Math.abs(this.delta * radius);
}
public get arcLength(): number {
return this.length;
}
public draw(
context: Context,
from: number,
to: number,
): [Vector2d, Vector2d, Vector2d, Vector2d] {
const startAngle = this.startAngle + this.delta * from;
const endAngle = this.startAngle + this.delta * to;
context.arc(
this.center.x,
this.center.y,
this.radius,
startAngle,
endAngle,
this.counter,
);
const startTangent = {
x: Math.cos(startAngle),
y: Math.sin(startAngle),
};
const endTangent = {
x: Math.cos(endAngle),
y: Math.sin(endAngle),
};
return [
{
x: this.center.x + this.radius * startTangent.x,
y: this.center.y + this.radius * startTangent.y,
},
this.counter
? {
x: -startTangent.x,
y: -startTangent.y,
}
: startTangent,
{
x: this.center.x + this.radius * endTangent.x,
y: this.center.y + this.radius * endTangent.y,
},
this.counter
? endTangent
: {
x: -endTangent.x,
y: -endTangent.y,
},
];
}
}
@KonvaNode()
export class Arrow extends Shape<ArrowConfig> {
@getset(8, Node.prototype.markDirty)
public radius: GetSet<number, this>;
@getset([], Node.prototype.markDirty)
public points: GetSet<number[], this>;
@getset(0)
public start: GetSet<number, this>;
@getset(1)
public end: GetSet<number, this>;
@getset(false)
public startArrow: GetSet<boolean, this>;
@getset(false)
public endArrow: GetSet<boolean, this>;
@getset(16)
public arrowSize: GetSet<number, this>;
private segments: Segment[] = [];
private arcLength = 0;
public _sceneFunc(context: Context) {
let start = this.start() * this.arcLength;
let end = this.end() * this.arcLength;
if (start > end) {
[start, end] = [end, start];
}
const offset = start;
const distance = end - start;
const arrowSize = this.arrowSize();
const arrowScale =
(distance > arrowSize ? arrowSize : distance < 0 ? 0 : distance) /
arrowSize;
context.beginPath();
let length = 0;
let firstPoint = null;
let firstTangent = null;
let lastPoint = null;
let lastTangent = null;
for (const segment of this.segments) {
length += segment.arcLength;
const relativeStart =
(start - length + segment.arcLength) / segment.arcLength;
const relativeEnd =
(end - length + segment.arcLength) / segment.arcLength;
const clampedStart =
relativeStart > 1 ? 1 : relativeStart < 0 ? 0 : relativeStart;
const clampedEnd =
relativeEnd > 1 ? 1 : relativeEnd < 0 ? 0 : relativeEnd;
if (length < start) {
continue;
}
const [first, fTangent, last, lTangent] = segment.draw(
context,
clampedStart,
clampedEnd,
firstPoint === null,
);
if (firstPoint === null) {
firstPoint = first;
firstTangent = fTangent;
}
lastPoint = last;
lastTangent = lTangent;
if (length > end) {
break;
}
}
this.dashOffset(offset);
context.strokeShape(this);
context.beginPath();
if (this.attrs.endArrow && lastPoint && arrowScale > 0.0001) {
this.drawArrow(context, lastPoint, lastTangent, arrowScale);
}
if (this.attrs.startArrow && firstPoint && arrowScale > 0.0001) {
this.drawArrow(context, firstPoint, firstTangent, arrowScale);
}
context.closePath();
this.fill(this.stroke());
context.fillShape(this);
}
public getStart(): number {
return clamp(this.attrs.start ?? 0, 0, 1);
}
public getEnd(): number {
return clamp(this.attrs.end ?? 1, 0, 1);
}
private drawArrow(
context: Context,
center: Vector2d,
tangent: Vector2d,
size: number,
) {
const arrowSize = this.arrowSize() * size;
const offset = this.strokeWidth() / 2;
// noinspection JSSuspiciousNameCombination
const normal = {
x: -tangent.y,
y: tangent.x,
};
center.x -= normal.x * offset * size;
center.y -= normal.y * offset * size;
context.moveTo(center.x, center.y);
context.lineTo(
center.x + (normal.x + tangent.x) * arrowSize,
center.y + (normal.y + tangent.y) * arrowSize,
);
context.lineTo(
center.x + (normal.x - tangent.x) * arrowSize,
center.y + (normal.y - tangent.y) * arrowSize,
);
context.lineTo(center.x, center.y);
context.closePath();
}
public recalculateLayout() {
this.arcLength = 0;
this.segments = [];
const points: number[] = this.points();
const radius = this.radius();
let lastX = points[0];
let lastY = points[1];
for (let i = 5; i < points.length; i += 2) {
const startX = points[i - 5];
const startY = points[i - 4];
const centerX = points[i - 3];
const centerY = points[i - 2];
const endX = points[i - 1];
const endY = points[i];
const toStartX = startX - centerX;
const toStartY = startY - centerY;
const toEndX = endX - centerX;
const toEndY = endY - centerY;
const correctAngle = Math.atan2(
toEndY * toStartX - toEndX * toStartY,
toEndX * toStartX + toEndY * toStartY,
);
const startAngle = Math.atan2(-toStartY, toStartX);
const endAngle = Math.atan2(-toEndY, toEndX);
const angle = startAngle - correctAngle / 2;
const length = radius / Math.abs(Math.sin(correctAngle / 2));
const circleX = length * Math.cos(angle);
const circleY = length * -Math.sin(angle);
const deltaLength = radius / Math.abs(Math.tan(correctAngle / 2));
const startDeltaX = deltaLength * Math.cos(startAngle);
const startDeltaY = deltaLength * -Math.sin(startAngle);
const endDeltaX = deltaLength * Math.cos(endAngle);
const endDeltaY = deltaLength * -Math.sin(endAngle);
const start = {x: centerX + startDeltaX, y: centerY + startDeltaY};
const center = {x: centerX + circleX, y: centerY + circleY};
const centerToStart = {x: start.x - center.x, y: start.y - center.y};
const perpendicularAngle = -Math.atan2(-centerToStart.y, centerToStart.x);
const line = new LineSegment({x: lastX, y: lastY}, start);
const circle = new CircleSegment(
center,
radius,
perpendicularAngle,
correctAngle - Math.PI,
correctAngle > 0,
);
this.segments.push(line);
this.segments.push(circle);
this.arcLength += line.arcLength;
this.arcLength += circle.arcLength;
lastX = centerX + endDeltaX;
lastY = centerY + endDeltaY;
}
const line = new LineSegment(
{x: lastX, y: lastY},
{x: points[points.length - 2], y: points[points.length - 1]},
);
this.segments.push(line);
this.arcLength += line.arcLength;
super.recalculateLayout();
}
public getArrowSize(): number {
return this.attrs.arrowSize ?? this.strokeWidth() * 2;
}
}

View File

@@ -1,125 +0,0 @@
import {LinearLayout, LinearLayoutConfig} from './LinearLayout';
import {Rect} from 'konva/lib/shapes/Rect';
import {Range, RangeConfig} from './Range';
import {KonvaNode, getset} from '../decorators';
import {parseColor} from 'mix-color';
import {Surface} from './Surface';
import {Origin} from '@motion-canvas/core/lib/types';
import {Style} from '../styles';
import {GetSet} from 'konva/lib/types';
import {clampRemap} from '@motion-canvas/core/lib/tweening';
import {makeRef} from '@motion-canvas/core/lib/utils';
export interface ColorPickerConfig extends LinearLayoutConfig {
previewColor?: string;
dissolve?: number;
style?: Style;
}
const colorRangeConfig: RangeConfig = {
width: 280,
height: 60,
range: [0, 255],
precision: 0,
margin: [10, 40],
value: 0,
};
@KonvaNode()
export class ColorPicker extends Surface {
@getset(null)
public style: GetSet<ColorPickerConfig['style'], this>;
@getset('#000000', ColorPicker.prototype.updateColor)
public previewColor: GetSet<ColorPickerConfig['previewColor'], this>;
@getset(0, ColorPicker.prototype.updateDissolve)
public dissolve: GetSet<ColorPickerConfig['dissolve'], this>;
public readonly preview: Rect;
public readonly r: Range;
public readonly g: Range;
public readonly b: Range;
public readonly a: Range;
public parsedColor: ReturnType<typeof parseColor>;
public constructor(config?: ColorPickerConfig) {
super(config);
this.setChild(
<LinearLayout origin={Origin.Top}>
<Rect
ref={makeRef(this, 'preview')}
width={360}
height={200}
fill={'yellow'}
cornerRadius={[8, 8, 0, 0]}
/>
<Range
ref={makeRef(this, 'r')}
{...colorRangeConfig}
label={'R:'}
margin={[40, 40, 10]}
/>
<Range ref={makeRef(this, 'g')} {...colorRangeConfig} label={'G:'} />
<Range ref={makeRef(this, 'b')} {...colorRangeConfig} label={'B:'} />
<Range
ref={makeRef(this, 'a')}
{...colorRangeConfig}
label={'A:'}
margin={[10, 40, 40]}
/>
</LinearLayout>,
);
this.updateColor();
this.updateDissolve();
}
private updateColor() {
if (!this.a) return;
const style = this.style();
const preview = this.previewColor();
this.style({
...style,
foreground: preview,
});
this.parsedColor = parseColor(preview);
this.parsedColor.a = Math.round(this.parsedColor.a * 255);
this.r.value(this.parsedColor.r);
this.g.value(this.parsedColor.g);
this.b.value(this.parsedColor.b);
this.a.value(this.parsedColor.a);
this.preview.fill(preview);
}
public setStyle(value: Style): this {
this.attrs.style = value;
this.background(value.background);
return this;
}
private updateDissolve() {
if (!this.r) return;
const opacity = clampRemap(0, 0.5, 1, 0, this.dissolve());
this.r.opacity(opacity);
this.g.opacity(opacity);
this.b.opacity(opacity);
this.a.opacity(opacity);
this.markDirty();
}
public recalculateLayout() {
if (this.preview) {
const rangeWidth = this.preview.width() - 80;
this.r.width(rangeWidth);
this.g.width(rangeWidth);
this.b.width(rangeWidth);
this.a.width(rangeWidth);
}
super.recalculateLayout();
}
}

View File

@@ -1,267 +0,0 @@
import {Node} from 'konva/lib/Node';
import {Center} from '@motion-canvas/core/lib/types';
import {KonvaNode} from '../decorators';
import {Pin} from './Pin';
import {Group} from 'konva/lib/Group';
import {ContainerConfig} from 'konva/lib/Container';
import {Arrow} from './Arrow';
import {map} from '@motion-canvas/core/lib/tweening';
import {useKonvaView} from '../scenes';
export interface ConnectionConfig extends ContainerConfig {
start?: Pin;
end?: Pin;
startTarget?: Node;
endTarget?: Node;
crossing?: Node;
arrow?: Arrow;
}
interface Measurement {
direction: -1 | 0 | 1;
range: [number, number];
rangeOffset: [number, number];
from: number;
to: number;
preferNegative: boolean;
}
function clamp(value: number, min: number, max: number): number {
if (min > max) [min, max] = [max, min];
return value < min ? min : value > max ? max : value;
}
@KonvaNode()
export class Connection extends Group {
public readonly start: Pin;
public readonly end: Pin;
public readonly crossing: Node = null;
public readonly arrow: Arrow;
public constructor(config?: ConnectionConfig) {
super(config);
this.start = config?.start ?? new Pin();
if (!this.start.getParent()) {
this.add(this.start);
}
this.end = config?.end ?? new Pin();
if (!this.end.getParent()) {
this.add(this.end);
}
if (config?.crossing) {
this.crossing = config.crossing;
}
this.arrow = config?.arrow ?? new Arrow();
if (!this.arrow.getParent()) {
this.add(this.arrow);
}
if (config?.startTarget) {
this.start.target(config?.startTarget);
}
if (config?.endTarget) {
this.end.target(config?.endTarget);
}
}
private static measurePosition(
positionA: number,
sizeA: number,
positionB: number,
sizeB: number,
): Measurement {
// FIXME use layout margin
const padding = 20;
const length = 20;
const offset = (padding + length) * 2;
let preferNegative = false;
let direction: -1 | 0 | 1;
let range: [number, number];
let rangeOffset: [number, number];
if (positionA - sizeA - offset > positionB + sizeB) {
direction = -1;
range = [positionA - sizeA - padding, positionB + sizeB + padding];
rangeOffset = [range[0] - length, range[1] + length];
} else if (positionA + sizeA + offset < positionB - sizeB) {
direction = 1;
range = [positionA + sizeA + padding, positionB - sizeB - padding];
rangeOffset = [range[0] + length, range[1] - length];
} else {
direction = 0;
if (
Math.abs(positionA + sizeA - positionB - sizeB) >
Math.abs(positionA - sizeA - positionB + sizeB)
) {
range = [positionA - sizeA - padding, positionB - sizeB - padding];
rangeOffset = [range[0] - length, range[1] - length];
} else {
preferNegative = true;
range = [positionA + sizeA + padding, positionB + sizeB + padding];
rangeOffset = [range[0] + length, range[1] + length];
}
}
return {
direction,
range,
rangeOffset,
from: positionA,
to: positionB,
preferNegative,
};
}
private static calculateCrossing(
crossing: number,
measurement: Measurement,
): number {
const fractionX = crossing >= 0 && crossing <= 1;
let clampedCrossing = crossing + measurement.from;
if (measurement.direction !== 0 && !fractionX) {
clampedCrossing = clamp(
clampedCrossing,
measurement.rangeOffset[0],
measurement.rangeOffset[1],
);
}
if (measurement.direction === 0) {
return measurement.preferNegative
? Math.max(
measurement.rangeOffset[0],
measurement.rangeOffset[1],
clampedCrossing,
)
: Math.min(
measurement.rangeOffset[0],
measurement.rangeOffset[1],
clampedCrossing,
);
}
return fractionX
? map(measurement.rangeOffset[0], measurement.rangeOffset[1], crossing)
: clampedCrossing;
}
public isDirty(): boolean {
return (
this.attrs.dirty ||
this.start.wasDirty() ||
this.end.wasDirty() ||
this.crossing?.wasDirty()
);
}
public updateLayout() {
this.start.updateLayout();
this.end.updateLayout();
this.attrs.wasDirty = false;
if (this.isDirty()) {
this.recalculateLayout();
this.attrs.dirty = false;
this.attrs.wasDirty = true;
}
this.arrow.updateLayout();
}
public recalculateLayout() {
if (!this.start || !this.end || !this.arrow) {
this.arrow?.points([]);
return;
}
const view = useKonvaView();
const crossing = this.crossing
? this.crossing.getAbsolutePosition(view)
: {x: 0.5, y: 0.5};
const fromRect = this.start.getClientRect({relativeTo: view});
fromRect.width /= 2;
fromRect.height /= 2;
fromRect.x += fromRect.width;
fromRect.y += fromRect.height;
const toRect = this.end.getClientRect({relativeTo: view});
toRect.width /= 2;
toRect.height /= 2;
toRect.x += toRect.width;
toRect.y += toRect.height;
const points: number[] = [];
const horizontal = Connection.measurePosition(
fromRect.x,
fromRect.width,
toRect.x,
toRect.width,
);
const vertical = Connection.measurePosition(
fromRect.y,
fromRect.height,
toRect.y,
toRect.height,
);
if (this.start.direction() === Center.Vertical) {
points.push(fromRect.x, vertical.range[0]);
} else {
points.push(horizontal.range[0], fromRect.y);
}
let endDirection = this.end.direction();
if (this.start.direction() !== endDirection) {
if (endDirection === Center.Vertical && vertical.direction === 0) {
endDirection = this.start.direction();
} else if (
endDirection === Center.Horizontal &&
horizontal.direction === 0
) {
endDirection = this.start.direction();
}
}
let distance = 100;
if (this.start.direction() === endDirection) {
if (endDirection === Center.Vertical) {
distance = Math.abs(toRect.x - fromRect.x);
if (distance >= 1) {
const y = Connection.calculateCrossing(crossing.y, vertical);
points.push(fromRect.x, y);
points.push(toRect.x, y);
}
} else {
distance = Math.abs(toRect.y - fromRect.y);
if (distance >= 1) {
const x = Connection.calculateCrossing(crossing.x, horizontal);
points.push(x, fromRect.y);
points.push(x, toRect.y);
}
}
} else {
if (endDirection === Center.Vertical) {
points.push(toRect.x, fromRect.y);
} else {
points.push(fromRect.x, toRect.y);
}
}
if (endDirection === Center.Vertical) {
points.push(toRect.x, vertical.range[1]);
} else {
points.push(horizontal.range[1], toRect.y);
}
this.arrow.radius(Math.min(8, distance / 2));
this.arrow.points(points);
super.recalculateLayout();
}
}

View File

@@ -1,109 +0,0 @@
import {Context} from 'konva/lib/Context';
import {GetSet} from 'konva/lib/types';
import {KonvaNode, getset} from '../decorators';
import {CanvasHelper} from '../helpers';
import {Shape, ShapeConfig} from 'konva/lib/Shape';
export interface GridConfig extends ShapeConfig {
gridSize?: number;
subdivision?: boolean;
checker?: boolean;
}
@KonvaNode()
export class Grid extends Shape {
@getset(16, Grid.prototype.recalculate)
public gridSize: GetSet<number, this>;
@getset(false)
public checker: GetSet<GridConfig['checker'], this>;
@getset(false)
public subdivision: GetSet<GridConfig['subdivision'], this>;
private path: Path2D;
private checkerPath: Path2D;
public constructor(config?: GridConfig) {
super(config);
this._strokeFunc = context => {
if (!this.path) this.recalculate();
context.stroke(this.path);
if (this.subdivision()) {
const offset = this.gridSize() / 2;
const dash = offset / 8;
context.setLineDash([
0,
dash / 2,
dash,
dash,
dash,
dash,
dash,
dash,
dash / 2,
]);
context.translate(offset, offset);
context.stroke(this.path);
}
};
this._fillFunc = context => {
if (!this.checkerPath) this.recalculate();
const size = this.size();
context._context.clip(
CanvasHelper.roundRectPath(
new Path2D(),
size.width / -2,
size.height / -2,
size.width,
size.height,
8,
),
);
context.fill(this.checkerPath);
};
}
private recalculate() {
this.path = new Path2D();
this.checkerPath = new Path2D();
let gridSize = this.gridSize();
if (gridSize < 1) {
console.warn('Too small grid size: ', gridSize);
gridSize = 1;
}
const size = this.getSize();
size.width = size.width / 2 + gridSize;
size.height = size.height / 2 + gridSize;
let i = 0;
for (let x = -size.width; x <= size.width; x += gridSize) {
this.path.moveTo(x, -size.height);
this.path.lineTo(x, size.height);
for (
let y = -size.height + (i % 2 ? 0 : gridSize);
y <= size.height;
y += gridSize * 2
) {
this.checkerPath.rect(x, y, gridSize, gridSize);
this.checkerPath.rect(x, y, gridSize, gridSize);
}
i++;
}
for (let y = -size.height; y <= size.height; y += gridSize) {
this.path.moveTo(-size.width, y);
this.path.lineTo(size.width, y);
}
}
public _sceneFunc(context: Context) {
if (this.checker()) {
context.fillShape(this);
} else {
context.strokeShape(this);
}
}
}

View File

@@ -1,72 +0,0 @@
import {KonvaNode} from '../decorators';
import {Context} from 'konva/lib/Context';
import {Shape, ShapeConfig} from 'konva/lib/Shape';
const FILL = [
new Path2D(
'M16.56,8.94L7.62,0L6.21,1.41l2.38,2.38L3.44,8.94c-0.59,0.59-0.59,1.54,0,2.12l5.5,5.5C9.23,16.85,9.62,17,10,17 s0.77-0.15,1.06-0.44l5.5-5.5C17.15,10.48,17.15,9.53,16.56,8.94z M5.21,10L10,5.21L14.79,10H5.21z M19,11.5c0,0-2,2.17-2,3.5 c0,1.1,0.9,2,2,2s2-0.9,2-2C21,13.67,19,11.5,19,11.5z M2',
),
];
const BRUSH = [
new Path2D(
'M7 14c-1.66 0-3 1.34-3 3 0 1.31-1.16 2-2 2 .92 1.22 2.49 2 4 2 2.21 0 4-1.79 4-4 0-1.66-1.34-3-3-3zm13.71-9.37l-1.34-1.34c-.39-.39-1.02-.39-1.41 0L9 12.25 11.75 15l8.96-8.96c.39-.39.39-1.02 0-1.41z',
),
];
const UNITY = [
new Path2D(
'M 46.523438 17.292969 L 61.820313 26.125 C 62.375 26.429688 62.386719 27.296875 61.820313 27.605469 L 43.636719 38.101563 C 43.089844 38.417969 42.4375 38.394531 41.921875 38.101563 L 23.742188 27.605469 C 23.1875 27.300781 23.171875 26.429688 23.742188 26.121094 L 39.039063 17.292969 L 39.039063 0 L 0 22.539063 L 0 67.617188 L 0 67.410156 L 0 67.617188 L 14.972656 58.972656 L 14.972656 41.308594 C 14.964844 40.675781 15.707031 40.230469 16.257813 40.570313 L 34.4375 51.070313 C 34.988281 51.386719 35.292969 51.960938 35.292969 52.554688 L 35.292969 73.546875 C 35.308594 74.175781 34.566406 74.625 34.019531 74.289063 L 18.71875 65.457031 L 3.742188 74.101563 L 42.78125 96.640625 L 81.820313 74.101563 L 66.84375 65.457031 L 51.550781 74.289063 C 51.007813 74.613281 50.25 74.191406 50.269531 73.546875 L 50.269531 52.550781 C 50.269531 51.917969 50.613281 51.363281 51.125 51.066406 L 69.304688 40.570313 C 69.847656 40.242188 70.609375 40.664063 70.589844 41.3125 L 70.589844 58.972656 L 85.5625 67.617188 L 85.5625 22.539063 L 46.523438 0 L 46.523438 17.292969 ',
),
];
const OBJECT = [
new Path2D(
'M49.71,20.94c-.36-1.31-1.05-2.51-2-3.48-.5-.51-1.07-.95-1.71-1.32l-12-6.92c-1.24-.72-2.62-1.08-4-1.08s-2.76,.36-4,1.08l-12,6.92c-.64,.37-1.21,.81-1.71,1.32-.95,.97-1.64,2.17-2,3.48-.19,.68-.29,1.4-.29,2.13v13.86c0,2.86,1.52,5.5,4,6.93l12,6.92c.64,.37,1.31,.64,2,.82,.65,.17,1.33,.26,2,.26s1.35-.09,2-.26c.69-.18,1.36-.45,2-.82l12-6.92c2.48-1.43,4-4.07,4-6.93v-13.86c0-.73-.1-1.45-.29-2.13ZM28,12.68c.61-.35,1.3-.54,2-.54s1.39,.19,2,.54l12,6.93-14,8.08-14-8.08,12-6.93Zm-12,27.71c-1.23-.71-2-2.04-2-3.46v-13.85l14,8.07v16.17l-12-6.93Zm30-3.46c0,1.42-.77,2.75-2,3.46l-12,6.93V31.15l14-8.08v13.86Z',
),
];
export enum IconType {
Fill,
Brush,
Unity,
Object,
}
interface IconConfig extends ShapeConfig {
type?: IconType;
}
@KonvaNode({centroid: false})
export class Icon extends Shape {
private readonly paths: Path2D[];
public constructor(config?: IconConfig) {
super(config);
switch (config?.type ?? IconType.Fill) {
case IconType.Brush:
this.paths = BRUSH;
break;
case IconType.Fill:
this.paths = FILL;
break;
case IconType.Unity:
this.paths = UNITY;
break;
case IconType.Object:
this.paths = OBJECT;
break;
}
this._fillFunc = context => {
for (const path of this.paths) {
context.fill(path);
}
};
}
public _sceneFunc(context: Context) {
context.fillShape(this);
}
}

View File

@@ -1,8 +0,0 @@
import {Size} from '../types';
import {Group} from 'konva/lib/Group';
export class LayeredLayout extends Group {
public getLayoutSize(): Size {
return this.getChildrenRect({skipTransform: true});
}
}

View File

@@ -1,137 +0,0 @@
import {Text, TextConfig} from 'konva/lib/shapes/Text';
import {GetSet, IRect, Vector2d} from 'konva/lib/types';
import {ShapeGetClientRectConfig} from 'konva/lib/Shape';
import {Origin} from '@motion-canvas/core/lib/types';
import {getOriginOffset} from '../types';
import {Size, Spacing} from '../types';
import {
Animator,
tween,
textLerp,
TimingFunction,
} from '@motion-canvas/core/lib/tweening';
import {threadable} from '@motion-canvas/core/lib/decorators';
import {getset} from '../decorators';
export interface LayoutTextConfig extends TextConfig {
minWidth?: number;
}
export class LayoutText extends Text {
@getset('', undefined, LayoutText.prototype.textTween)
public text: GetSet<LayoutTextConfig['text'], this>;
private overrideWidth: number | null = null;
private isConstructed = false;
public constructor(config?: LayoutTextConfig) {
super({
padd: new Spacing(30),
align: 'center',
verticalAlign: 'middle',
fontSize: 28,
fontFamily: 'JetBrains Mono',
fill: 'rgba(30, 30, 30, 0.87)',
...config,
});
this.isConstructed = true;
}
public getLayoutSize(custom?: LayoutTextConfig): Size {
const padding = this.getPadd();
let size: Size;
if (custom?.text) {
const text = this.text();
this.text(custom.text);
size = {
width: this.textWidth,
height: this.textHeight,
};
this.text(text);
} else {
size = {
width: this.textWidth,
height: this.textHeight,
};
}
return {
width: Math.max(
custom?.minWidth ?? this.getMinWidth(),
this.overrideWidth ?? size.width + padding.x,
),
height: this.height() + padding.y,
};
}
public setMinWidth(value: number): this {
this.attrs.minWidth = value;
this.markDirty();
return this;
}
public getMinWidth(): number {
return this.attrs.minWidth ?? 0;
}
public setText(text: string): this {
super.setText(text);
this.markDirty();
return this;
}
public getOriginOffset(custom?: LayoutTextConfig): Vector2d {
const padding = this.getPadd();
const size = this.getLayoutSize({minWidth: 0, ...custom});
const offset = getOriginOffset(size, custom?.origin ?? this.getOrigin());
offset.x += size.width / 2 - padding.left;
offset.y += size.height / 2 - padding.top;
return offset;
}
public get animate(): Animator<string, this> {
return new Animator<string, this>(this, 'text', this.textTween);
}
@threadable()
private *textTween(
fromText: string,
text: string,
time: number,
timingFunction: TimingFunction,
onEnd: Callback,
) {
const fromWidth = this.getLayoutSize({text: fromText, minWidth: 0}).width;
const toWidth = this.getLayoutSize({text, minWidth: 0}).width;
this.overrideWidth = fromWidth;
yield* tween(time, value => {
this.overrideWidth = timingFunction(value, fromWidth, toWidth);
this.setText(textLerp(fromText, text, timingFunction(value)));
});
this.overrideWidth = null;
this.setText(text);
onEnd();
}
public getClientRect(config?: ShapeGetClientRectConfig): IRect {
const realSize = this.getLayoutSize({minWidth: 0});
const size = this.getLayoutSize();
const offset = this.getOriginOffset({origin: Origin.TopLeft});
const rect: IRect = {
x: offset.x + (realSize.width - size.width) / 2,
y: offset.y,
width: size.width,
height: size.height,
};
if (!config?.skipTransform) {
return this._transformedRect(rect, config?.relativeTo);
}
return rect;
}
}

View File

@@ -1,95 +0,0 @@
import {Group} from 'konva/lib/Group';
import {GetSet} from 'konva/lib/types';
import {Shape} from 'konva/lib/Shape';
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';
export interface LinearLayoutConfig extends ContainerConfig {
direction?: Center;
}
@KonvaNode()
export class LinearLayout extends Group {
@getset(Center.Vertical, Node.prototype.markDirty)
public direction: GetSet<Center, this>;
private contentSize: Size;
public constructor(config?: LinearLayoutConfig) {
super(config);
}
public getLayoutSize(): Size {
return this.getPadd().expand({
width: this.contentSize?.width ?? 0,
height: this.contentSize?.height ?? 0,
});
}
//TODO Recalculate upon removing children as well.
public add(...children: (Group | Shape)[]): this {
super.add(...children);
this.recalculateLayout();
return this;
}
public recalculateLayout() {
if (!this.children) return;
const direction = this.direction();
this.contentSize = {width: 0, height: 0};
for (const child of this.children) {
const size = child.getLayoutSize();
const margin = child.getMargin();
const scale = child.getAbsoluteScale(this);
const boxSize = {
x: (size.width + margin.x) * scale.x,
y: (size.height + margin.y) * scale.y,
};
if (direction === Center.Vertical) {
this.contentSize.width = Math.max(this.contentSize.width, boxSize.x);
this.contentSize.height += boxSize.y;
} else {
this.contentSize.height = Math.max(this.contentSize.height, boxSize.y);
this.contentSize.width += boxSize.x;
}
}
let length =
direction === Center.Vertical
? this.contentSize.height / -2
: this.contentSize.width / -2;
for (const child of this.children) {
const size = child.getLayoutSize();
const margin = child.getMargin();
const scale = child.getAbsoluteScale(this);
const offset = child.getOriginDelta(Origin.TopLeft);
const parentOffset = getOriginOffset(
margin.shrink(this.contentSize),
child.origin(),
);
if (direction === Center.Vertical) {
child.position({
x: parentOffset.x,
y: length + (-offset.y + margin.top) * scale.y,
});
length += (size.height + margin.y) * scale.y;
} else {
child.position({
x: length + (-offset.x + margin.left) * scale.x,
y: parentOffset.y,
});
length += (size.width + margin.x) * scale.x;
}
}
super.recalculateLayout();
}
}

View File

@@ -1,64 +0,0 @@
import {Group} from 'konva/lib/Group';
import {Container, ContainerConfig} from 'konva/lib/Container';
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';
import {useKonvaView} from '../scenes';
export interface PinConfig extends ContainerConfig {
target?: Node;
attach?: Node;
direction?: Center;
}
@KonvaNode()
export class Pin extends Group {
@getset(null, Node.prototype.markDirty)
public target: GetSet<PinConfig['target'], this>;
@getset(null, Node.prototype.markDirty)
public attach: GetSet<PinConfig['attach'], this>;
@getset(null, Pin.prototype.markDirty)
public direction: GetSet<PinConfig['direction'], this>;
public constructor(config?: PinConfig) {
super(config);
}
public getDirection(): Center {
return (
this.attrs.direction ??
(this.attach() ? Center.Vertical : Center.Horizontal)
);
}
public isDirty(): boolean {
return super.isDirty() || this.target()?.wasDirty();
}
public recalculateLayout() {
const attach = this.attach();
if (attach) {
const attachDirection = flipOrigin(attach.getOrigin(), this.direction());
const rect = this.getClientRect({
relativeTo: useKonvaView(),
});
const offset = getOriginDelta(rect, Origin.TopLeft, attachDirection);
attach.position({
x: rect.x + offset.x,
y: rect.y + offset.y,
});
}
super.recalculateLayout();
}
public getClientRect(config?: {
skipTransform?: boolean;
skipShadow?: boolean;
skipStroke?: boolean;
relativeTo?: Container;
}): IRect {
return this.target()?.getClientRect(config) ?? super.getClientRect(config);
}
}

View File

@@ -1,34 +0,0 @@
import {Reference, useRef} from '@motion-canvas/core/lib/utils';
import {Node} from 'konva/lib/Node';
import {LayoutText, LayoutTextConfig} from './LayoutText';
import {Pin} from './Pin';
import {Center, Origin} from '@motion-canvas/core/lib/types';
interface PinnedLabelConfig extends LayoutTextConfig {
children: string;
target: Reference<Node>;
ref?: Reference<LayoutText>;
direction?: Center;
}
export function PinnedLabel(config: PinnedLabelConfig) {
const {children, target, ref, direction, ...rest} = config;
const reference = ref ?? useRef<LayoutText>();
return (
<>
<LayoutText
ref={reference}
text={children}
padd={[30, 0]}
fill={'rgba(255, 255, 255, 0.54'}
origin={Origin.BottomLeft}
{...rest}
/>
<Pin
target={target.value}
attach={reference.value}
direction={direction ?? Center.Vertical}
/>
</>
);
}

View File

@@ -1,109 +0,0 @@
import {Context} from 'konva/lib/Context';
import {KonvaNode, getset} from '../decorators';
import {CanvasHelper} from '../helpers';
import {GetSet} from 'konva/lib/types';
import {getFontColor, getStyle, Style} from '../styles';
import {remap} from '@motion-canvas/core/lib/tweening';
import {Shape, ShapeConfig} from 'konva/lib/Shape';
export interface RangeConfig extends ShapeConfig {
range?: [number, number];
value?: number;
precision?: number;
label?: string;
style?: Style;
}
@KonvaNode()
export class Range extends Shape {
@getset(null)
public style: GetSet<RangeConfig['style'], this>;
@getset([0, 1])
public range: GetSet<RangeConfig['range'], this>;
@getset(0.5)
public value: GetSet<RangeConfig['value'], this>;
@getset(0)
public precision: GetSet<RangeConfig['precision'], this>;
@getset(null)
public label: GetSet<RangeConfig['label'], this>;
public constructor(config?: RangeConfig) {
super(config);
}
public _sceneFunc(context: Context) {
const style = getStyle(this);
const ctx = context._context;
const size = this.getSize();
const value = this.value();
const range = this.range();
const precision = this.precision();
const label = this.label();
const text = value.toLocaleString('en-EN', {
minimumFractionDigits: precision,
maximumFractionDigits: precision,
});
const minText = range[0].toLocaleString('en-EN', {
minimumFractionDigits: precision,
maximumFractionDigits: precision,
});
const position = {
x: size.width / -2,
y: size.height / -2,
};
if (label) {
position.x += 60;
size.width -= 60;
}
ctx.fillStyle = style.backgroundLight;
CanvasHelper.roundRect(
ctx,
position.x,
position.y,
size.width,
size.height,
8,
);
ctx.fill();
ctx.font = style.bodyFont;
const textSize = ctx.measureText(minText);
const width = remap(
range[0],
range[1],
textSize.width + 40,
size.width,
value,
);
ctx.fillStyle = style.foreground;
CanvasHelper.roundRect(ctx, position.x, position.y, width, size.height, 8);
ctx.fill();
ctx.font = style.bodyFont;
ctx.fillStyle = getFontColor(style.foreground);
ctx.fillText(text, position.x + 20, 10);
if (label) {
ctx.fillStyle = getFontColor(style.backgroundLight);
ctx.fillText(label, position.x - 60, 10);
}
}
public _hitFunc(context: Context) {
const size = this.getSize();
const position = {
x: size.width / -2,
y: size.height / -2,
};
context.beginPath();
context.rect(position.x, position.y, size.width, size.height);
context.closePath();
context.fillShape(this);
}
}

View File

@@ -1,362 +0,0 @@
import {Context} from 'konva/lib/Context';
import {GetSet, Vector2d} from 'konva/lib/types';
import {waitFor} from '@motion-canvas/core/lib/flow';
import {threadable} from '@motion-canvas/core/lib/decorators';
import {cached, KonvaNode, getset} from '../decorators';
import {GeneratorHelper} from '@motion-canvas/core/lib/helpers';
import {TimingFunction, map, tween} from '@motion-canvas/core/lib/tweening';
import {cancel, ThreadGenerator} from '@motion-canvas/core/lib/threading';
import {parseColor} from 'mix-color';
import {Shape, ShapeConfig} from 'konva/lib/Shape';
import {getImageData, ImageDataSource} from '@motion-canvas/core/lib/media';
export const SPRITE_CHANGE_EVENT = 'spriteChange';
export interface SpriteConfig extends ShapeConfig {
animation: ImageDataSource[];
skin?: ImageDataSource;
mask?: ImageDataSource;
playing?: boolean;
fps?: number;
maskBlend?: number;
frame?: number;
}
/**
* A class for animated sprites.
*
* @remarks
* Allows to use custom alpha masks and skins.
*/
@KonvaNode()
export class Sprite extends Shape {
@getset(null, Sprite.prototype.updateAnimation)
public animation: GetSet<SpriteConfig['animation'], this>;
@getset(0, Sprite.prototype.updateAnimation)
public frame: GetSet<SpriteConfig['frame'], this>;
@getset(false)
public playing: GetSet<SpriteConfig['playing'], this>;
@getset(10)
public fps: GetSet<SpriteConfig['fps'], this>;
/**
* An image used for 2D UV mapping.
*/
@getset(null, Sprite.prototype.updateSkin)
public skin: GetSet<SpriteConfig['skin'], this>;
/**
* An image that defines which parts of the sprite should be visible.
*
* @remarks
* The red channel is used for sampling.
*/
@getset(null, Sprite.prototype.updateMask, Sprite.prototype.maskTween)
public mask: GetSet<SpriteConfig['mask'], this>;
/**
* The blend between the original opacity and the opacity calculated using the
* mask.
*/
@getset(0, Sprite.prototype.updateFrame)
public maskBlend: GetSet<SpriteConfig['maskBlend'], this>;
private task: ThreadGenerator | null = null;
private baseMask: ImageDataSource;
private baseMaskBlend = 0;
private synced = false;
private readonly computeCanvas: HTMLCanvasElement;
private readonly context: CanvasRenderingContext2D;
public constructor(config?: SpriteConfig) {
super(config);
this.computeCanvas = document.createElement('canvas');
this.context = this.computeCanvas.getContext('2d');
}
protected _sceneFunc(context: Context) {
const size = this.getSize();
let source: ImageDataSource = this.computeCanvas;
if (this.requiresProcessing()) {
// Make sure the compute canvas is up-to-date
this.getFrameData();
} else {
const animation = this.animation();
const frame = this.frame();
source = animation[frame % animation.length];
}
context.save();
context._context.imageSmoothingEnabled = false;
context.drawImage(
source,
0,
0,
source.width,
source.height,
size.width / -2,
size.height / -2,
size.width,
size.height,
);
context.restore();
}
private updateSkin() {
this._clearCache(this.getSkinData);
this.updateFrame();
}
private updateAnimation() {
this._clearCache(this.getRawFrameData);
this.updateFrame();
}
private updateMask() {
this._clearCache(this.getMaskData);
this.updateFrame();
}
private updateFrame() {
this._clearCache(this.getFrameData);
}
@cached('Sprite.skinData')
private getSkinData() {
const skin = this.skin();
return skin ? getImageData(skin) : null;
}
@cached('Sprite.maskData')
private getMaskData() {
const mask = this.mask();
return mask ? getImageData(mask) : null;
}
@cached('Sprite.baseMaskData')
private getBaseMaskData() {
return this.baseMask ? getImageData(this.baseMask) : null;
}
@cached('Sprite.rawFrameData')
private getRawFrameData() {
const animation = this.animation();
const frameId = this.frame() % animation.length;
return getImageData(animation[frameId]);
}
@cached('Sprite.frameData')
private getFrameData() {
if (!this.requiresProcessing()) {
return this.getRawFrameData();
}
const skin = this.skin();
const mask = this.mask();
const blend = this.maskBlend();
const rawFrameData = this.getRawFrameData();
const frameData = this.context.createImageData(rawFrameData);
this.computeCanvas.width = rawFrameData.width;
this.computeCanvas.height = rawFrameData.height;
if (skin) {
const skinData = this.getSkinData();
for (let y = 0; y < rawFrameData.height; y++) {
for (let x = 0; x < rawFrameData.width; x++) {
const id = this.positionToId({x, y});
const skinX = rawFrameData.data[id];
const skinY = rawFrameData.data[id + 1];
const skinId = ((skin.height - 1 - skinY) * skin.width + skinX) * 4;
frameData.data[id] = skinData.data[skinId];
frameData.data[id + 1] = skinData.data[skinId + 1];
frameData.data[id + 2] = skinData.data[skinId + 2];
frameData.data[id + 3] = Math.round(
(rawFrameData.data[id + 3] / 255) *
(skinData.data[skinId + 3] / 255) *
255,
);
}
}
} else {
frameData.data.set(rawFrameData.data);
}
if (mask || this.baseMask) {
const maskData = this.getMaskData();
const baseMaskData = this.getBaseMaskData();
for (let y = 0; y < rawFrameData.height; y++) {
for (let x = 0; x < rawFrameData.width; x++) {
const id = this.positionToId({x, y});
const maskValue = map(
maskData?.data[id] ?? 255,
baseMaskData?.data[id] ?? 255,
this.baseMaskBlend,
);
frameData.data[id + 3] *= map(1, maskValue / 255, blend);
}
}
}
this.context.putImageData(frameData, 0, 0);
this.fire(SPRITE_CHANGE_EVENT);
return frameData;
}
private requiresProcessing(): boolean {
return !!(this.skin() || this.mask() || this.baseMask);
}
/**
* A generator that runs this sprite's animation.
*
* @remarks
* For the animation to actually play, the {@link Sprite.playing} value has to
* be set to `true`.
*
* Should be run concurrently:
* ```ts
* yield sprite.play();
* ```
*/
public play(): ThreadGenerator {
const runTask = this.playRunner();
if (this.task) {
const previousTask = this.task;
this.task = (function* (): ThreadGenerator {
cancel(previousTask);
yield* runTask;
})();
GeneratorHelper.makeThreadable(this.task, runTask);
} else {
this.task = runTask;
}
return this.task;
}
/**
* Play the given animation once.
*
* @param animation - The animation to play.
* @param next - An optional animation that should be switched to next. If not
* present the sprite will go back to the previous animation.
*/
@threadable()
public *playOnce(
animation: ImageDataSource[],
next: ImageDataSource[] = null,
): ThreadGenerator {
next ??= this.animation();
this.animation(animation);
for (let i = 0; i < animation.length; i++) {
this.frame(i);
yield* waitFor(1 / this.fps());
}
this.animation(next);
}
/**
* Cancel the current {@link Sprite.play} generator.
*
* @remarks
* Should be used instead of manually canceling the generator.
*/
@threadable()
public *stop() {
if (this.task) {
cancel(this.task);
this.task = null;
}
}
@threadable('spriteAnimationRunner')
private *playRunner(): ThreadGenerator {
this.frame(0);
while (this.task !== null) {
if (this.playing()) {
this.synced = true;
this.frame(this.frame() + 1);
} else {
this.synced = false;
}
yield* waitFor(1 / this.fps());
}
}
/**
* Wait until the given frame is shown.
*
* @param frame - The frame to wait for.
*/
@threadable()
public *waitForFrame(frame: number): ThreadGenerator {
let limit = 1000;
while (
this.frame() % this.animation().length !== frame &&
limit > 0 &&
!this.synced
) {
limit--;
yield;
}
if (limit === 0) {
console.warn(`Sprite.waitForFrame cancelled`);
}
}
@threadable()
private *maskTween(
from: ImageDataSource,
to: ImageDataSource,
time: number,
timingFunction: TimingFunction,
onEnd: () => void,
): ThreadGenerator {
this.baseMask = from;
this._clearCache(this.getBaseMaskData);
this.baseMaskBlend = 1;
this.mask(to);
yield* tween(time, value => {
this.baseMaskBlend = timingFunction(1 - value);
this.updateFrame();
});
this.baseMask = null;
this.baseMaskBlend = 0;
onEnd();
}
public getColorAt(position: Vector2d): string {
const id = this.positionToId(position);
const frameData = this.getFrameData();
return `rgba(${frameData.data[id]
.toString()
.padStart(3, ' ')}, ${frameData.data[id + 1]
.toString()
.padStart(3, ' ')}, ${frameData.data[id + 2]
.toString()
.padStart(3, ' ')}, ${frameData.data[id + 3] / 255})`;
}
public getParsedColorAt(position: Vector2d): ReturnType<typeof parseColor> {
const id = this.positionToId(position);
const frameData = this.getFrameData();
return {
r: frameData.data[id],
g: frameData.data[id + 1],
b: frameData.data[id + 2],
a: frameData.data[id + 3],
};
}
public positionToId(position: Vector2d): number {
const frameData = this.getRawFrameData();
return (position.y * frameData.width + position.x) * 4;
}
}

View File

@@ -1,287 +0,0 @@
import {ContainerConfig} from 'konva/lib/Container';
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';
import {threadable} from '@motion-canvas/core/lib/decorators';
import {KonvaNode, getset} from '../decorators';
import {Node} from 'konva/lib/Node';
import {Reference} from '@motion-canvas/core/lib/utils';
import {Group} from 'konva/lib/Group';
import {Shape} from 'konva/lib/Shape';
import {Canvas} from 'konva/lib/Canvas';
export interface SurfaceMask {
width: number;
height: number;
radius: number;
color: string;
}
export interface CircleMask {
radius: number;
x: number;
y: number;
}
export interface SurfaceConfig extends ContainerConfig {
ref?: Reference<Surface>;
radius?: number;
circleMask?: CircleMask;
background?: string;
child?: Node;
rescaleChild?: boolean;
shadow?: boolean;
overflow?: boolean;
}
@KonvaNode()
export class Surface extends Group {
@getset(8)
public radius: GetSet<SurfaceConfig['radius'], this>;
@getset('#00000000')
public background: GetSet<SurfaceConfig['background'], this>;
@getset(null)
public child: GetSet<SurfaceConfig['child'], this>;
@getset(true)
public rescaleChild: GetSet<SurfaceConfig['rescaleChild'], this>;
@getset(false)
public shadow: GetSet<SurfaceConfig['shadow'], this>;
@getset(false)
public overflow: GetSet<SurfaceConfig['overflow'], this>;
private surfaceMask: SurfaceMask | null = null;
private circleMask: CircleMask | null = null;
private layoutData: Size;
private rippleTween = 0;
public constructor(config?: SurfaceConfig) {
super(config);
this.markDirty();
}
public setChild(value: Shape | Group): this {
this.attrs.child?.remove();
this.attrs.child = value;
if (value) {
this.add(value);
}
this.markDirty();
return this;
}
public getChild<T extends Node>(): T {
return <T>this.attrs.child;
}
public setCircleMask(value: CircleMask | null): this {
this.circleMask = value;
return this;
}
public getCircleMask(): CircleMask | null {
return this.circleMask ?? null;
}
public clone(obj?: unknown): this {
const child = this.child();
this.child(null);
const clone: this = Node.prototype.clone.call(this, obj);
this.child(child);
if (child) {
clone.setChild(child.clone());
}
this.getChildren().forEach(node => {
if (node !== child) {
clone.add(node.clone());
}
});
return clone;
}
public getLayoutSize(): Size {
return {
width: this.surfaceMask?.width ?? this.layoutData?.width ?? 0,
height: this.surfaceMask?.height ?? this.layoutData?.height ?? 0,
};
}
/**
* @deprecated Use {@link ripple} instead.
*/
public doRipple() {
return this.ripple();
}
@threadable()
public *ripple() {
if (this.surfaceMask) return;
yield* tween(1, value => {
this.rippleTween = value;
});
this.rippleTween = 0;
}
public setMask(data: SurfaceMask) {
if (data === null) {
this.surfaceMask = null;
this.markDirty();
return;
}
const child = this.child();
const contentSize = child.getLayoutSize();
const contentMargin = child.getMargin();
const scale = Math.min(
1,
data.width / (contentSize.width + contentMargin.x),
);
this.surfaceMask = data;
if (this.rescaleChild()) {
child.scaleX(scale);
child.scaleY(scale);
}
child.position(getOriginDelta(data, Origin.Middle, child.getOrigin()));
this.markDirty();
}
public getMask(): SurfaceMask {
return {
...this.layoutData,
radius: this.radius(),
color: this.background(),
};
}
public recalculateLayout() {
if (this.surfaceMask) return;
this.layoutData ??= {
width: 0,
height: 0,
};
const child = this.child();
if (child) {
const size = child.getLayoutSize();
const margin = child.getMargin();
const scale = child.getAbsoluteScale(this);
const padding = this.getPadd();
this.layoutData = {
width: (size.width + margin.x + padding.x) * scale.x,
height: (size.height + margin.y + padding.y) * scale.y,
};
const offset = child.getOriginDelta(Origin.Middle);
child.position({
x: -offset.x,
y: -offset.y,
});
}
super.recalculateLayout();
}
public _drawChildren(drawMethod: string, canvas: Canvas, top: Node): void {
const context = canvas && canvas.getContext();
context.save();
const transform = this.getAbsoluteTransform(top);
let m = transform.getMatrix();
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
const opacity = this.getAbsoluteOpacity();
const size = this.surfaceMask ?? this.layoutData;
const radius = this.surfaceMask?.radius ?? this.radius();
if (this.rippleTween > 0) {
const width = size.width + easeOutExpo(this.rippleTween, 0, 100);
const height = size.height + easeOutExpo(this.rippleTween, 0, 100);
const rippleRadius = radius + easeOutExpo(this.rippleTween, 0, 50);
context.save();
context._context.fillStyle = this.surfaceMask?.color ?? this.background();
context._context.globalAlpha = linear(this.rippleTween, opacity / 2, 0);
CanvasHelper.roundRect(
context._context,
-width / 2,
-height / 2,
width,
height,
rippleRadius,
);
context._context.fill();
context.restore();
}
if (this.circleMask) {
context._context.beginPath();
context._context.arc(
this.circleMask.x,
this.circleMask.y,
this.circleMask.radius,
0,
Math.PI * 2,
);
context._context.closePath();
context._context.clip();
}
context.save();
context._context.fillStyle = this.surfaceMask?.color ?? this.background();
context._context.globalAlpha = opacity;
if (this.shadow()) {
context._context.shadowColor = 'rgba(0, 0, 0, 0.32)';
context._context.shadowOffsetY = 10;
context._context.shadowBlur = 40;
}
const path = CanvasHelper.roundRectPath(
new Path2D(),
-size.width / 2,
-size.height / 2,
size.width,
size.height,
radius,
);
context._context.fill(path);
context.restore();
if (!this.overflow() || this.surfaceMask) {
context._context.clip(path);
}
m = transform.copy().invert().getMatrix();
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
super._drawChildren(drawMethod, canvas, top);
context.restore();
}
public getAbsoluteCircleMask(custom?: SurfaceConfig): CircleMask {
const mask = custom?.circleMask ?? this.circleMask ?? null;
if (mask === null) return null;
const size = this.getLayoutSize();
const position = {
x: (size.width * mask.x) / 2,
y: (size.height * mask.y) / 2,
};
const farthestEdge = {
x: Math.abs(position.x) + size.width / 2,
y: Math.abs(position.y) + size.height / 2,
};
const distance = Math.sqrt(
farthestEdge.x * farthestEdge.x + farthestEdge.y * farthestEdge.y,
);
return {
...position,
radius: distance * mask.radius,
};
}
}

View File

@@ -1,162 +0,0 @@
import {PossibleSpacing, Size} from '../types';
import {Util} from 'konva/lib/Util';
import {Context} from 'konva/lib/Context';
import * as THREE from 'three';
import {CanvasHelper} from '../helpers';
import {GetSet} from 'konva/lib/types';
import {KonvaNode, getset} from '../decorators';
import {Shape, ShapeConfig} from 'konva/lib/Shape';
export interface ThreeViewConfig extends ShapeConfig {
canvasSize: Size;
cameraScale?: number;
quality?: number;
skipFrames?: number;
scene?: THREE.Scene;
camera?: THREE.Camera;
radius?: PossibleSpacing;
background?: string;
}
interface Pool<T> {
borrow(): T;
dispose(object: T): void;
}
class RendererPool implements Pool<THREE.WebGLRenderer> {
private pool: THREE.WebGLRenderer[] = [];
public borrow(): THREE.WebGLRenderer {
if (this.pool.length) {
return this.pool.pop();
} else {
return new THREE.WebGLRenderer({
canvas: Util.createCanvasElement(),
antialias: true,
});
}
}
public dispose(renderer: THREE.WebGLRenderer) {
this.pool.push(renderer);
}
}
const rendererPool = new RendererPool();
@KonvaNode()
export class ThreeView extends Shape {
@getset(null)
public scene: GetSet<ThreeViewConfig['scene'], this>;
@getset(null)
public camera: GetSet<ThreeViewConfig['camera'], this>;
@getset({width: 0, height: 0}, ThreeView.prototype.handleCanvasSizeChange)
public canvasSize: GetSet<ThreeViewConfig['canvasSize'], this>;
@getset(1, ThreeView.prototype.handleCanvasSizeChange)
public cameraScale: GetSet<ThreeViewConfig['cameraScale'], this>;
@getset(1, ThreeView.prototype.handleCanvasSizeChange)
public quality: GetSet<ThreeViewConfig['quality'], this>;
@getset(0)
public skipFrames: GetSet<ThreeViewConfig['skipFrames'], this>;
@getset(0)
public radius: GetSet<ThreeViewConfig['radius'], this>;
private readonly renderer: THREE.WebGLRenderer;
private readonly context: WebGLRenderingContext;
private renderedFrames = 0;
public constructor(config?: ThreeViewConfig) {
super(config);
this.renderer = rendererPool.borrow();
this.context = this.renderer.getContext();
this.handleCanvasSizeChange();
}
public setBackground(value: string): this {
const scene = this.scene();
if (scene) {
scene.background = new THREE.Color(value);
}
return this;
}
public getBackground(): string {
const background = this.scene()?.background;
return background instanceof THREE.Color
? background.getHexString()
: '#000000';
}
public destroy(): this {
rendererPool.dispose(this.renderer);
return super.destroy();
}
private handleCanvasSizeChange() {
if (!this.renderer) return;
const size = {...this.canvasSize()};
const camera = this.camera();
const ratio = size.width / size.height;
const scale = this.cameraScale() / 2;
if (camera instanceof THREE.OrthographicCamera) {
camera.left = -ratio * scale;
camera.right = ratio * scale;
camera.bottom = -scale;
camera.top = scale;
camera.updateProjectionMatrix();
} else if (camera instanceof THREE.PerspectiveCamera) {
camera.aspect = ratio;
camera.updateProjectionMatrix();
}
size.width *= this.quality();
size.height *= this.quality();
this.renderer.setSize(size.width, size.height);
this.markDirty();
}
public getLayoutSize(): Size {
return this.canvasSize();
}
public _sceneFunc(context: Context) {
const scale = this.quality();
const size = {...this.canvasSize()};
if (this.renderedFrames < 1) {
this.renderedFrames = this.skipFrames();
this.renderer.render(this.scene(), this.camera());
} else {
this.renderedFrames--;
}
context._context.imageSmoothingEnabled = false;
context._context.clip(
CanvasHelper.roundRectPath(
new Path2D(),
size.width / -2,
size.height / -2,
size.width,
size.height,
this.radius(),
),
);
context._context.drawImage(
this.renderer.domElement,
0,
0,
size.width * scale,
size.height * scale,
size.width / -2,
size.height / -2,
size.width,
size.height,
);
}
}

View File

@@ -1,90 +0,0 @@
import {Shape, ShapeConfig} from 'konva/lib/Shape';
import {threadable} from '@motion-canvas/core/lib/decorators';
import {getset} from '../decorators';
import {Context} from 'konva/lib/Context';
import {GetSet} from 'konva/lib/types';
import {cancel, ThreadGenerator} from '@motion-canvas/core/lib/threading';
import {waitFor} from '@motion-canvas/core/lib/flow';
import {GeneratorHelper} from '@motion-canvas/core/lib/helpers';
import {CanvasHelper} from '../helpers';
interface VideoConfig extends ShapeConfig {
frames: ImageBitmap[];
frame?: number;
fps?: number;
playing?: number;
radius?: number;
}
export class Video extends Shape {
@getset([])
public frames: GetSet<VideoConfig['frames'], this>;
@getset(0)
public frame: GetSet<VideoConfig['frame'], this>;
@getset(30)
public fps: GetSet<VideoConfig['fps'], this>;
@getset(true)
public playing: GetSet<VideoConfig['playing'], this>;
@getset(8)
public radius: GetSet<VideoConfig['radius'], this>;
private task: ThreadGenerator | null = null;
public constructor(config?: VideoConfig) {
super(config);
}
public _sceneFunc(context: Context) {
const frames = this.frames();
context._context.clip(
CanvasHelper.roundRectPath(
new Path2D(),
0,
0,
this.width(),
this.height(),
this.radius(),
),
);
if (frames.length) {
context.drawImage(frames[this.frame() % frames.length], 0, 0);
} else {
context._context.fillStyle = '#666666';
context.fillRect(0, 0, this.width(), this.height());
}
}
public play(): ThreadGenerator {
const runTask = this.playRunner();
if (this.task) {
const previousTask = this.task;
this.task = (function* (): ThreadGenerator {
cancel(previousTask);
yield* runTask;
})();
GeneratorHelper.makeThreadable(this.task, runTask);
} else {
this.task = runTask;
}
return this.task;
}
@threadable()
public *stop() {
if (this.task) {
cancel(this.task);
this.task = null;
}
}
@threadable('videoRunner')
private *playRunner(): ThreadGenerator {
while (this.task !== null) {
if (this.playing()) {
this.frame(this.frame() + 1);
}
yield* waitFor(1 / this.fps());
}
}
}

View File

@@ -1,437 +0,0 @@
import {cached, KonvaNode, getset} from '../../decorators';
import {GetSet} from 'konva/lib/types';
import PrismJS from 'prismjs';
import {Context} from 'konva/lib/Context';
import {Text, TextConfig} from 'konva/lib/shapes/Text';
import {Util} from 'konva/lib/Util';
import {easeInExpo, easeOutExpo, tween} from '@motion-canvas/core/lib/tweening';
import {CodeTheme, CodeTokens} from './CodeTheme';
import {JS_CODE_THEME} from '../../themes';
import {ThreadGenerator} from '@motion-canvas/core/lib/threading';
import {threadable} from '@motion-canvas/core/lib/decorators';
type CodePoint = [number, number];
type CodeRange = [CodePoint, CodePoint];
interface CodeConfig extends TextConfig {
selection?: CodeRange[];
theme?: CodeTheme;
numbers?: boolean | number;
language?: string;
}
const FALLBACK_COLOR = '#FF00FF';
@KonvaNode({centroid: false})
export class Code extends Text {
@getset([])
public selection: GetSet<CodeConfig['selection'], this>;
@getset(JS_CODE_THEME)
public theme: GetSet<CodeConfig['theme'], this>;
@getset(false)
public numbers: GetSet<CodeConfig['numbers'], this>;
@getset('js')
public language: GetSet<CodeConfig['language'], this>;
private readonly textCanvas: HTMLCanvasElement;
private readonly textCtx: CanvasRenderingContext2D;
private readonly selectionCanvas: HTMLCanvasElement;
private readonly selectionCtx: CanvasRenderingContext2D;
private outline = 0;
private unselectedOpacity = 1;
public constructor(config?: CodeConfig) {
super({
fontFamily: 'JetBrains Mono, monospace',
fontSize: 28,
lineHeight: 2,
...config,
});
this.textCanvas = Util.createCanvasElement();
this.textCtx = this.textCanvas.getContext('2d');
this.selectionCanvas = Util.createCanvasElement();
this.selectionCtx = this.selectionCanvas.getContext('2d');
}
public _sceneFunc(context: Context) {
const padding = this.getPadd();
context.translate(padding.left, padding.right);
this.drawSelection(context._context);
this.drawText(context._context);
if (this.numbers() !== false) {
this.drawLineNumbers(context._context);
}
}
public setText(text: string): this {
super.setText(text);
this.markDirty();
this._clearCache(this.getLines);
this._clearCache(this.getTokens);
this._clearCache(this.getNormalizedSelection);
return this;
}
public setLanguage(language: string): this {
this.attrs.language = language;
this._clearCache(this.getTokens);
return this;
}
public setSelection(value: CodeRange[]): this {
this.attrs.selection = value;
this._clearCache(this.getNormalizedSelection);
return this;
}
@cached('Code.lines')
private getLines(): string[] {
return this.text().split('\n');
}
@cached('Code.tokens')
private getTokens(): (PrismJS.Token | string)[] {
const language = this.language();
if (language in PrismJS.languages) {
const env = {
code: this.text(),
grammar: PrismJS.languages[language],
language: language,
tokens: PrismJS.tokenize(this.text(), PrismJS.languages[language]),
};
// the after-tokenize hook will update env tokens for the jsx language
PrismJS.hooks.run('after-tokenize', env);
return env.tokens;
} else {
console.warn(
`Missing language: ${language}.`,
`Make sure that 'prismjs/components/prism-${language}' has been imported.`,
);
return PrismJS.tokenize(this.text(), PrismJS.languages.plain);
}
}
@cached('Code.selection')
private getNormalizedSelection(): CodeRange[] {
const lines = this.getLines();
const normalized: CodeRange[] = [];
const selection = [...this.selection()];
for (const range of selection) {
let [[startLine, startColumn], [endLine, endColumn]] = range;
if (startLine > endLine) {
[startLine, endLine] = [endLine, startLine];
}
if (startLine === endLine && startColumn > endColumn) {
[startColumn, endColumn] = [endColumn, startColumn];
}
if (startLine >= lines.length) {
startLine = lines.length - 1;
}
if (endLine >= lines.length) {
endLine = lines.length - 1;
}
if (endColumn >= lines[endLine].length) {
endColumn = Math.max(lines[endLine].length, 1);
}
if (startLine !== endLine) {
const nextLineOffset =
startLine + 1 === endLine
? endColumn
: Math.max(1, lines[startLine + 1].length);
if (nextLineOffset <= startColumn) {
normalized.push([
[startLine + 1, 0],
[endLine, endColumn],
]);
endLine = startLine;
endColumn = lines[startLine].length;
}
}
normalized.push([
[startLine, startColumn],
[endLine, endColumn],
]);
}
return normalized;
}
private drawSelection(context: CanvasRenderingContext2D) {
const letterWidth = this.measureSize(' ').width;
const lineHeight = this.fontSize() * this.lineHeight();
const selection = this.getNormalizedSelection();
const outline = this.outline;
const lines = this.getLines();
context.beginPath();
for (const range of selection) {
const [[startLine, startColumn], [endLine, endColumn]] = range;
let offset =
startLine === endLine
? endColumn * letterWidth
: Math.max(1, lines[startLine].length) * letterWidth;
context.moveTo(
startColumn * letterWidth - outline,
(startLine + 0.5) * lineHeight,
);
context.arcTo(
startColumn * letterWidth - outline,
startLine * lineHeight - outline,
offset + outline,
startLine * lineHeight - outline,
8,
);
context.arcTo(
offset + outline,
startLine * lineHeight - outline,
offset + outline,
(startLine + 1) * lineHeight,
8,
);
for (let i = startLine + 1; i <= endLine; i++) {
const lineOffset =
Math.max(1, i === endLine ? endColumn : lines[i].length) *
letterWidth;
const linePadding = lineOffset > offset ? -outline : outline;
context.arcTo(
offset + outline,
i * lineHeight + linePadding,
lineOffset + outline,
i * lineHeight + linePadding,
8,
);
offset = lineOffset;
context.arcTo(
offset + outline,
i * lineHeight + linePadding,
offset + outline,
(i + 1) * lineHeight + linePadding,
8,
);
}
const endOffset = startLine === endLine ? startColumn * letterWidth : 0;
context.arcTo(
offset + outline,
(endLine + 1) * lineHeight + outline,
endOffset - outline,
(endLine + 1) * lineHeight + outline,
8,
);
context.arcTo(
endOffset - outline,
(endLine + 1) * lineHeight + outline,
endOffset - outline,
endLine * lineHeight + outline,
8,
);
if (startLine !== endLine) {
context.arcTo(
endOffset - outline,
(startLine + 1) * lineHeight - outline,
startColumn * letterWidth - outline,
(startLine + 1) * lineHeight - outline,
8,
);
context.arcTo(
startColumn * letterWidth - outline,
(startLine + 1) * lineHeight - outline,
startColumn * letterWidth - outline,
startLine * lineHeight - outline,
8,
);
}
context.lineTo(
startColumn * letterWidth - outline,
(startLine + 0.5) * lineHeight,
);
}
context.closePath();
context.fillStyle = '#242424';
context.globalAlpha = this.getAbsoluteOpacity();
context.fill();
}
private drawText(context: CanvasRenderingContext2D) {
const letterWidth = this.measureSize(' ').width;
const lineHeight = this.fontSize() * this.lineHeight();
const theme = this.theme();
context.font = this._getContextFont();
context.textBaseline = 'middle';
let x = 0;
let y = 0;
const draw = (token: string | PrismJS.Token, colors: CodeTokens) => {
if (typeof token === 'string') {
context.fillStyle = colors.punctuation ?? FALLBACK_COLOR;
const lines = token.split('\n');
let isFirst = true;
for (const line of lines) {
if (!isFirst) {
x = 0;
y++;
}
isFirst = false;
const trim = line.length - line.trimStart().length;
context.globalAlpha = this.getOpacityAtPoint(x + trim, y);
context.fillText(line, x * letterWidth, (y + 0.5) * lineHeight);
x += line.length;
}
} else if (
typeof token.content === 'string' &&
// FIXME Handle newlines no matter the token type
token.type !== 'plain-text'
) {
if (!(token.type in colors)) {
console.warn(`Unstyled token type:`, token.type);
}
context.fillStyle = colors[token.type] ?? FALLBACK_COLOR;
context.globalAlpha = this.getOpacityAtPoint(x, y);
context.fillText(
token.content,
x * letterWidth,
(y + 0.5) * lineHeight,
);
x += token.length;
} else if (Array.isArray(token.content)) {
const subTheme = theme[token.type] ?? colors;
for (const subToken of token.content) {
draw(subToken, subTheme);
}
} else {
const subTheme = theme[token.type] ?? colors;
draw(token.content, subTheme);
}
};
for (const token of this.getTokens()) {
draw(token, theme.default);
}
}
private drawLineNumbers(context: CanvasRenderingContext2D) {
const theme = this.theme();
const numbers = this.numbers();
const lines = this.getLines();
const lineHeight = this.fontSize() * this.lineHeight();
context.save();
context.fillStyle = theme.default.comment ?? FALLBACK_COLOR;
context.globalAlpha = this.getAbsoluteOpacity();
context.textAlign = 'right';
for (let i = 0; i < lines.length; i++) {
const number = typeof numbers === 'number' ? numbers + i : i;
context.fillText(number.toString(), -20, (i + 0.5) * lineHeight);
}
context.restore();
}
private getOpacityAtPoint(x: number, y: number): number {
return this.isSelected(x, y)
? this.getAbsoluteOpacity()
: this.getAbsoluteOpacity() * this.unselectedOpacity;
}
private isSelected(x: number, y: number): boolean {
if (this.selection().length === 0) {
return false;
}
return !!this.selection().find(
([[startLine, startColumn], [endLine, endColumn]]) => {
return (
((y === startLine && x >= startColumn) || y > startLine) &&
((y === endLine && x < endColumn) || y < endLine)
);
},
);
}
public selectLines(from: number, to?: number): this {
this.selection([
[
[from, 0],
[to ?? from, Infinity],
],
]);
return this;
}
public selectWord(line: number, from: number, length?: number): this {
this.selection([
[
[line, from],
[line, from + (length ?? Infinity)],
],
]);
return this;
}
public selectRange(
startLine: number,
startColumn: number,
endLine: number,
endColumn: number,
): this {
this.selection([
[
[startLine, startColumn],
[endLine, endColumn],
],
]);
return this;
}
public clearSelection(): this {
this.selection([]);
return this;
}
public hasSelection(): boolean {
return this.selection().length > 0;
}
public apply() {
this.outline = 0;
this.unselectedOpacity = this.hasSelection() ? 0.32 : 1;
}
@threadable('animateCode')
public *animate(): ThreadGenerator {
const hasSelection = this.hasSelection();
const currentOpacity = this.unselectedOpacity;
yield* tween(0.5, value => {
this.outline = easeOutExpo(value, -8, 0);
this.unselectedOpacity = easeOutExpo(
value,
currentOpacity,
hasSelection ? 0.32 : 1,
);
});
this.apply();
}
@threadable()
public *animateClearSelection() {
const currentOpacity = this.unselectedOpacity;
yield* tween(0.5, value => {
this.outline = easeInExpo(value, 0, -8);
this.unselectedOpacity = easeInExpo(value, currentOpacity, 1);
});
this.clearSelection();
this.apply();
}
}

View File

@@ -1,26 +0,0 @@
export type CodeTokens = Record<string, string>;
export type CodeTheme<T extends CodeTokens = CodeTokens> = {
[Key: string]: T;
default: T;
};
export type JSCodeTokens = CodeTokens & {
boolean?: string;
'class-name'?: string;
comment?: string;
constant?: string;
function?: string;
'function-variable'?: string;
hashbang?: string;
keyword?: string;
'literal-property'?: string;
number?: string;
operator?: string;
parameter?: string;
punctuation?: string;
regex?: string;
string?: string;
'string-property'?: string;
'template-string'?: string;
};

View File

@@ -1,2 +0,0 @@
export * from './Code';
export * from './CodeTheme';

View File

@@ -1,17 +0,0 @@
export * from './Align';
export * from './AnimationClip';
export * from './Arrow';
export * from './ColorPicker';
export * from './Connection';
export * from './Grid';
export * from './Icon';
export * from './LayeredLayout';
export * from './LayoutText';
export * from './LinearLayout';
export * from './Pin';
export * from './PinnedLabel';
export * from './Range';
export * from './Sprite';
export * from './Surface';
export * from './ThreeView';
export * from './Video';

View File

@@ -1,9 +0,0 @@
export function KonvaNode(config?: {
name?: string;
centroid?: boolean;
}): ClassDecorator {
return function (target) {
target.prototype.className = config?.name ?? target.name;
target.prototype._centroid = config?.centroid ?? true;
};
}

View File

@@ -1,12 +0,0 @@
import type {Node} from 'konva/lib/Node';
export function cached(key: string): MethodDecorator {
return function (target, propertyKey, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (this: Node) {
return this._getCache(key, original);
};
descriptor.value.prototype.cachedKey = key;
return descriptor;
};
}

View File

@@ -1,15 +0,0 @@
import {Factory} from 'konva/lib/Factory';
import {TweenProvider} from '@motion-canvas/core/lib/tweening';
export function getset<T = unknown>(
defaultValue?: T,
after?: Callback,
tween?: TweenProvider<T>,
): PropertyDecorator {
return function (target, propertyKey) {
Factory.addGetter(target.constructor, propertyKey, defaultValue);
Factory.addSetter(target.constructor, propertyKey, undefined, after);
// @ts-ignore
Factory.addOverloadedGetterSetter(target.constructor, propertyKey, tween);
};
}

View File

@@ -1,3 +0,0 @@
export * from './cached';
export * from './getset';
export * from './KonvaNode';

View File

@@ -1,9 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars,@typescript-eslint/no-namespace */
/// <reference types="@motion-canvas/core/project" />
declare namespace JSX {
type ElementClass = import('konva/lib/Node').Node;
interface ElementChildrenAttribute {
children: unknown;
}
}

View File

@@ -1,43 +0,0 @@
import type {Context} from 'konva/lib/Context';
import {PossibleSpacing, Spacing} from '../types';
export const CanvasHelper = {
roundRect<T extends CanvasRenderingContext2D | Context>(
ctx: T,
x: number,
y: number,
width: number,
height: number,
radius: PossibleSpacing,
): T {
ctx.beginPath();
this.roundRectPath(ctx, x, y, width, height, radius);
ctx.closePath();
return ctx;
},
roundRectPath<T extends CanvasRenderingContext2D | Context | Path2D>(
ctx: T,
x: number,
y: number,
width: number,
height: number,
radius: PossibleSpacing,
): T {
const spacing = new Spacing(radius);
const maxRadius = Math.min(height / 2, width / 2);
spacing.left = Math.min(spacing.left, maxRadius);
spacing.right = Math.min(spacing.right, maxRadius);
spacing.top = Math.min(spacing.top, maxRadius);
spacing.bottom = Math.min(spacing.bottom, maxRadius);
ctx.moveTo(x + spacing.left, y);
ctx.arcTo(x + width, y, x + width, y + height, spacing.top);
ctx.arcTo(x + width, y + height, x, y + height, spacing.right);
ctx.arcTo(x, y + height, x, y, spacing.bottom);
ctx.arcTo(x, y, x + width, y, spacing.left);
return ctx;
},
};

View File

@@ -1 +0,0 @@
export * from './CanvasHelper';

View File

@@ -1,63 +0,0 @@
import type {NodeConfig} from 'konva/lib/Node';
import type {Shape} from 'konva/lib/Shape';
import type {Reference} from '@motion-canvas/core/lib/utils';
import {Container} from 'konva/lib/Container';
import {Surface} from './components';
function isConstructor(
// eslint-disable-next-line @typescript-eslint/ban-types
fn: Function,
): fn is new (...args: unknown[]) => unknown {
return !!fn.prototype?.name;
}
type ChildrenConfig = {
[key in keyof JSX.ElementChildrenAttribute]:
| JSX.ElementClass
| JSX.ElementClass[];
};
type ReferenceConfig = {
ref?: Reference<JSX.ElementClass>;
};
export const Fragment = Symbol.for('Fragment');
export function jsx(
type:
| (new (config?: NodeConfig) => JSX.ElementClass)
| ((config: NodeConfig) => JSX.ElementClass)
| typeof Fragment,
config: NodeConfig & ChildrenConfig & ReferenceConfig,
): JSX.ElementClass | JSX.ElementClass[] {
const {children, ref, ...rest} = config;
const flatChildren = Array.isArray(children) ? children.flat() : [children];
if (type === Fragment) {
return flatChildren;
}
if (!isConstructor(type)) {
return type(config);
}
const node = new type(rest);
if (children) {
if (node instanceof Surface) {
node.setChild(<Shape>flatChildren[0]);
} else if (node instanceof Container) {
node.add(...flatChildren);
}
}
if (ref) {
if (Array.isArray(ref)) {
console.warn('Reference arrays are deprecated. Use makeRef() instead.');
ref[0][ref[1]] = node;
} else {
ref.value = node;
}
}
return node;
}
export {jsx as jsxs};

View File

@@ -1,36 +0,0 @@
import {Node} from 'konva/lib/Node';
import {Container} from 'konva/lib/Container';
import {IRect} from 'konva/lib/types';
declare module 'konva/lib/Container' {
export interface Container {
getChildrenRect(config?: {
skipTransform?: boolean;
skipShadow?: boolean;
skipStroke?: boolean;
relativeTo?: Container<Node>;
}): IRect;
}
}
Container.prototype.updateLayout = function (this: Container): void {
for (const child of this.children) {
child.updateLayout();
if (child.wasDirty()) {
this.markDirty();
}
}
Node.prototype.updateLayout.call(this);
};
Container.prototype._centroid = true;
const super_setChildrenIndices = Container.prototype._setChildrenIndices;
Container.prototype._setChildrenIndices = function (this: Container) {
super_setChildrenIndices.call(this);
this.markDirty();
};
Container.prototype.getChildrenRect = Container.prototype.getClientRect;
Container.prototype.getClientRect = Node.prototype.getClientRect;

View File

@@ -1,64 +0,0 @@
import type {Node} from 'konva/lib/Node';
import {Factory} from 'konva/lib/Factory';
import {ANIMATE} from '@motion-canvas/core/lib';
import {
Animator,
TimingFunction,
InterpolationFunction,
TweenProvider,
} from '@motion-canvas/core/lib/tweening';
import {ThreadGenerator} from '@motion-canvas/core/lib/threading';
import {Vector2d} from 'konva/lib/types';
declare module 'konva/lib/Factory' {
export interface Factory {
addOverloadedGetterSetter(
constructor: new (...args: unknown[]) => unknown,
attr: string,
tween?: TweenProvider<unknown>,
): void;
}
}
declare module 'konva/lib/types' {
export interface GetSet<Type, This extends Node> {
(): Type;
(value: Type): This;
(value: typeof ANIMATE): Animator<Type, This>;
<Rest extends unknown[]>(
value: Type,
time: number,
timingFunction?: TimingFunction,
interpolationFunction?: InterpolationFunction<Type, Rest>,
...rest: Rest
): ThreadGenerator;
}
}
Factory.addOverloadedGetterSetter = function addOverloadedGetterSetter(
constructor: new (...args: unknown[]) => unknown,
attr: string,
tween?: TweenProvider<unknown>,
) {
const capitalizedAttr = attr.charAt(0).toUpperCase() + attr.slice(1);
const setter = 'set' + capitalizedAttr;
const getter = 'get' + capitalizedAttr;
constructor.prototype[attr] = function <Rest extends unknown[]>(
value?: Vector2d | typeof ANIMATE,
time?: number,
timingFunction?: TimingFunction,
interpolationFunction?: InterpolationFunction<unknown, Rest>,
...rest: Rest
) {
if (value === ANIMATE) {
return new Animator<unknown, Node>(this, attr, tween);
}
if (time !== undefined) {
return new Animator<unknown, Node>(this, attr, tween)
.key(value, time, timingFunction, interpolationFunction, ...rest)
.run();
}
return value === undefined ? this[getter]() : this[setter](value);
};
};

View File

@@ -1,370 +0,0 @@
import type {Style} from '../styles';
import {Node, NodeConfig} from 'konva/lib/Node';
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';
import {NODE_ID} from '@motion-canvas/core/lib';
import {useScene} from '@motion-canvas/core/lib/utils';
declare module 'konva/lib/Node' {
export interface Node {
/**
* @internal
*/
_centroid: boolean;
style?: GetSet<Partial<Style>, this>;
/**
* The empty space between the borders of this node and its content.
*
* Analogous to CSS padding.
*/
padd: GetSet<PossibleSpacing, this>;
/**
* The empty space between the borders of this node and surrounding nodes.
*
* Analogous to CSS margin.
*/
margin: GetSet<PossibleSpacing, this>;
/**
* The origin of this node.
*
* By default, each node has its origin in the middle.
*
* Analogous to CSS margin.
*/
origin: GetSet<Origin, this>;
/**
* @ignore
*/
setX(value: number): this;
/**
* @ignore
*/
setY(value: number): this;
/**
* @ignore
*/
setWidth(width: number): void;
/**
* @ignore
*/
setHeight(height: number): void;
/**
* @ignore
*/
setPadd(value: PossibleSpacing): this;
/**
* @ignore
*/
setMargin(value: PossibleSpacing): this;
/**
* @ignore
*/
setOrigin(value: Origin): this;
/**
* @ignore
*/
getPadd(): Spacing;
/**
* @ignore
*/
getMargin(): Spacing;
/**
* @ignore
*/
getOrigin(): Origin;
/**
* Get the size of this node used for layout calculations.
*
* @remarks
* The returned size should include the padding.
* A node can use the size of its children to derive its own dimensions.
*
* @param custom - Custom node configuration to use during the calculations.
* When present, the method will return the layout size that
* the node would have, if it had these options configured.
*/
getLayoutSize(custom?: NodeConfig): Size;
/**
* Get the vector from the local origin of this node to its current origin.
*
* @remarks
* The local origin is the center of coordinates of the canvas when drawing
* the node. Centroid nodes will have their local origin at the center.
* Other shapes will have it in the top left corner.
*
* The current origin is configured via {@link Node.origin}.
*
* @param custom - Custom node configuration to use during the calculations.
* When present, the method will return the origin offset
* that the node would have, if it had these options
* configured.
*/
getOriginOffset(custom?: NodeConfig): Vector2d;
/**
* Get the vector from the current origin of this node to the `newOrigin`.
*
* @param newOrigin - The origin to which the delta should be calculated.
*
* @param custom - Custom node configuration to use during the calculations.
* When present, the method will return the origin offset
* that the node would have, if it had these options
* configured.
*/
getOriginDelta(newOrigin: Origin, custom?: NodeConfig): Vector2d;
/**
* Update the layout of this node and all its children.
*
* @remarks
* If the node is considered dirty the {@link recalculateLayout} method will
* be called.
*/
updateLayout(): void;
/**
* Perform any computations necessary to update the layout of this node.
*/
recalculateLayout(): void;
/**
* Mark this node as dirty.
*
* @remarks
* It will cause the layout of this node and all its ancestors to be
* recalculated before drawing the next frame.
*/
markDirty(force?: boolean): void;
/**
* Check if this node is dirty.
*/
isDirty(): boolean;
/**
* Check if the layout of this node has been recalculated during the current
* layout process.
*
* @remarks
* Containers can use this method to check if their children has changed.
*/
wasDirty(): boolean;
subscribe(event: string, handler: () => void): () => void;
_clearCache(attr?: string | Callback): void;
}
export interface NodeConfig {
margin?: PossibleSpacing;
padd?: PossibleSpacing;
origin?: Origin;
}
}
Node.prototype.setPadd = function (this: Node, value: PossibleSpacing) {
this.attrs.padd = new Spacing(value);
this.markDirty();
return this;
};
Node.prototype.setMargin = function (this: Node, value: PossibleSpacing) {
this.attrs.margin = new Spacing(value);
this.markDirty();
return this;
};
Node.prototype.setOrigin = function (this: Node, value: Origin) {
this.attrs.origin = value;
this.markDirty();
return this;
};
Node.prototype.getLayoutSize = function (
this: Node,
custom?: NodeConfig,
): Size {
const padding =
custom?.padd === null || custom?.padd === undefined
? this.getPadd()
: new Spacing(custom.padd);
return padding.expand(this.getSize());
};
Node.prototype.getOriginOffset = function (
this: Node,
custom?: NodeConfig,
): Vector2d {
return getOriginDelta(
this.getLayoutSize(custom),
this._centroid ? Origin.Middle : Origin.TopLeft,
custom?.origin ?? this.getOrigin(),
);
};
Node.prototype.getOriginDelta = function (
this: Node,
newOrigin?: Origin,
custom?: NodeConfig,
): Vector2d {
return getOriginDelta(
this.getLayoutSize(custom),
custom?.origin ?? this.getOrigin(),
newOrigin,
);
};
Node.prototype.updateLayout = function (this: Node): void {
this.attrs.wasDirty = false;
if (this.isDirty()) {
this.recalculateLayout();
this.attrs.dirty = false;
this.attrs.wasDirty = true;
}
};
Node.prototype.recalculateLayout = function (this: Node): void {
// do nothing
};
Node.prototype.markDirty = function (this: Node, force = false): void {
this.attrs.dirty = true;
if (
force ||
// When the layout size changes and the origin is other than default,
// the transform will also most likely change.
(this._centroid ? Origin.Middle : Origin.TopLeft) !== this.origin()
) {
this._clearCache('transform');
this._clearCache('absoluteTransform');
}
};
Node.prototype.isDirty = function (this: Node): boolean {
return this.attrs.dirty;
};
Node.prototype.wasDirty = function (this: Node): boolean {
return this.attrs.wasDirty;
};
Node.prototype.subscribe = function (
this: Node,
event: string,
handler: () => void,
): () => void {
this.on(event, handler);
return () => this.off(event, handler);
};
Node.prototype.getClientRect = function (
this: Node,
config?: {
skipTransform?: boolean;
skipShadow?: boolean;
skipStroke?: boolean;
relativeTo?: Container;
},
): IRect {
const size = this.getLayoutSize();
const offset = this.getOriginOffset({origin: Origin.TopLeft});
const rect: IRect = {
x: offset.x,
y: offset.y,
width: size.width,
height: size.height,
};
if (!config?.skipTransform) {
return this._transformedRect(rect, config?.relativeTo);
}
return rect;
};
const super_setX = Node.prototype.setX;
Node.prototype.setX = function (this: Node, value: number) {
if (this.attrs.x !== value) this.markDirty();
return super_setX.call(this, value);
};
const super_setY = Node.prototype.setY;
Node.prototype.setY = function (this: Node, value: number) {
if (this.attrs.y !== value) this.markDirty();
return super_setY.call(this, value);
};
const super_setWidth = Node.prototype.setWidth;
Node.prototype.setWidth = function (this: Node, value: number) {
if (this.attrs.width !== value) {
this.markDirty();
}
return super_setWidth.call(this, value);
};
const super_setHeight = Node.prototype.setHeight;
Node.prototype.setHeight = function (this: Node, value: number) {
if (this.attrs.height !== value) {
this.markDirty();
}
return super_setHeight.call(this, value);
};
const super__getTransform = Node.prototype._getTransform;
Node.prototype._getTransform = function (this: Node) {
const m = super__getTransform.call(this);
const offset = this.getOriginOffset();
if (offset.x !== 0 || offset.y !== 0) {
m.translate(-1 * offset.x, -1 * offset.y);
}
m.dirty = false;
return m;
};
const super_setAttrs = Node.prototype.setAttrs;
Node.prototype.setAttrs = function (this: Node, config: unknown) {
if (!(NODE_ID in this.attrs)) {
const scene: any = useScene();
if (scene && 'generateNodeId' in scene) {
const type = this.className;
this.attrs[NODE_ID] = scene.generateNodeId(type);
}
}
return super_setAttrs.call(this, config);
};
const super__clearCache = Node.prototype._clearCache;
Node.prototype._clearCache = function (this: Node, attr?: string | Callback) {
if (typeof attr === 'function') {
if (attr.prototype?.cachedKey) {
this._cache.delete(attr.prototype.cachedKey);
}
} else {
super__clearCache.call(this, attr);
}
};
Factory.addGetterSetter(Node, 'padd', new Spacing());
Factory.addGetterSetter(Node, 'margin', new Spacing());
Factory.addGetterSetter(Node, 'origin', Origin.Middle);

View File

@@ -1,16 +0,0 @@
import {Node} from 'konva/lib/Node';
import {Shape, ShapeGetClientRectConfig} from 'konva/lib/Shape';
declare module 'konva/lib/Shape' {
export interface Shape {
getShapeRect(config?: ShapeGetClientRectConfig): {
width: number;
height: number;
x: number;
y: number;
};
}
}
Shape.prototype.getShapeRect = Shape.prototype.getClientRect;
Shape.prototype.getClientRect = Node.prototype.getClientRect;

View File

@@ -1,190 +0,0 @@
import {Container} from 'konva/lib/Container';
import {
GeneratorScene,
Inspectable,
InspectedElement,
InspectedAttributes,
InspectedSize,
Scene,
SceneDescription,
SceneRenderEvent,
ThreadGeneratorFactory,
} from '@motion-canvas/core/lib/scenes';
import {HitCanvas, SceneCanvas} from 'konva/lib/Canvas';
import {Shape, shapes} from 'konva/lib/Shape';
import {Group} from 'konva/lib/Group';
import {useScene} from '@motion-canvas/core/lib/utils';
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;
const sceneCanvasMap = new Map<HTMLCanvasElement, SceneCanvas>();
export function useKonvaView(): KonvaView {
const scene = useScene();
if (scene instanceof KonvaScene) {
return scene.view;
}
return null;
}
/**
* Create a descriptor for a Konva scene.
*
* @example
* ```ts
* // example.scene.ts
*
* export default makeKonvaScene(function* example(view) {
* yield* view.transition();
* // perform animation
* });
* ```
*
* @param factory - The generator function for this scene.
*/
export function makeKonvaScene(
factory: ThreadGeneratorFactory<KonvaView>,
): SceneDescription {
return {
config: factory,
klass: KonvaScene,
};
}
class KonvaView extends Container {
public constructor(private readonly scene: KonvaScene) {
super();
}
/**
* Start transitioning out of the current scene.
*/
public canFinish() {
this.scene.enterCanTransitionOut();
}
public updateLayout() {
super.updateLayout();
let limit = 10;
while (this.wasDirty() && limit > 0) {
super.updateLayout();
limit--;
}
if (limit === 0) {
console.warn('Layout iteration limit exceeded');
}
}
public add(...children: (Shape | Group)[]): this {
super.add(...children.flat());
this.updateLayout();
return this;
}
public _validateAdd() {
// do nothing
}
}
export class KonvaScene
extends GeneratorScene<KonvaView>
implements Inspectable
{
public readonly view = new KonvaView(this);
private hitCanvas = new HitCanvas({pixelRatio: 1});
public getView(): KonvaView {
return this.view;
}
public update() {
this.view.updateLayout();
}
public render(context: CanvasRenderingContext2D, canvas: HTMLCanvasElement) {
let sceneCanvas = sceneCanvasMap.get(canvas);
if (!sceneCanvas) {
sceneCanvas = new SceneCanvas({
width: canvas.width,
height: canvas.height,
pixelRatio: 1,
});
sceneCanvas._canvas = canvas;
sceneCanvas.getContext()._context = context;
}
context.save();
this.renderLifecycle.dispatch([SceneRenderEvent.BeforeRender, context]);
context.save();
this.renderLifecycle.dispatch([SceneRenderEvent.BeginRender, context]);
this.view.drawScene(sceneCanvas);
this.renderLifecycle.dispatch([SceneRenderEvent.FinishRender, context]);
context.restore();
this.renderLifecycle.dispatch([SceneRenderEvent.AfterRender, context]);
context.restore();
}
public reset(previousScene: Scene = null) {
this.view.x(0).y(0).opacity(1).show();
this.view.destroyChildren();
return super.reset(previousScene);
}
//#region Inspectable Interface
public inspectPosition(x: number, y: number): InspectedElement | null {
this.hitCanvas.setSize(this.getSize().width, this.getSize().height);
this.project.transformCanvas(this.hitCanvas.context._context);
this.view.drawHit(this.hitCanvas, this.view);
const color = this.hitCanvas.context.getImageData(x, y, 1, 1).data;
if (color[3] < 255) return null;
const key = Util._rgbToHex(color[0], color[1], color[2]);
return shapes[`#${key}`] ?? null;
}
public validateInspection(
element: InspectedElement | null,
): InspectedElement | null {
if (!(element instanceof Node)) return null;
if (element.isAncestorOf(this.view)) return element;
const id = element.attrs[NODE_ID];
return (
this.view.findOne((node: Node) => node.attrs[NODE_ID] === id) ?? null
);
}
public inspectAttributes(
element: InspectedElement,
): InspectedAttributes | null {
if (!(element instanceof Node)) return null;
return element.attrs;
}
public inspectBoundingBox(element: InspectedElement): InspectedSize {
if (!(element instanceof Node)) return {};
const rect = element.getClientRect({relativeTo: this.view});
const scale = element.getAbsoluteScale(this.view);
const position = element.getAbsolutePosition(this.view);
const offset = element.getOriginOffset();
return {
rect,
contentRect: element.getPadd().scale(scale).shrink(rect),
marginRect: element.getMargin().scale(scale).expand(rect),
position: {
x: position.x + offset.x,
y: position.y + offset.y,
},
};
}
//#endregion
}

View File

@@ -1 +0,0 @@
export * from './KonvaScene';

View File

@@ -1,46 +0,0 @@
import type {Node} from 'konva/lib/Node';
import Color from 'colorjs.io';
export interface Style {
labelFont: string;
bodyFont: string;
background: string;
backgroundLight: string;
foreground: string;
foregroundLight: string;
}
export const MISSING_STYLE: Style = {
labelFont: '20px "Times New Roman"',
bodyFont: '20px Arial',
background: '#FF00FF',
backgroundLight: '#FF00FF',
foreground: '#FF00FF',
foregroundLight: '#FF00FF',
};
export function getStyle(node: Node): Style {
let mergedStyle = {};
do {
const style = node.style?.() ?? null;
if (style) {
mergedStyle = {
...style,
...mergedStyle,
};
}
node = node.getParent();
} while (node);
return {
...MISSING_STYLE,
...mergedStyle,
};
}
export function getFontColor(background: string) {
const color = new Color(background);
return color.lab.l > 50 ? 'rgba(0, 0, 0, 0.87)' : 'rgba(255, 255, 255, 0.87)';
}

View File

@@ -1 +0,0 @@
export * from './Style';

View File

@@ -1,35 +0,0 @@
import {CodeTheme, JSCodeTokens} from '../components/code';
const KEYWORD = '#ff6470';
const TEXT = '#ACB3BF';
const FUNCTION = '#ffc66d';
const STRING = '#99C47A';
const NUMBER = '#68ABDF';
const PROPERTY = '#AC7BB5';
const COMMENT = '#5c5e60';
export const JS_CODE_THEME: CodeTheme<JSCodeTokens> = {
default: {
boolean: KEYWORD,
constant: KEYWORD,
keyword: KEYWORD,
'class-name': NUMBER,
operator: TEXT,
punctuation: TEXT,
'script-punctuation': TEXT,
function: FUNCTION,
string: STRING,
number: NUMBER,
'literal-property': PROPERTY,
comment: COMMENT,
'function-variable': FUNCTION,
},
parameter: {
'literal-property': TEXT,
operator: TEXT,
punctuation: NUMBER,
},
'attr-name': {
punctuation: FUNCTION,
},
};

View File

@@ -1 +0,0 @@
export * from './code';

View File

@@ -1,46 +0,0 @@
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

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

View File

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

View File

@@ -1,105 +0,0 @@
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

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

View File

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

View File

@@ -1 +0,0 @@
export * from './slide';

View File

@@ -1,23 +0,0 @@
import type {Container} from 'konva/lib/Container';
import type {Vector2} from '../types';
export function slide(container: Container, offset: Vector2): void;
export function slide(container: Container, x: number, y?: number): void;
export function slide(
container: Container,
offset: number | Vector2,
y = 0,
): void {
if (typeof offset === 'number') {
offset = {x: offset, y};
} else {
offset = {...offset};
}
container.move(offset);
offset.x *= -1;
offset.y *= -1;
for (const child of container.children) {
child.move(offset);
}
}

View File

@@ -1,24 +0,0 @@
{
"compilerOptions": {
"baseUrl": "src",
"outDir": "lib",
"inlineSourceMap": true,
"noImplicitAny": true,
"module": "esnext",
"target": "es2020",
"allowJs": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"jsx": "react-jsx",
"jsxImportSource": "@motion-canvas/legacy/lib",
"paths": {
"@motion-canvas/legacy/lib/jsx-runtime": ["jsx-runtime.ts"]
},
"types": ["node", "prismjs", "three"]
},
"include": ["src"]
}

View File

@@ -1,7 +0,0 @@
{
"extends": "@motion-canvas/core/tsconfig.project.json",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@motion-canvas/legacy/lib"
}
}

View File

@@ -1,3 +0,0 @@
import type {Plugin} from 'vite';
declare const _default: () => Plugin;
export default _default;

View File

@@ -1,22 +0,0 @@
module.exports = () => ({
name: 'motion-canvas:legacy',
transform(code, id) {
if (id.endsWith('?project')) {
return (
`import '@motion-canvas/legacy/lib/patches/Factory';` +
`import '@motion-canvas/legacy/lib/patches/Node';` +
`import '@motion-canvas/legacy/lib/patches/Shape';` +
`import '@motion-canvas/legacy/lib/patches/Container';` +
code
);
}
},
config() {
return {
esbuild: {
jsx: 'automatic',
jsxImportSource: '@motion-canvas/legacy/lib',
},
};
},
});

View File

@@ -1 +1 @@
/// <reference types="@motion-canvas/legacy/project" />
/// <reference types="@motion-canvas/core/project" />

View File

@@ -1,9 +1,10 @@
import {makeKonvaScene} from '@motion-canvas/legacy/lib/scenes';
import {makeScene2D} from '@motion-canvas/2d';
import {Circle} from '@motion-canvas/2d/lib/components';
import {waitFor, waitUntil} from '@motion-canvas/core/lib/flow';
import {useRef} from '@motion-canvas/core/lib/utils';
import {Circle} from 'konva/lib/shapes/Circle';
import {Vector2} from '@motion-canvas/core/lib/types';
export default makeKonvaScene(function* (view) {
export default makeScene2D(function* (view) {
const circle = useRef<Circle>();
view.add(
@@ -11,7 +12,7 @@ export default makeKonvaScene(function* (view) {
);
yield* waitUntil('circle');
yield* circle.value.scale({x: 2, y: 2}, 2);
yield* circle.value.scale(Vector2.fromScalar(2), 2);
yield* waitFor(5);
});

View File

@@ -1,5 +1,5 @@
{
"extends": "@motion-canvas/legacy/tsconfig.project.json",
"extends": "@motion-canvas/2d/tsconfig.project.json",
"compilerOptions": {
"baseUrl": "src",
"types": ["node", "prismjs", "three", "dom-webcodecs"]

View File

@@ -1,7 +1,6 @@
import {defineConfig} from 'vite';
import motionCanvas from '@motion-canvas/vite-plugin';
import legacyRenderer from '@motion-canvas/legacy/vite';
export default defineConfig({
plugins: [motionCanvas(), legacyRenderer()],
plugins: [motionCanvas()],
});

View File

@@ -339,6 +339,10 @@ export default ({
server: {
port: 9000,
},
esbuild: {
jsx: 'automatic',
jsxImportSource: '@motion-canvas/2d/lib',
},
};
},
};