feat: add basic documentation structure (#10)

Documentation can now be accessed by visiting
http://localhost:9000/api/

BREAKING CHANGE: `waitFor` and `waitUntil` were moved

They should be imported from `@motion-canvas/core/lib/flow`.

Fixes #2
This commit is contained in:
Jacob
2022-06-12 22:57:32 +02:00
committed by GitHub
parent cc86a4a6d5
commit 1e46433af3
32 changed files with 903 additions and 325 deletions

View File

@@ -13,6 +13,7 @@ jobs:
registry-url: https://npm.pkg.github.com/
- run: npm ci
- run: npm run build
- run: npm run docs
- run: npm run release
env:
NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
node_modules
lib
api
test
.idea

View File

@@ -89,7 +89,7 @@ Otherwise, below are the steps to set it up manually:
7. Initialize the project with your scene in `src/project.ts`:
```ts
import {bootstrap} from '@motion-canvas/core/lib/bootstrap';
import {bootstrap} from '@motion-canvas/core/lib';
import example from './scenes/example.scene';

View File

@@ -121,6 +121,13 @@ const server = new WebpackDevServer(
compress: true,
port: 9000,
hot: true,
static: [
{
directory: path.join(__dirname, '../api'),
publicPath: '/api',
watch: false,
}
],
setupMiddlewares: middlewares => {
middlewares.unshift({
name: 'render',

181
package-lock.json generated
View File

@@ -43,7 +43,8 @@
"@typescript-eslint/parser": "^5.27.1",
"eslint": "^8.17.0",
"prettier": "^2.6.2",
"semantic-release": "^19.0.2"
"semantic-release": "^19.0.2",
"typedoc": "^0.22.17"
}
},
"node_modules/@babel/code-frame": {
@@ -5155,6 +5156,12 @@
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"dev": true
},
"node_modules/jsonc-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz",
"integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==",
"dev": true
},
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
@@ -5353,6 +5360,12 @@
"node": ">=10"
}
},
"node_modules/lunr": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
"dev": true
},
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -9611,6 +9624,17 @@
"node": ">=8"
}
},
"node_modules/shiki": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-0.10.1.tgz",
"integrity": "sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng==",
"dev": true,
"dependencies": {
"jsonc-parser": "^3.0.0",
"vscode-oniguruma": "^1.6.1",
"vscode-textmate": "5.2.0"
}
},
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
@@ -10455,6 +10479,68 @@
"node": ">= 0.6"
}
},
"node_modules/typedoc": {
"version": "0.22.17",
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.17.tgz",
"integrity": "sha512-h6+uXHVVCPDaANzjwzdsj9aePBjZiBTpiMpBBeyh1zcN2odVsDCNajz8zyKnixF93HJeGpl34j/70yoEE5BfNg==",
"dev": true,
"dependencies": {
"glob": "^8.0.3",
"lunr": "^2.3.9",
"marked": "^4.0.16",
"minimatch": "^5.1.0",
"shiki": "^0.10.1"
},
"bin": {
"typedoc": "bin/typedoc"
},
"engines": {
"node": ">= 12.10.0"
},
"peerDependencies": {
"typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x || 4.6.x || 4.7.x"
}
},
"node_modules/typedoc/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/typedoc/node_modules/glob": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
"integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^5.0.1",
"once": "^1.3.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/typedoc/node_modules/minimatch": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/typescript": {
"version": "4.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz",
@@ -10578,6 +10664,18 @@
"node": ">= 0.8"
}
},
"node_modules/vscode-oniguruma": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz",
"integrity": "sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA==",
"dev": true
},
"node_modules/vscode-textmate": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz",
"integrity": "sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==",
"dev": true
},
"node_modules/watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
@@ -14990,6 +15088,12 @@
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"dev": true
},
"jsonc-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz",
"integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==",
"dev": true
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
@@ -15141,6 +15245,12 @@
"yallist": "^4.0.0"
}
},
"lunr": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
"dev": true
},
"make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -18145,6 +18255,17 @@
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
},
"shiki": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-0.10.1.tgz",
"integrity": "sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng==",
"dev": true,
"requires": {
"jsonc-parser": "^3.0.0",
"vscode-oniguruma": "^1.6.1",
"vscode-textmate": "5.2.0"
}
},
"side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
@@ -18781,6 +18902,52 @@
"mime-types": "~2.1.24"
}
},
"typedoc": {
"version": "0.22.17",
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.17.tgz",
"integrity": "sha512-h6+uXHVVCPDaANzjwzdsj9aePBjZiBTpiMpBBeyh1zcN2odVsDCNajz8zyKnixF93HJeGpl34j/70yoEE5BfNg==",
"dev": true,
"requires": {
"glob": "^8.0.3",
"lunr": "^2.3.9",
"marked": "^4.0.16",
"minimatch": "^5.1.0",
"shiki": "^0.10.1"
},
"dependencies": {
"brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0"
}
},
"glob": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
"integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^5.0.1",
"once": "^1.3.0"
}
},
"minimatch": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
"dev": true,
"requires": {
"brace-expansion": "^2.0.1"
}
}
}
},
"typescript": {
"version": "4.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz",
@@ -18873,6 +19040,18 @@
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"vscode-oniguruma": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz",
"integrity": "sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA==",
"dev": true
},
"vscode-textmate": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz",
"integrity": "sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==",
"dev": true
},
"watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",

View File

@@ -12,6 +12,8 @@
"lint:fix": "eslint --fix \"src/**/*.ts?(x)\"",
"prettier": "prettier --check \"src/**/*\"",
"prettier:fix": "prettier --write \"src/**/*\"",
"docs": "typedoc",
"docs:watch": "typedoc --watch",
"release": "semantic-release"
},
"publishConfig": {
@@ -27,6 +29,7 @@
"files": [
"lib",
"bin",
"api",
"tsconfig.project.json"
],
"dependencies": {
@@ -61,6 +64,7 @@
"@typescript-eslint/parser": "^5.27.1",
"eslint": "^8.17.0",
"prettier": "^2.6.2",
"semantic-release": "^19.0.2"
"semantic-release": "^19.0.2",
"typedoc": "^0.22.17"
}
}

View File

@@ -1,6 +1,8 @@
export * from './motion';
export * from './scheduling';
export * from './sequence';
/**
* Animation utilities.
*
* @module
*/
export * from './show';
export * from './surfaceFrom';
export * from './surfaceTransition';

View File

@@ -1,55 +0,0 @@
import {Node} from 'konva/lib/Node';
import {Vector2d} from 'konva/lib/types';
import {decorate, threadable} from '../decorators';
import {tween, easeInOutQuint, vector2dTween} from '../tweening';
import {ThreadGenerator} from '../threading';
export interface MoveConfig {
absolute?: boolean;
speed?: number;
}
decorate(move, threadable());
export function move(
node: Node,
position: Vector2d,
config?: MoveConfig,
): ThreadGenerator;
export function move(
node: Node,
positionX: number,
positionY: number,
config?: MoveConfig,
): ThreadGenerator;
export function move(
node: Node,
arg0: number | Vector2d,
arg1?: number | MoveConfig,
arg2?: MoveConfig,
): ThreadGenerator {
let delta: Vector2d;
let config: MoveConfig;
if (typeof arg0 === 'number') {
delta = {x: arg0, y: <number>arg1};
config = arg2 ?? {};
} else {
delta = arg0;
config = <MoveConfig>arg1 ?? {};
}
const positionFrom = node.position();
const positionTo = config.absolute
? delta
: {x: delta.x + positionFrom.x, y: delta.y + positionFrom.y};
const distance = Math.sqrt(
Math.pow(positionFrom.x - positionTo.x, 2) +
Math.pow(positionFrom.y - positionTo.y, 2),
);
return tween(config.speed ?? distance / 1000, value =>
node.position(
vector2dTween(positionFrom, positionTo, easeInOutQuint(value)),
),
);
}

View File

@@ -1,74 +1,67 @@
import {Node} from 'konva/lib/Node';
import type {Node} from 'konva/lib/Node';
import {Surface} from '../components';
import {Origin, originPosition, Spacing} from '../types';
import {chain} from '../flow';
import {Origin, originPosition} from '../types';
import {all} from '../flow';
import {Vector2d} from 'konva/lib/types';
import {
clampRemap,
easeInExpo,
easeInOutCubic,
easeOutCubic,
easeOutExpo,
linear,
map,
rectArcTween,
spacingTween,
tween,
} from '../tweening';
import {decorate, threadable} from '../decorators';
import {ThreadGenerator} from '../threading';
decorate(showTop, threadable());
export function showTop(node: Node): [ThreadGenerator, ThreadGenerator] {
/**
* Show the given node by sliding it up.
*
* @param node
*/
export function* showTop(node: Node): ThreadGenerator {
const to = node.offsetY();
const from = to - 40;
node.show();
node.cache();
node.offsetY(from);
node.opacity(0);
return [
tween(0.5, value => {
node.opacity(Math.min(1, linear(value, 0, 2)));
node.offsetY(easeOutExpo(value, from, to));
}),
tween(0.5, value => {
node.opacity(Math.min(1, linear(value, 2, 0)));
node.offsetY(easeInExpo(value, to, from));
}),
];
yield* all(
node.opacity(1, 0.5, easeOutExpo),
node.offsetY(to, 0.5, easeOutExpo),
);
node.clearCache();
}
decorate(showSurface, threadable());
export function showSurface(surface: Surface): ThreadGenerator {
const marginFrom = new Spacing();
const margin = surface.getMargin();
const toMask = surface.getMask();
const fromMask = {
...toMask,
width: 0,
height: 0,
};
surface.setMargin(0);
surface.setMask(fromMask);
return tween(
0.5,
value => {
surface.setMask({
...toMask,
...rectArcTween(fromMask, toMask, easeInOutCubic(value)),
});
surface.setMargin(
spacingTween(marginFrom, margin, easeInOutCubic(value)),
);
surface.opacity(clampRemap(0.3, 1, 0, 1, value));
},
() => surface.setMask(null),
);
decorate(showSurfaceVertically, threadable());
/**
* Show the given surface by expanding its mask vertically.
*
* @param surface
*/
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());
export function showCircle(
/**
* Show the given surface using a circle mask.
*
* @param surface
* @param duration
* @param origin The center of the circle mask.
*/
export function* showCircle(
surface: Surface,
duration = 0.6,
origin?: Origin | Vector2d,
@@ -83,30 +76,13 @@ export function showCircle(
radius: 1,
},
});
surface.show();
surface.setCircleMask(mask);
const target = mask.radius;
mask.radius = 0;
return chain(
tween(duration, value => {
mask.radius = easeInOutCubic(value, 0, target);
}),
() => surface.setCircleMask(null),
);
}
export function unravelSurface(surface: Surface): ThreadGenerator {
const mask = surface.getMask();
surface.show();
surface.setMask({...mask, height: 0});
return tween(
0.5,
value => {
surface.setMask({
...mask,
height: map(0, mask.height, easeOutCubic(value)),
});
},
() => surface.setMask(null),
);
yield* tween(duration, value => {
mask.radius = easeInOutCubic(value, 0, target);
});
surface.setCircleMask(null);
}

View File

@@ -1,7 +1,8 @@
import {Surface} from '../components';
import type {Vector2d} from 'konva/lib/types';
import type {ThreadGenerator} from '../threading';
import type {Surface, SurfaceMask} from '../components';
import {
calculateRatio,
clampRemap,
colorTween,
easeInOutCubic,
easeInOutQuint,
@@ -9,73 +10,87 @@ import {
tween,
} from '../tweening';
import {decorate, threadable} from '../decorators';
import {ThreadGenerator} from '../threading';
/**
* Configuration for {@link surfaceFrom}.
*/
export interface SurfaceFromConfig {
/**
* Whether the transition arc should be reversed.
*
* See {@link rectArcTween} from more detail.
*/
reverse?: boolean;
onOpacityChange?: (
surface: Surface,
value: number,
relativeValue: number,
) => boolean | void;
transitionTime?: number;
/**
* A function called when the initial surface is updated.
*
* @param surface The initial surface.
* @param value Completion of the entire transition.
*
* @return `true` if the default changes made by {@link surfaceFrom}
* should be prevented.
*/
onUpdate?: (surface: Surface, value: number) => boolean | void;
duration?: number;
}
decorate(surfaceFrom, threadable());
export function surfaceFrom(fromSurface: Surface) {
const from = fromSurface.getMask();
/**
* Animate the mask of the surface from the initial state to its current state.
*
* @param surface
* @param mask The initial mask
* @param position The initial position
* @param config
*/
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();
decorate(surfaceTransitionExecutor, threadable());
function* surfaceTransitionExecutor(
target: Surface,
config: SurfaceFromConfig = {},
): ThreadGenerator {
const transitionTime = config.transitionTime ?? 1 / 3;
const to = target.getMask();
const toPos = target.getPosition();
const fromDelta = fromSurface.getOriginDelta(target.getOrigin());
target.show();
if (position) {
surface.position(position);
}
surface.show().setMask(fromMask);
const ratio =
(calculateRatio(fromSurface.getPosition(), toPos) +
calculateRatio(from, to)) /
2;
const ratio =
(calculateRatio(surface.getPosition(), toPosition) +
calculateRatio(fromMask, toMask)) /
2;
yield* tween(0.6, value => {
const relativeValue = clampRemap(transitionTime, 1, 0, 1, value);
const fromPos = fromSurface.getPosition();
const fromNewPos = {
x: fromPos.x + fromDelta.x,
y: fromPos.y + fromDelta.y,
};
target.setMask({
...from,
...rectArcTween(from, to, easeInOutQuint(value), config.reverse, ratio),
radius: easeInOutCubic(value, from.radius, target.radius()),
color: colorTween(
from.color,
target.background(),
easeInOutQuint(value),
),
});
target.setPosition(
yield* tween(config.duration ?? 0.6, value => {
surface.setMask({
...fromMask,
...rectArcTween(
fromMask,
toMask,
easeInOutQuint(value),
config.reverse,
ratio,
),
radius: easeInOutCubic(value, fromMask.radius, toMask.radius),
color: colorTween(fromMask.color, toMask.color, easeInOutQuint(value)),
});
if (position) {
surface.setPosition(
rectArcTween(
fromNewPos,
toPos,
position,
toPosition,
easeInOutQuint(value),
config.reverse,
ratio,
),
);
if (!config.onOpacityChange?.(target, value, relativeValue)) {
target.getChild().opacity(relativeValue);
}
});
}
if (!config.onUpdate?.(surface, value)) {
surface.getChild().opacity(value);
}
});
target.setMask(null);
target.show();
}
return surfaceTransitionExecutor;
surface.setMask(null);
}

View File

@@ -11,144 +11,174 @@ import {
import {decorate, threadable} from '../decorators';
import {ThreadGenerator} from '../threading';
/**
* Configuration for {@link surfaceTransition}.
*
* 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.
*
* See {@link rectArcTween} from more detail.
*/
reverse?: boolean;
/**
* A function called when the currently displayed surface changes.
*
* @param surface
*/
onSurfaceChange?: (surface: Surface) => void;
onFromOpacityChange?: (
/**
* 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.
*
* @return `true` if the default changes made by {@link surfaceTransition}
* should be prevented
*/
onInitialSurfaceUpdate?: (
surface: Surface,
value: number,
relativeValue: number,
) => boolean | void;
onToOpacityChange?: (
) => 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.
*
* @return `true` if the default changes made by {@link surfaceTransition}
* should be prevented
*/
onTargetSurfaceUpdate?: (
surface: Surface,
value: number,
relativeValue: number,
) => boolean | void;
) => true | void;
/**
* Duration at which the surfaces are swapped.
*/
transitionTime?: number;
duration?: number;
}
decorate(surfaceTransition, threadable());
export function surfaceTransition(fromSurfaceOriginal: Surface, clone = true) {
const fromSurface = clone
? fromSurfaceOriginal
.clone()
.moveTo(fromSurfaceOriginal.parent)
.zIndex(fromSurfaceOriginal.zIndex())
: fromSurfaceOriginal;
/**
* Morph one surface into another.
*
* @param initial
* @param target
* @param config
*/
export function* surfaceTransition(
initial: Surface,
target: Surface,
config: SurfaceTransitionConfig = {},
): ThreadGenerator {
const from = initial.getMask();
if (clone) {
fromSurfaceOriginal.hide();
}
const from = fromSurfaceOriginal.getMask();
const transitionTime = config.transitionTime ?? 1 / 3;
const to = target.getMask();
const toPos = target.getPosition();
const fromPos = initial.getPosition();
decorate(surfaceTransitionRunner, threadable());
function* surfaceTransitionRunner(
target: Surface,
config: SurfaceTransitionConfig = {},
): ThreadGenerator {
const transitionTime = config.transitionTime ?? 1 / 3;
const to = target.getMask();
const toPos = target.getPosition();
const fromPos = fromSurface.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,
};
let relativeValue = 0;
const fromDelta = fromSurface.getOriginDelta(target.getOrigin());
const fromNewPos = {
x: fromPos.x + fromDelta.x,
y: fromPos.y + fromDelta.y,
};
const toDelta = target.getOriginDelta(fromSurfaceOriginal.getOrigin());
const toNewPos = {
x: toPos.x + toDelta.x,
y: toPos.y + toDelta.y,
};
const ratio =
(calculateRatio(fromNewPos, toPos) + calculateRatio(from, to)) / 2;
const ratio =
(calculateRatio(fromNewPos, toPos) + calculateRatio(from, to)) / 2;
target.hide();
config.onSurfaceChange?.(initial);
target.hide();
config.onSurfaceChange?.(fromSurface);
let check = true;
yield* tween(0.6, value => {
if (value > transitionTime) {
relativeValue = clampRemap(transitionTime, 1, 0, 1, value);
if (check) {
target.show();
fromSurface.destroy();
}
target.setMask({
...from,
...rectArcTween(
from,
to,
easeInOutQuint(value),
config.reverse,
ratio,
),
radius: easeInOutCubic(value, from.radius, target.radius()),
color: colorTween(
from.color,
target.background(),
easeInOutQuint(value),
),
});
target.setPosition(
rectArcTween(
fromNewPos,
toPos,
easeInOutQuint(value),
config.reverse,
ratio,
),
);
if (!config.onToOpacityChange?.(target, value, relativeValue)) {
target.getChild().opacity(relativeValue);
}
if (check) {
config.onSurfaceChange?.(target);
check = false;
}
} else {
relativeValue = clampRemap(0, transitionTime, 1, 0, value);
fromSurface.setMask({
...from,
...rectArcTween(
from,
to,
easeInOutQuint(value),
config.reverse,
ratio,
),
radius: easeInOutCubic(value, from.radius, target.radius()),
color: colorTween(
from.color,
target.background(),
easeInOutQuint(value),
),
});
fromSurface.setPosition(
rectArcTween(
fromPos,
toNewPos,
easeInOutQuint(value),
config.reverse,
ratio,
),
);
if (!config.onFromOpacityChange?.(target, value, relativeValue)) {
fromSurface.getChild().opacity(relativeValue);
}
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,
...rectArcTween(from, to, easeInOutQuint(value), config.reverse, ratio),
radius: easeInOutCubic(value, from.radius, target.radius()),
color: colorTween(
from.color,
target.background(),
easeInOutQuint(value),
),
});
target.setPosition(
rectArcTween(
fromNewPos,
toPos,
easeInOutQuint(value),
config.reverse,
ratio,
),
);
if (!config.onTargetSurfaceUpdate?.(target, value, relativeValue)) {
target.getChild().opacity(relativeValue);
}
});
target.setMask(null);
target.show();
}
if (check) {
config.onSurfaceChange?.(target);
check = false;
}
} else {
relativeValue = clampRemap(0, transitionTime, 1, 0, value);
initial.setMask({
...from,
...rectArcTween(from, to, easeInOutQuint(value), config.reverse, ratio),
radius: easeInOutCubic(value, from.radius, target.radius()),
color: colorTween(
from.color,
target.background(),
easeInOutQuint(value),
),
});
initial.setPosition(
rectArcTween(
fromPos,
toNewPos,
easeInOutQuint(value),
config.reverse,
ratio,
),
);
return surfaceTransitionRunner;
if (!config.onInitialSurfaceUpdate?.(target, value, relativeValue)) {
initial.getChild().opacity(relativeValue);
}
}
});
initial.position(fromPos).setMask(null);
target.position(toPos).setMask(null);
}

View File

@@ -1,7 +1,7 @@
import {Context} from 'konva/lib/Context';
import {Util} from 'konva/lib/Util';
import {GetSet, Vector2d} from 'konva/lib/types';
import {waitFor} from '../animations';
import {waitFor} from '../flow';
import {getset, KonvaNode, threadable} from '../decorators';
import {GeneratorHelper} from '../helpers';
import {InterpolationFunction, map, tween} from '../tweening';

View File

@@ -3,7 +3,7 @@ import {getset, threadable} from '../decorators';
import {Context} from 'konva/lib/Context';
import {GetSet} from 'konva/lib/types';
import {cancel, ThreadGenerator} from '../threading';
import {waitFor} from '../animations';
import {waitFor} from '../flow';
import {CanvasHelper, GeneratorHelper} from '../helpers';
interface VideoConfig extends ShapeConfig {

View File

@@ -2,6 +2,21 @@ import {join, ThreadGenerator} from '../threading';
import {decorate, threadable} from '../decorators';
decorate(all, threadable());
/**
* Run all tasks concurrently and wait for all of them to finish.
*
* Example:
* ```
* // current time: 0s
* yield* all(
* rect.fill('#ff0000', 2),
* rect.opacity(1, 1),
* );
* // current time: 2s
* ```
*
* @param tasks
*/
export function* all(...tasks: ThreadGenerator[]): ThreadGenerator {
for (const task of tasks) {
yield task;

View File

@@ -2,6 +2,21 @@ import {join, ThreadGenerator} from '../threading';
import {decorate, threadable} from '../decorators';
decorate(any, threadable());
/**
* Run all tasks concurrently and wait for any of them to finish.
*
* Example:
* ```
* // current time: 0s
* yield* all(
* rect.fill('#ff0000', 2),
* rect.opacity(1, 1),
* );
* // current time: 1s
* ```
*
* @param tasks
*/
export function* any(...tasks: ThreadGenerator[]): ThreadGenerator {
for (const task of tasks) {
yield task;

View File

@@ -2,8 +2,43 @@ import {decorate, threadable} from '../decorators';
import {isThreadGenerator, ThreadGenerator} from '../threading';
decorate(chain, threadable());
export function* chain(...args: (ThreadGenerator | Callback)[]) {
for (const generator of args) {
/**
* Run tasks one after another.
*
* Example:
* ```ts
* // current time: 0s
* yield* chain(
* rect.fill('#ff0000', 2),
* rect.opacity(1, 1),
* );
* // current time: 3s
* ```
*
* Note that the same animation can be written as:
* ```ts
* yield* rect.fill('#ff0000', 2),
* yield* rect.opacity(1, 1),
* ```
*
* The reason `chain` exists is to make it easier to pass it to other flow
* functions. For example:
* ```ts
* yield* all(
* rect.corenerRadius(20, 3),
* chain(
* rect.fill('#ff0000', 2),
* rect.opacity(1, 1),
* ),
* );
* ```
*
* @param tasks
*/
export function* chain(
...tasks: (ThreadGenerator | Callback)[]
): ThreadGenerator {
for (const generator of tasks) {
if (isThreadGenerator(generator)) {
yield* generator;
} else {

View File

@@ -1,8 +1,34 @@
import {waitFor} from '../animations';
import {waitFor} from './scheduling';
import {decorate, threadable} from '../decorators';
import {isThreadGenerator, ThreadGenerator} from '../threading';
decorate(delay, threadable());
/**
* Run the given generator or callback after a specific amount of time.
*
* Example:
* ```ts
* yield* delay(1, rect.fill('#ff0000', 2));
* ```
*
* Note that the same animation can be written as:
* ```ts
* yield* waitFor(1),
* yield* rect.fill('#ff0000', 2),
* ```
*
* The reason `delay` exists is to make it easier to pass it to other flow
* functions. For example:
* ```ts
* yield* all(
* rect.opacity(1, 3),
* delay(1, rect.fill('#ff0000', 2));
* );
* ```
*
* @param time Delay in seconds
* @param task
*/
export function* delay(
time: number,
task: ThreadGenerator | Callback,

View File

@@ -2,7 +2,49 @@ import {ThreadGenerator} from '../threading';
import {decorate, threadable} from '../decorators';
import {useProject} from '../utils';
export function every(seconds: number, callback: (frame: number) => void) {
/**
* A callback called by {@link EveryTimer} every N seconds.
*/
export interface EveryCallback {
/**
* @param tick The amount of times the timer has ticked.
*/
(tick: number): void;
}
export interface EveryTimer {
/**
* The generator responsible for running this timer.
*/
runner: ThreadGenerator;
setInterval(value: number): void;
setCallback(value: EveryCallback): void;
/**
* Wait until the timer ticks.
*/
sync(): ThreadGenerator;
}
/**
* Call the given callback every N seconds.
*
* Example:
* ```ts
* const timer = every(2, time => console.log(time));
* yield timer.runner;
*
* // current time: 0s
* yield* waitFor(5);
* // current time: 5s
* yield* timer.sync();
* // current time: 6s
* ```
*
* @param interval
* @param callback
*/
export function every(interval: number, callback: EveryCallback): EveryTimer {
let changed = false;
decorate(everyRunner, threadable('every'));
function* everyRunner(): ThreadGenerator {
@@ -13,7 +55,7 @@ export function every(seconds: number, callback: (frame: number) => void) {
changed = true;
while (true) {
if (acc >= project.secondsToFrames(seconds)) {
if (acc >= project.secondsToFrames(interval)) {
acc = 0;
tick++;
callback(tick);
@@ -28,15 +70,15 @@ export function every(seconds: number, callback: (frame: number) => void) {
return {
runner: everyRunner(),
setSeconds(value: number) {
seconds = value;
setInterval(value) {
interval = value;
changed = false;
},
setCallback(value: (frame: number) => void) {
setCallback(value) {
callback = value;
changed = false;
},
*sync(): ThreadGenerator {
*sync() {
while (!changed) {
yield;
}

View File

@@ -1,6 +1,13 @@
/**
* Utilities for controlling the flow and timing of an animation.
*
* @module
*/
export * from './all';
export * from './any';
export * from './chain';
export * from './delay';
export * from './every';
export * from './loop';
export * from './scheduling';
export * from './sequence';

View File

@@ -1,11 +1,45 @@
import {decorate, threadable} from '../decorators';
import {ThreadGenerator} from '../threading';
/**
* A callback called by {@link loop} during each iteration.
*/
export interface LoopCallback {
/**
* @param i The current iteration index.
*/
(i: number): ThreadGenerator | void;
}
decorate(loop, threadable());
/**
* Run the given generator N times.
*
* Each time iteration waits until the previous one is completed.
*
* Example:
* ```ts
* const colors = [
* '#ff6470',
* '#ffc66d',
* '#68abdf',
* '#99c47a',
* ];
*
* yield* loop(
* colors.length,
* i => rect.fill(colors[i], 2),
* });
* ```
*
* @param iterations Number of iterations.
* @param factory A function creating the generator to run. Because generators
* can't be reset, a new generator is created each iteration.
*/
export function* loop(
iterations: number,
factory: (i: number) => ThreadGenerator | void,
) {
factory: LoopCallback,
): ThreadGenerator {
for (let i = 0; i < iterations; i++) {
const generator = factory(i);
if (generator) {

View File

@@ -3,10 +3,39 @@ import {ThreadGenerator} from '../threading';
import {useProject, useScene} from '../utils';
decorate(waitUntil, threadable());
/**
* Wait until the given time.
*
* Example:
* ```ts
* // current time: 0s
* yield waitUntil(2);
* // current time: 2s
* yield waitUntil(3);
* // current time: 3s
* ```
*
* @param time Absolute time in seconds.
* @param after
*/
export function waitUntil(
time: number,
after?: ThreadGenerator,
): ThreadGenerator;
/**
* Wait until the given time event.
*
* Time events are displayed on the timeline and can be edited to adjust the
* delay. By default, an event happens immediately - without any delay.
*
* Example:
* ```ts
* yield waitUntil('event');
* ```
*
* @param event Name of the time event.
* @param after
*/
export function waitUntil(
event: string,
after?: ThreadGenerator,
@@ -31,6 +60,21 @@ export function* waitUntil(
}
decorate(waitFor, threadable());
/**
* Wait for the given amount of time.
*
* Example:
* ```ts
* // current time: 0s
* yield waitFor(2);
* // current time: 2s
* yield waitFor(3);
* // current time: 5s
* ```
*
* @param seconds Relative time in seconds.
* @param after
*/
export function* waitFor(
seconds = 0,
after?: ThreadGenerator,

View File

@@ -3,6 +3,12 @@ import {decorate, threadable} from '../decorators';
import {join, ThreadGenerator} from '../threading';
decorate(sequence, threadable());
/**
* Run
*
* @param delay
* @param sequences
*/
export function* sequence(
delay: number,
...sequences: ThreadGenerator[]

4
src/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export * from './bootstrap';
export * from './Project';
export * from './Scene';
export * from './symbols';

View File

@@ -1,19 +1,41 @@
import {GeneratorHelper} from '../helpers';
import {ThreadGenerator} from './ThreadGenerator';
/**
* A class representing an individual thread.
*
* Thread is a wrapper for a generator that can be executed concurrently.
*
* Aside from the main thread, all threads need to have a parent.
* If a parent finishes execution, all of its child threads are terminated.
*/
export class Thread {
public children: Thread[] = [];
/**
* The next value to be passed to the wrapped generator.
*/
public value: unknown;
private parent: Thread = null;
/**
* Check if this thread or any of its ancestors has been canceled.
*/
public get canceled(): boolean {
return this._canceled || (this.parent?.canceled ?? false);
}
private parent: Thread = null;
private _canceled = false;
public constructor(public readonly runner: ThreadGenerator) {}
public constructor(
/**
* The generator wrapped by this thread.
*/
public readonly runner: ThreadGenerator,
) {}
/**
* Progress the wrapped generator once.
*/
public next() {
const result = this.runner.next(this.value);
this.value = null;

View File

@@ -1,13 +1,44 @@
import type {Project} from '../Project';
import {JoinYieldResult} from './join';
import {CancelYieldResult} from './cancel';
/**
* The main generator type produced by all generator functions in Motion Canvas.
*
* Yielded values can be used to control the flow of animation:
*
* - Progress to the next frame:
* ```ts
* yield;
* ```
*
* - Run another generator synchronously:
* ```ts
* yield* generatorFunction();
* ```
*
* - Run another generator concurrently:
* ```ts
* const task = yield generatorFunction();
* ```
* - Await a [Promise][promise]:
* ```ts
* const result = yield asyncFunction();
* ```
*
* [promise]: https://developer.mozilla.org/en-US/docs/web/javascript/reference/global_objects/promise
*/
export type ThreadGenerator = Generator<
ThreadGenerator | JoinYieldResult | CancelYieldResult | Promise<any> | symbol,
ThreadGenerator | JoinYieldResult | CancelYieldResult | Promise<any>,
void,
ThreadGenerator | Project | any
ThreadGenerator | any
>;
/**
* Check if the given value is a {@link ThreadGenerator}.
*
* @param value A possible thread {@link ThreadGenerator}.
*/
export function isThreadGenerator(value: unknown): value is ThreadGenerator {
return typeof value === 'object' && Symbol.iterator in value;
}

View File

@@ -1,12 +1,26 @@
import {decorate, threadable} from '../decorators';
import {ThreadGenerator} from './ThreadGenerator';
/**
* @internal
*/
export const THREAD_CANCEL = Symbol.for('THREAD_CANCEL');
/**
* An instruction passed to the {@link threads} generator to cancel tasks.
*/
export interface CancelYieldResult {
/**
* Tasks to cancel.
*/
[THREAD_CANCEL]: ThreadGenerator[];
}
/**
* Check if the given value is a {@link CancelYieldResult}.
*
* @param value A possible {@link CancelYieldResult}.
*/
export function isCancelYieldResult(
value: unknown,
): value is CancelYieldResult {
@@ -14,6 +28,20 @@ export function isCancelYieldResult(
}
decorate(cancel, threadable());
/**
* Cancel all listed tasks.
*
* Example:
* ```ts
* const task = yield generatorFunction();
*
* // do something concurrently
*
* yield* cancel(task);
* ```
*
* @param tasks
*/
export function* cancel(...tasks: ThreadGenerator[]): ThreadGenerator {
yield {[THREAD_CANCEL]: tasks};
}

View File

@@ -1,3 +1,8 @@
/**
* Thread management.
*
* @module
*/
export * from './cancel';
export * from './join';
export * from './Thread';

View File

@@ -1,23 +1,71 @@
import {decorate, threadable} from '../decorators';
import {ThreadGenerator} from './ThreadGenerator';
/**
* @internal
*/
export const THREAD_JOIN = Symbol.for('THREAD_JOIN');
/**
* An instruction passed to the {@link threads} generator to join tasks.
*/
export interface JoinYieldResult {
/**
* Tasks to join.
*/
[THREAD_JOIN]: ThreadGenerator[];
/**
* Whether we should wait for all tasks or for at least one.
*/
all: boolean;
}
/**
* Check if the given value is a {@link JoinYieldResult}.
*
* @param value A possible {@link JoinYieldResult}.
*/
export function isJoinYieldResult(value: unknown): value is JoinYieldResult {
return typeof value === 'object' && THREAD_JOIN in value;
}
decorate(join, threadable());
/**
* Pause the current generator until all listed tasks are finished.
*
* Example:
* ```ts
* const task = yield generatorFunction();
*
* // do something concurrently
*
* yield* join(task);
* ```
*
* @param tasks
*/
export function join(...tasks: ThreadGenerator[]): ThreadGenerator;
/**
* Pause the current generator until listed tasks are finished.
*
* Example
* ```ts
* const taskA = yield generatorFunctionA();
* const taskB = yield generatorFunctionB();
*
* // do something concurrently
*
* // await any of the tasks
* yield* join(false, taskA, taskB);
* ```
*
* @param all Whether we should wait for all tasks or for at least one.
* @param tasks
*/
export function join(
all: boolean,
...tasks: ThreadGenerator[]
): ThreadGenerator;
export function join(...tasks: ThreadGenerator[]): ThreadGenerator;
export function* join(
first: ThreadGenerator | boolean,
...tasks: ThreadGenerator[]

View File

@@ -5,10 +5,20 @@ import {isJoinYieldResult, THREAD_JOIN} from './join';
import {isCancelYieldResult, THREAD_CANCEL} from './cancel';
import {isThreadGenerator, ThreadGenerator} from './ThreadGenerator';
/**
* Check if the given value is a [Promise][promise].
*
* @param value A possible [Promise][promise].
*
* [promise]: https://developer.mozilla.org/en-US/docs/web/javascript/reference/global_objects/promise
*/
export function isPromise(value: any): value is Promise<any> {
return typeof value?.then === 'function';
}
/**
* A generator function or a normal function that returns a generator.
*/
export interface ThreadsFactory {
(): ThreadGenerator;
}
@@ -18,6 +28,30 @@ export interface ThreadsCallback {
}
decorate(threads, threadable());
/**
* Create a context in which generators can be run concurrently.
*
* From the perspective of the external generator, `threads` is executed
* synchronously. By default, each scene generator is wrapped in its own
* `threads` generator.
*
* Example:
* ```ts
* // first
*
* yield* threads(function* () {
* const task = yield generatorFunction();
* // second
* }); // <- `task` will be terminated here because the scope
* // of this `threads` generator has ended
*
* // third
* ```
*
* @param factory
* @param callback Called whenever threads are created, canceled or finished.
* Used for debugging purposes.
*/
export function* threads(
factory: ThreadsFactory,
callback?: ThreadsCallback,

View File

@@ -11,7 +11,7 @@ import {
vector2dTween,
} from './index';
import {threadable} from '../decorators';
import {waitFor, waitUntil} from '../animations';
import {waitFor, waitUntil} from '../flow';
import {ThreadGenerator} from '../threading';
import {GeneratorHelper} from '../helpers';

1
src/video/index.ts Normal file
View File

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

21
typedoc.json Normal file
View File

@@ -0,0 +1,21 @@
{
"out": "api",
"entryPoints": [
"src",
"src/animations",
"src/audio",
"src/components",
"src/decorators",
"src/flow",
"src/helpers",
"src/player",
"src/styles",
"src/themes",
"src/threading",
"src/transitions",
"src/tweening",
"src/types",
"src/utils",
"src/video"
]
}