mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-11 23:07:57 -05:00
feat: switch to Vite (#28)
This PR replaces webpack with Vite and restructures the project
to prepare for bundling animations in the future.
BREAKING CHANGE: change the overall structure of a project
`vite` and `@motion-canvas/vite-plugin` packages are now required to build a project:
```
npm i -D vite @motion-canvas/vite-plugin
```
The following `vite.config.ts` file needs to be created in the root of the project:
```ts
import {defineConfig} from 'vite';
import motionCanvas from '@motion-canvas/vite-plugin';
export default defineConfig({
plugins: [motionCanvas()],
});
```
Types exposed by Motion Canvas are no longer global.
An additional `motion-canvas.d.ts` file needs to be created in the `src` directory:
```ts
/// <reference types="@motion-canvas/core/project" />
```
Finally, the `bootstrap` function no longer exists.
Project files should export an instance of the `Project` class instead:
```ts
import {Project} from '@motion-canvas/core/lib';
import example from './scenes/example.scene';
export default new Project({
name: 'project',
scenes: [example],
// same options as in bootstrap() are available:
background: '#141414',
});
```
Including `import '@motion-canvas/core/lib/patches';` at the top of the project file is no longer necessary.
Closes: #13
This commit is contained in:
1796
package-lock.json
generated
1796
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,11 +10,13 @@
|
||||
"core:watch": "npm run watch -w packages/core",
|
||||
"core:test": "npm run test -w packages/core",
|
||||
"ui:build": "npm run build -w packages/ui",
|
||||
"ui:serve": "npm run serve -w packages/ui",
|
||||
"ui:dev": "npm run dev -w packages/ui",
|
||||
"template:serve": "npm run serve -w packages/template",
|
||||
"template:ui": "npm run ui -w packages/template",
|
||||
"template:build": "npm run build -w packages/template",
|
||||
"docs:start": "npm run start -w packages/docs",
|
||||
"docs:build": "npm run build -w packages/docs",
|
||||
"vite-plugin:build": "npm run build -w packages/vite-plugin",
|
||||
"vite-plugin:watch": "npm run watch -w packages/vite-plugin",
|
||||
"eslint": "eslint --fix \"**/*.ts?(x)\"",
|
||||
"prettier": "prettier --write ."
|
||||
},
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import {fileURLToPath} from 'url';
|
||||
import webpack from 'webpack';
|
||||
import WebpackDevServer from 'webpack-dev-server';
|
||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||
import meow from 'meow';
|
||||
import UIPlugin from './plugins/UIPlugin.mjs';
|
||||
import {createRequire} from 'module';
|
||||
import {metaImportTransformer} from './transformers/metaImportTransformer.mjs';
|
||||
|
||||
const cli = meow({
|
||||
importMeta: import.meta,
|
||||
flags: {
|
||||
uiServer: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
uiPath: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
output: {
|
||||
type: 'string',
|
||||
alias: 'o',
|
||||
default: 'output',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (cli.flags.uiServer) {
|
||||
cli.flags.uiPath ||= 'http://localhost:9001/main.js';
|
||||
} else {
|
||||
if (cli.flags.uiPath) {
|
||||
cli.flags.uiPath = path.resolve(cli.flags.uiPath);
|
||||
} else {
|
||||
const require = createRequire(import.meta.url);
|
||||
cli.flags.uiPath = path.dirname(require.resolve('@motion-canvas/ui'));
|
||||
}
|
||||
}
|
||||
|
||||
const META_VERSION = 1;
|
||||
|
||||
const projectFile = path.resolve(cli.input[0]);
|
||||
const renderOutput = path.resolve(cli.flags.output);
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const packageJSON = JSON.parse(
|
||||
fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf8'),
|
||||
);
|
||||
|
||||
const compiler = webpack({
|
||||
entry: {project: projectFile},
|
||||
mode: 'development',
|
||||
devtool: 'inline-source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
getCustomTransformers: () => ({
|
||||
before: [
|
||||
metaImportTransformer({
|
||||
project: projectFile,
|
||||
version: META_VERSION,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.meta/i,
|
||||
loader: 'meta-loader',
|
||||
},
|
||||
{
|
||||
test: /\.(wav|mp3|ogg|mp4)$/i,
|
||||
type: 'asset',
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g)$/i,
|
||||
oneOf: [
|
||||
{
|
||||
resourceQuery: /img/,
|
||||
loader: 'image-loader',
|
||||
},
|
||||
{
|
||||
resourceQuery: /anim/,
|
||||
loader: 'animation-loader',
|
||||
},
|
||||
{
|
||||
type: 'asset',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.csv$/,
|
||||
loader: 'csv-loader',
|
||||
options: {
|
||||
dynamicTyping: true,
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.glsl$/i,
|
||||
type: 'asset/source',
|
||||
},
|
||||
],
|
||||
},
|
||||
resolveLoader: {
|
||||
modules: ['node_modules', path.resolve(__dirname, './loaders')],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts', '.tsx'],
|
||||
},
|
||||
output: {
|
||||
filename: `[name].js`,
|
||||
publicPath: '/',
|
||||
path: __dirname,
|
||||
},
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
// Required to load additional languages for Prism
|
||||
Prism: 'prismjs',
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
PROJECT_FILE_NAME: `'${path.parse(projectFile).name}'`,
|
||||
CORE_VERSION: `'${packageJSON.version}'`,
|
||||
META_VERSION,
|
||||
}),
|
||||
new HtmlWebpackPlugin({title: 'Motion Canvas'}),
|
||||
new UIPlugin(cli.flags),
|
||||
],
|
||||
});
|
||||
|
||||
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',
|
||||
path: '/render/:name',
|
||||
middleware: (req, res) => {
|
||||
const file = path.join(renderOutput, req.params.name);
|
||||
const directory = path.dirname(file);
|
||||
if (!fs.existsSync(directory)) {
|
||||
fs.mkdirSync(directory, {recursive: true});
|
||||
}
|
||||
const stream = fs.createWriteStream(file, {encoding: 'base64'});
|
||||
req.pipe(stream);
|
||||
req.on('end', () => res.end());
|
||||
},
|
||||
});
|
||||
|
||||
middlewares.unshift({
|
||||
name: 'meta',
|
||||
path: '/meta/:source',
|
||||
middleware: (req, res) => {
|
||||
const stream = fs.createWriteStream(
|
||||
path.join(compiler.context, req.params.source),
|
||||
{encoding: 'utf8'},
|
||||
);
|
||||
req.pipe(stream);
|
||||
req.on('end', () => res.end());
|
||||
},
|
||||
});
|
||||
|
||||
if (!cli.flags.uiServer) {
|
||||
middlewares.unshift({
|
||||
name: 'ui',
|
||||
path: '/ui/:name',
|
||||
middleware: (req, res) => {
|
||||
fs.createReadStream(path.join(cli.flags.uiPath, req.params.name), {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
.on('error', () => res.sendStatus(404))
|
||||
.pipe(res);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return middlewares;
|
||||
},
|
||||
},
|
||||
compiler,
|
||||
);
|
||||
server.start().catch(console.error);
|
||||
@@ -1,30 +0,0 @@
|
||||
const path = require('path');
|
||||
const {readdirSync} = require('fs');
|
||||
const nameRegex = /\D*(\d+)\.png$/;
|
||||
|
||||
function animationLoader() {
|
||||
const callback = this.async();
|
||||
const directoryPath = path.dirname(this.resourcePath);
|
||||
|
||||
const files = readdirSync(directoryPath)
|
||||
.map(file => nameRegex.exec(file))
|
||||
.filter(match => !!match)
|
||||
.map(match => [match.input, parseInt(match[1])])
|
||||
.sort(([, indexA], [, indexB]) =>
|
||||
indexA < indexB ? -1 : indexA > indexB ? 1 : 0,
|
||||
)
|
||||
.map(([file]) => path.resolve(directoryPath, file));
|
||||
|
||||
loadAnimation(files, this.importModule)
|
||||
.then(code => callback(null, code))
|
||||
.catch(error => callback(error));
|
||||
}
|
||||
|
||||
async function loadAnimation(files, importModule) {
|
||||
const urls = await Promise.all(files.map(file => importModule(file)));
|
||||
|
||||
return `import {loadAnimation} from '@motion-canvas/core/lib/media';
|
||||
export default loadAnimation(${JSON.stringify(urls)});`;
|
||||
}
|
||||
|
||||
module.exports = animationLoader;
|
||||
@@ -1,15 +0,0 @@
|
||||
function imageLoader() {
|
||||
const callback = this.async();
|
||||
loadImage(this.resourcePath, this.importModule)
|
||||
.then(code => callback(null, code))
|
||||
.catch(error => callback(error));
|
||||
}
|
||||
|
||||
async function loadImage(fileName, importModule) {
|
||||
const url = await importModule(fileName);
|
||||
|
||||
return `import {loadImage} from '@motion-canvas/core/lib/media';
|
||||
export default loadImage('${url}');`;
|
||||
}
|
||||
|
||||
module.exports = imageLoader;
|
||||
@@ -1,39 +0,0 @@
|
||||
const {SourceMapGenerator} = require('source-map');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Webpack Loader for meta files.
|
||||
*
|
||||
* @param {string} source
|
||||
*/
|
||||
function metaLoader(source) {
|
||||
const relative = path.relative(this.rootContext, this.resourcePath);
|
||||
const name = path.basename(relative, '.meta');
|
||||
const generator = new SourceMapGenerator({file: this.resourcePath});
|
||||
const lines = source.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
generator.addMapping({
|
||||
source: this.resourcePath,
|
||||
generated: {
|
||||
line: i + 5,
|
||||
column: i === 0 ? 4 : 3,
|
||||
},
|
||||
original: {
|
||||
line: i + 1,
|
||||
column: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
generator.setSourceContent(this.resourcePath, source);
|
||||
const map = generator.toJSON();
|
||||
const content = `import {Meta} from '@motion-canvas/core/lib';
|
||||
Meta.register(
|
||||
'${name}',
|
||||
'${encodeURIComponent(relative)}',
|
||||
\`${source}\`
|
||||
);
|
||||
module.hot.accept(console.error);`;
|
||||
this.callback(null, content, map);
|
||||
}
|
||||
|
||||
module.exports = metaLoader;
|
||||
@@ -1,23 +0,0 @@
|
||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||
|
||||
export default class UIPlugin {
|
||||
constructor(config) {
|
||||
this.name = 'UIPlugin';
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
apply(compiler) {
|
||||
compiler.hooks.compilation.tap(this.name, compilation => {
|
||||
HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(
|
||||
this.name,
|
||||
(data, cb) => {
|
||||
data.assets.js.push(
|
||||
this.config.uiServer ? this.config.uiPath : 'ui/main.js',
|
||||
);
|
||||
data.assets.favicon = 'data:;base64,iVBORw0KGgo=';
|
||||
cb(null, data);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import ts from 'typescript';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
const SCENE_REGEX = /\.scene\.tsx?$/;
|
||||
|
||||
/**
|
||||
* Create a transformer that adds meta imports to projects and scenes.
|
||||
*
|
||||
* Example meta import:
|
||||
* ```ts
|
||||
* import './name.meta';
|
||||
* ```
|
||||
*
|
||||
* @param {{project: string, version: string}} config
|
||||
* @return {(context: TransformationContext) => TransformerFactory}
|
||||
*/
|
||||
export function metaImportTransformer(config) {
|
||||
return context => {
|
||||
const visitor = node => {
|
||||
if (
|
||||
node.kind === ts.SyntaxKind.SourceFile &&
|
||||
(config.project === path.resolve(node.fileName) ||
|
||||
SCENE_REGEX.test(node.fileName))
|
||||
) {
|
||||
node = createMeta(context, node, config.version);
|
||||
}
|
||||
|
||||
return ts.visitEachChild(node, visitor, context);
|
||||
};
|
||||
|
||||
return node => ts.visitNode(node, visitor);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a meta import at the top of the given SourceFile.
|
||||
*
|
||||
* @param {TransformationContext} context
|
||||
* @param {SourceFile} node
|
||||
* @param {string} version
|
||||
*
|
||||
* @return {SourceFile}
|
||||
*/
|
||||
function createMeta(context, node, version) {
|
||||
const {name, dir} = path.parse(node.fileName);
|
||||
const metaFile = `${name}.meta`;
|
||||
const metaPath = path.join(dir, metaFile);
|
||||
|
||||
if (!fs.existsSync(metaPath)) {
|
||||
fs.writeFileSync(metaPath, JSON.stringify({version}, undefined, 2), 'utf8');
|
||||
}
|
||||
|
||||
const importStringLiteral = context.factory.createStringLiteral(
|
||||
`./${metaFile}`,
|
||||
true,
|
||||
);
|
||||
const importDeclaration = context.factory.createImportDeclaration(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
importStringLiteral,
|
||||
);
|
||||
return context.factory.updateSourceFile(node, [
|
||||
importDeclaration,
|
||||
...node.statements,
|
||||
]);
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
rootDir: 'src',
|
||||
preset: 'ts-jest',
|
||||
preset: 'ts-jest/presets/js-with-ts-esm',
|
||||
testEnvironment: 'jsdom',
|
||||
setupFiles: ['jest-canvas-mock'],
|
||||
setupFiles: ['jest-canvas-mock', './setup.test.ts'],
|
||||
testPathIgnorePatterns: ['setup.test.ts'],
|
||||
globals: {
|
||||
CORE_VERSION: '1.0.0',
|
||||
PROJECT_FILE_NAME: 'tests',
|
||||
META_VERSION: 1,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,33 +17,23 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/motion-canvas/motion-canvas.git"
|
||||
},
|
||||
"bin": {
|
||||
"motion-canvas": "bin/index.mjs"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"bin",
|
||||
"tsconfig.project.json"
|
||||
"tsconfig.project.json",
|
||||
"project.d.ts"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"vite": "^3.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/three": "^0.141.0",
|
||||
"@types/webpack-env": "^1.17.0",
|
||||
"colorjs.io": "^0.3.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"image-size": "^1.0.1",
|
||||
"konva": "^8.3.9",
|
||||
"meow": "^10.1.2",
|
||||
"mix-color": "^1.1.2",
|
||||
"mp4box": "^0.5.2",
|
||||
"prismjs": "^1.28.0",
|
||||
"source-map": "^0.7.4",
|
||||
"three": "^0.141.0",
|
||||
"ts-loader": "^9.3.0",
|
||||
"typescript": "^4.7.3",
|
||||
"webpack": "^5.73.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.9.2"
|
||||
"three": "^0.141.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dom-webcodecs": "^0.1.4",
|
||||
@@ -52,6 +42,8 @@
|
||||
"jest": "^28.1.2",
|
||||
"jest-canvas-mock": "^2.4.0",
|
||||
"jest-environment-jsdom": "^28.1.2",
|
||||
"ts-jest": "^28.0.5"
|
||||
"ts-jest": "^28.0.5",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^3.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
38
packages/core/project.d.ts
vendored
Normal file
38
packages/core/project.d.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars,@typescript-eslint/no-namespace */
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*?img' {
|
||||
const value: Promise<HTMLImageElement>;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module '*?anim' {
|
||||
const value: Promise<HTMLImageElement[]>;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module '*.csv' {
|
||||
const value: unknown;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module '*.glsl' {
|
||||
const value: string;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare namespace JSX {
|
||||
type ElementClass = import('konva/lib/Node').Node;
|
||||
interface ElementChildrenAttribute {
|
||||
children: unknown;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'colorjs.io' {
|
||||
const noTypesYet: any;
|
||||
export default noTypesYet;
|
||||
}
|
||||
|
||||
declare type Callback = (...args: unknown[]) => void;
|
||||
|
||||
declare const PROJECT_FILE_NAME: string;
|
||||
@@ -1,4 +1,7 @@
|
||||
import {ValueDispatcher} from './events';
|
||||
import {ifHot} from './utils';
|
||||
|
||||
const META_VERSION = 1;
|
||||
|
||||
/**
|
||||
* Represents the contents of a meta file.
|
||||
@@ -23,12 +26,9 @@ export class Meta<T extends Metadata = Metadata> {
|
||||
}
|
||||
private readonly data = new ValueDispatcher(<T>{version: META_VERSION});
|
||||
|
||||
private rawData: string;
|
||||
private source: string;
|
||||
private source: string | false;
|
||||
|
||||
private constructor(private readonly name: string) {
|
||||
this.rawData = JSON.stringify(this.data.current, undefined, 2);
|
||||
}
|
||||
private constructor(private readonly name: string) {}
|
||||
|
||||
public getData() {
|
||||
return this.data.current;
|
||||
@@ -51,24 +51,52 @@ export class Meta<T extends Metadata = Metadata> {
|
||||
...this.data.current,
|
||||
...data,
|
||||
};
|
||||
this.rawData = JSON.stringify(this.data.current, undefined, 2);
|
||||
if (this.source) {
|
||||
const response = await fetch(`/meta/${this.source}`, {
|
||||
method: 'POST',
|
||||
body: this.rawData,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
await ifHot(async hot => {
|
||||
if (this.source === false) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
`The meta file for ${this.name} is missing\n`,
|
||||
`Make sure the file containing your scene is called "${this.name}.ts to match the generator function name`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.source) {
|
||||
console.warn(
|
||||
`The meta file for ${this.name} is missing\n`,
|
||||
`Make sure the file containing your scene is called "${this.name}.ts" to match the generator function name`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Meta.sourceLookup[this.source]) {
|
||||
console.warn(`Metadata for ${this.name} is already being updated`);
|
||||
return;
|
||||
}
|
||||
|
||||
const source = this.source;
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
delete Meta.sourceLookup[source];
|
||||
reject(`Connection timeout when updating metadata for ${this.name}`);
|
||||
}, 1000);
|
||||
Meta.sourceLookup[source] = () => {
|
||||
delete Meta.sourceLookup[source];
|
||||
resolve();
|
||||
};
|
||||
hot.send('motion-canvas:meta', {
|
||||
source,
|
||||
data: this.data.current,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static metaLookup: Record<string, Meta> = {};
|
||||
private static sourceLookup: Record<string, Callback> = {};
|
||||
|
||||
static {
|
||||
ifHot(hot => {
|
||||
hot.on('motion-canvas:meta-ack', ({source}) => {
|
||||
this.sourceLookup[source]?.();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Meta} object for the given entity.
|
||||
@@ -96,27 +124,25 @@ export class Meta<T extends Metadata = Metadata> {
|
||||
* Occurs during the initial load as well as during hot reloads.
|
||||
*
|
||||
* @param name - The Name of the entity this metadata refers to.
|
||||
* @param source - The path to the source file relative to the compilation
|
||||
* context.
|
||||
* @param source - The absolute path to the source file.
|
||||
* @param rawData - New metadata as JSON.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static register(name: string, source: string, rawData: string) {
|
||||
public static register(
|
||||
name: string,
|
||||
source: string | false,
|
||||
rawData: string,
|
||||
) {
|
||||
const meta = Meta.getMetaFor(name);
|
||||
meta.source = source;
|
||||
|
||||
if (meta.rawData === rawData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data: Metadata = JSON.parse(rawData);
|
||||
data.version ??= META_VERSION;
|
||||
data.version ||= META_VERSION;
|
||||
meta.data.current = data;
|
||||
meta.rawData = rawData;
|
||||
} catch (e) {
|
||||
console.error(`Error when parsing ${decodeURIComponent(source)}:`);
|
||||
console.error(`Error when parsing ${source}:`);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {Scene, SceneDescription} from './scenes';
|
||||
import {Meta, Metadata} from './Meta';
|
||||
import {ValueDispatcher} from './events';
|
||||
import {EventDispatcher, ValueDispatcher} from './events';
|
||||
import {Size, CanvasColorSpace} from './types';
|
||||
import {AudioManager} from './media';
|
||||
|
||||
export const ProjectSize = {
|
||||
FullHD: {width: 1920, height: 1080},
|
||||
@@ -10,26 +11,40 @@ export const ProjectSize = {
|
||||
export interface ProjectConfig {
|
||||
name: string;
|
||||
scenes: SceneDescription[];
|
||||
audio?: string;
|
||||
audioOffset?: number;
|
||||
canvas?: HTMLCanvasElement;
|
||||
size?: Size;
|
||||
background?: string | false;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export type ProjectMetadata = Metadata;
|
||||
|
||||
export class Project {
|
||||
/**
|
||||
* Triggered after the scenes were recalculated.
|
||||
*/
|
||||
public get onScenesChanged() {
|
||||
return this.scenes.subscribable;
|
||||
}
|
||||
private readonly scenes = new ValueDispatcher<Scene[]>([]);
|
||||
|
||||
/**
|
||||
* Triggered when the current scene changes.
|
||||
*/
|
||||
public get onCurrentSceneChanged() {
|
||||
return this.currentScene.subscribable;
|
||||
}
|
||||
private readonly currentScene = new ValueDispatcher<Scene>(null);
|
||||
|
||||
public readonly version = CORE_VERSION;
|
||||
/**
|
||||
* Triggered after any of the scenes were reloaded.
|
||||
*/
|
||||
public get onReloaded() {
|
||||
return this.reloaded;
|
||||
}
|
||||
private readonly reloaded = new EventDispatcher<void>();
|
||||
|
||||
public readonly meta: Meta<ProjectMetadata>;
|
||||
public frame = 0;
|
||||
|
||||
@@ -88,6 +103,7 @@ export class Project {
|
||||
this.height = height;
|
||||
}
|
||||
this.updateCanvas();
|
||||
this.reloadAll();
|
||||
}
|
||||
|
||||
public getSize(): Size {
|
||||
@@ -98,6 +114,7 @@ export class Project {
|
||||
}
|
||||
|
||||
public readonly name: string;
|
||||
public readonly audio = new AudioManager();
|
||||
private _resolutionScale = 1;
|
||||
private _colorSpace: CanvasColorSpace = 'srgb';
|
||||
private _speed = 1;
|
||||
@@ -111,27 +128,37 @@ export class Project {
|
||||
private width: number;
|
||||
private height: number;
|
||||
|
||||
public constructor(config: ProjectConfig) {
|
||||
this.setCanvas(config.canvas ?? null);
|
||||
this.setSize(
|
||||
config.width ?? ProjectSize.FullHD.width,
|
||||
config.height ?? ProjectSize.FullHD.height,
|
||||
);
|
||||
this.name = config.name;
|
||||
this.background = config.background ?? false;
|
||||
public constructor({
|
||||
name,
|
||||
scenes,
|
||||
audio,
|
||||
audioOffset,
|
||||
canvas,
|
||||
size = ProjectSize.FullHD,
|
||||
background = false,
|
||||
}: ProjectConfig) {
|
||||
this.setCanvas(canvas);
|
||||
this.setSize(size);
|
||||
this.name = name;
|
||||
this.background = background;
|
||||
this.meta = Meta.getMetaFor(PROJECT_FILE_NAME);
|
||||
|
||||
for (const scene of config.scenes) {
|
||||
if (audio) {
|
||||
this.audio.setSource(audio);
|
||||
}
|
||||
if (audioOffset) {
|
||||
this.audio.setOffset(audioOffset);
|
||||
}
|
||||
|
||||
for (const scene of scenes) {
|
||||
if (this.sceneLookup[scene.name]) {
|
||||
console.error('Duplicated scene name: ', scene.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.sceneLookup[scene.name] = new scene.klass(
|
||||
this,
|
||||
scene.name,
|
||||
scene.config,
|
||||
);
|
||||
const instance = new scene.klass(this, scene.name, scene.config);
|
||||
instance.onReloaded.subscribe(() => this.reloaded.dispatch());
|
||||
this.sceneLookup[scene.name] = instance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,13 +200,7 @@ export class Project {
|
||||
this.currentScene.current?.render(this.context, this.canvas);
|
||||
}
|
||||
|
||||
public reload(runners: SceneDescription[]) {
|
||||
for (const runner of runners) {
|
||||
this.sceneLookup[runner.name]?.reload(runner.config);
|
||||
}
|
||||
}
|
||||
|
||||
public reloadAll() {
|
||||
private reloadAll() {
|
||||
for (const scene of Object.values(this.sceneLookup)) {
|
||||
scene.reload();
|
||||
}
|
||||
@@ -265,6 +286,14 @@ export class Project {
|
||||
return finished;
|
||||
}
|
||||
|
||||
public syncAudio(frameOffset = 0) {
|
||||
this.audio.setTime(this.framesToSeconds(this.frame + frameOffset));
|
||||
}
|
||||
|
||||
public updateScene(description: SceneDescription) {
|
||||
this.sceneLookup[description.name]?.reload(description.config);
|
||||
}
|
||||
|
||||
private findBestScene(frame: number): Scene {
|
||||
let lastScene = null;
|
||||
for (const scene of Object.values(this.sceneLookup)) {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import './globals';
|
||||
import type {Size} from './types';
|
||||
import type {SceneDescription} from './scenes';
|
||||
import {Project, ProjectSize} from './Project';
|
||||
import {Player} from './player';
|
||||
import {hot} from './hot';
|
||||
import {AudioManager} from './media';
|
||||
|
||||
interface BootstrapConfig {
|
||||
name: string;
|
||||
scenes: SceneDescription[];
|
||||
size?: Size;
|
||||
background?: string | false;
|
||||
audio?: string;
|
||||
audioOffset?: number;
|
||||
}
|
||||
|
||||
export function bootstrap(config: BootstrapConfig) {
|
||||
const project = new Project({
|
||||
name: config.name,
|
||||
scenes: config.scenes,
|
||||
background: config.background ?? '#141414',
|
||||
...(config.size ?? ProjectSize.FullHD),
|
||||
});
|
||||
const audio = new AudioManager();
|
||||
if (config.audio) {
|
||||
audio.setSource(config.audio);
|
||||
}
|
||||
if (config.audioOffset) {
|
||||
audio.setOffset(config.audioOffset);
|
||||
}
|
||||
const player = new Player(project, audio);
|
||||
window.player = player;
|
||||
|
||||
let root: NodeModule = null;
|
||||
const queue = [...module.parents];
|
||||
while (queue.length > 0) {
|
||||
const path = queue.shift();
|
||||
const current = __webpack_require__.c[path];
|
||||
if (!path.endsWith('lib/index.js')) {
|
||||
root = current;
|
||||
break;
|
||||
}
|
||||
queue.push(...current.parents);
|
||||
}
|
||||
|
||||
if (root) {
|
||||
hot(player, root);
|
||||
} else {
|
||||
console.warn('Root module not found. Hot reload will not work.');
|
||||
}
|
||||
}
|
||||
@@ -1,90 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars,@typescript-eslint/no-namespace */
|
||||
|
||||
declare module '*.png' {
|
||||
const value: string;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module '*.png?img' {
|
||||
const value: Promise<HTMLImageElement>;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module '*.png?anim' {
|
||||
const value: Promise<HTMLImageElement[]>;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: string;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module '*.jpg?img' {
|
||||
const value: Promise<HTMLImageElement>;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module '*.jpg?anim' {
|
||||
const value: Promise<HTMLImageElement[]>;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module '*.jpeg' {
|
||||
const value: string;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module '*.jpeg?img' {
|
||||
const value: Promise<HTMLImageElement>;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module '*.jpeg?anim' {
|
||||
const value: Promise<HTMLImageElement[]>;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module '*.wav' {
|
||||
const value: string;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module '*.mp3' {
|
||||
const value: string;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module '*.ogg' {
|
||||
const value: string;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module '*.mp4' {
|
||||
const value: string;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module '*.csv' {
|
||||
const value: unknown;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module '*.glsl' {
|
||||
const value: string;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare interface Window {
|
||||
player: import('./player/Player').Player;
|
||||
}
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface Module {
|
||||
parents: string[];
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace JSX {
|
||||
type ElementClass = import('konva/lib/Node').Node;
|
||||
interface ElementChildrenAttribute {
|
||||
@@ -96,10 +11,6 @@ declare type Callback = (...args: unknown[]) => void;
|
||||
|
||||
declare const PROJECT_FILE_NAME: string;
|
||||
|
||||
declare const CORE_VERSION: string;
|
||||
|
||||
declare const META_VERSION: number;
|
||||
|
||||
declare module 'colorjs.io' {
|
||||
const noTypesYet: any;
|
||||
export default noTypesYet;
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import type {Player} from './player';
|
||||
|
||||
export function hot(player: Player, root: NodeModule) {
|
||||
const updateScenes = async (modules: string[]) => {
|
||||
const runners = [];
|
||||
for (const module of modules) {
|
||||
const runner = __webpack_require__(module).default;
|
||||
if (
|
||||
// FIXME Find a better way to detect runner factories.
|
||||
runner.name === '__WEBPACK_DEFAULT_EXPORT__' &&
|
||||
typeof runner === 'function'
|
||||
) {
|
||||
runners.push(await runner());
|
||||
} else {
|
||||
runners.push(runner);
|
||||
}
|
||||
}
|
||||
|
||||
player.project.reload(runners);
|
||||
player.reload();
|
||||
};
|
||||
|
||||
const updateAudio = async (modules: string[]) => {
|
||||
let src = null;
|
||||
for (const module of modules) {
|
||||
const audio = __webpack_require__(module);
|
||||
if (typeof audio === 'string') {
|
||||
src = audio;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (src) {
|
||||
player.audio.setSource(src);
|
||||
}
|
||||
};
|
||||
|
||||
const scenePaths = __webpack_require__.c[root.id].children.filter(
|
||||
(name: string) => name.match(/\.scene\.[jt]sx?/),
|
||||
);
|
||||
|
||||
const audioPaths = __webpack_require__.c[root.id].children.filter(
|
||||
(name: string) => name.match(/\.wav/),
|
||||
);
|
||||
|
||||
root.hot.accept(scenePaths, updateScenes);
|
||||
module.hot.accept(scenePaths, updateScenes);
|
||||
root.hot.accept(audioPaths, updateAudio);
|
||||
module.hot.accept(audioPaths, updateAudio);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './bootstrap';
|
||||
export * from './Meta';
|
||||
export * from './Project';
|
||||
export * from './symbols';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {AudioData} from './AudioData';
|
||||
import {ValueDispatcher} from '../events';
|
||||
import {ifHot} from '../utils';
|
||||
|
||||
export class AudioManager {
|
||||
public get onDataChanged() {
|
||||
@@ -18,6 +19,16 @@ export class AudioManager {
|
||||
private error = false;
|
||||
private abortController: AbortController = null;
|
||||
|
||||
public constructor() {
|
||||
ifHot(hot => {
|
||||
hot.on('motion-canvas:assets', ({urls}) => {
|
||||
if (urls.includes(this.source)) {
|
||||
this.setSource(this.source);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public getTime() {
|
||||
return this.toAbsoluteTime(this.audioElement.currentTime);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import {Size} from '../types';
|
||||
|
||||
const imageLookup: Record<string, HTMLImageElement> = {};
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
export type ImageDataSource = CanvasImageSource & Size;
|
||||
|
||||
export async function loadImage(source: string): Promise<HTMLImageElement> {
|
||||
if (!imageLookup[source]) {
|
||||
const image = new Image();
|
||||
imageLookup[source] = image;
|
||||
image.src = source;
|
||||
await new Promise(resolve => (image.onload = resolve));
|
||||
}
|
||||
|
||||
return imageLookup[source];
|
||||
export function loadImage(source: string): Promise<HTMLImageElement> {
|
||||
const image = new Image();
|
||||
image.src = source;
|
||||
return new Promise((resolve, reject) => {
|
||||
if (image.complete) {
|
||||
resolve(image);
|
||||
} else {
|
||||
image.onload = () => resolve(image);
|
||||
image.onerror = reject;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function loadAnimation(sources: string[]): Promise<HTMLImageElement[]> {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import './Factory';
|
||||
import './Node';
|
||||
import './Shape';
|
||||
import './Container';
|
||||
@@ -1,4 +1,3 @@
|
||||
import {AudioManager} from '../media';
|
||||
import type {Project} from '../Project';
|
||||
import {
|
||||
AsyncEventDispatcher,
|
||||
@@ -79,25 +78,24 @@ export class Player {
|
||||
recalculate: true,
|
||||
};
|
||||
|
||||
public constructor(
|
||||
public readonly project: Project,
|
||||
public readonly audio: AudioManager,
|
||||
) {
|
||||
public constructor(public readonly project: Project) {
|
||||
this.startTime = performance.now();
|
||||
this.project.framerate = this.state.current.fps;
|
||||
this.project.resolutionScale = this.state.current.scale;
|
||||
this.project.colorSpace = this.state.current.colorSpace;
|
||||
// TODO Recalculation should be handled by the project.
|
||||
this.project.onReloaded.subscribe(() => this.reload());
|
||||
|
||||
this.request();
|
||||
}
|
||||
|
||||
public loadState(state: Partial<PlayerState>) {
|
||||
this.updateState(state);
|
||||
this.project.speed = state.speed;
|
||||
this.project.framerate = state.fps;
|
||||
this.project.resolutionScale = state.scale;
|
||||
this.project.colorSpace = state.colorSpace;
|
||||
this.setRange(state.startFrame, state.endFrame);
|
||||
this.project.speed = this.state.current.speed;
|
||||
this.project.framerate = this.state.current.fps;
|
||||
this.project.resolutionScale = this.state.current.scale;
|
||||
this.project.colorSpace = this.state.current.colorSpace;
|
||||
this.setRange(this.state.current.startFrame, this.state.current.endFrame);
|
||||
}
|
||||
|
||||
private updateState(newState: Partial<PlayerState>) {
|
||||
@@ -125,7 +123,7 @@ export class Player {
|
||||
return commands;
|
||||
}
|
||||
|
||||
public reload() {
|
||||
private reload() {
|
||||
this.commands.recalculate = true;
|
||||
if (this.requestId === null) {
|
||||
this.request();
|
||||
@@ -262,11 +260,11 @@ export class Player {
|
||||
state.paused ||
|
||||
state.finished ||
|
||||
state.render ||
|
||||
!this.audio.isInRange(this.project.time);
|
||||
if (await this.audio.setPaused(audioPaused)) {
|
||||
this.syncAudio(-3);
|
||||
!this.project.audio.isInRange(this.project.time);
|
||||
if (await this.project.audio.setPaused(audioPaused)) {
|
||||
this.project.syncAudio(-3);
|
||||
}
|
||||
this.audio.setMuted(state.muted);
|
||||
this.project.audio.setMuted(state.muted);
|
||||
|
||||
// Rendering
|
||||
if (state.render) {
|
||||
@@ -295,27 +293,29 @@ export class Player {
|
||||
console.time('seek time');
|
||||
state.finished = await this.project.seek(clampedFrame);
|
||||
console.timeEnd('seek time');
|
||||
this.syncAudio(-3);
|
||||
this.project.syncAudio(-3);
|
||||
}
|
||||
// Do nothing if paused or is ahead of the audio.
|
||||
else if (
|
||||
state.paused ||
|
||||
(state.speed === 1 &&
|
||||
this.audio.isReady() &&
|
||||
this.audio.isInRange(this.project.time) &&
|
||||
this.audio.getTime() < this.project.time)
|
||||
this.project.audio.isReady() &&
|
||||
this.project.audio.isInRange(this.project.time) &&
|
||||
this.project.audio.getTime() < this.project.time)
|
||||
) {
|
||||
this.request();
|
||||
return;
|
||||
}
|
||||
// Seek to synchronize animation with audio.
|
||||
else if (
|
||||
this.audio.isReady() &&
|
||||
this.project.audio.isReady() &&
|
||||
state.speed === 1 &&
|
||||
this.audio.isInRange(this.project.time) &&
|
||||
this.project.time < this.audio.getTime() - MAX_AUDIO_DESYNC
|
||||
this.project.audio.isInRange(this.project.time) &&
|
||||
this.project.time < this.project.audio.getTime() - MAX_AUDIO_DESYNC
|
||||
) {
|
||||
const seekFrame = this.project.secondsToFrames(this.audio.getTime());
|
||||
const seekFrame = this.project.secondsToFrames(
|
||||
this.project.audio.getTime(),
|
||||
);
|
||||
state.finished = await this.project.seek(seekFrame);
|
||||
}
|
||||
// Simply move forward one frame
|
||||
@@ -324,7 +324,7 @@ export class Player {
|
||||
|
||||
// Synchronize audio.
|
||||
if (state.speed !== 1) {
|
||||
this.syncAudio();
|
||||
this.project.syncAudio();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,12 +358,6 @@ export class Player {
|
||||
return frame >= state.startFrame && frame <= state.endFrame;
|
||||
}
|
||||
|
||||
private syncAudio(frameOffset = 0) {
|
||||
this.audio.setTime(
|
||||
this.project.framesToSeconds(this.project.frame + frameOffset),
|
||||
);
|
||||
}
|
||||
|
||||
private request() {
|
||||
this.requestId = requestAnimationFrame(async time => {
|
||||
if (time - this.renderTime >= 990 / this.state.current.fps) {
|
||||
|
||||
@@ -201,7 +201,6 @@ export class TimeEvents {
|
||||
this.previousReference = data.timeEvents;
|
||||
this.load(data.timeEvents ?? []);
|
||||
this.scene.reload();
|
||||
window.player.reload();
|
||||
};
|
||||
|
||||
private load(events: SavedTimeEvent[]) {
|
||||
|
||||
2
packages/core/src/setup.test.ts
Normal file
2
packages/core/src/setup.test.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
jest.mock('./utils/ifHot');
|
||||
global.AudioContext = class {} as new () => AudioContext;
|
||||
3
packages/core/src/utils/__mocks__/ifHot.ts
Normal file
3
packages/core/src/utils/__mocks__/ifHot.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function ifHot() {
|
||||
// do nothing
|
||||
}
|
||||
23
packages/core/src/utils/ifHot.ts
Normal file
23
packages/core/src/utils/ifHot.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type {ViteHotContext} from 'vite/types/hot';
|
||||
|
||||
/**
|
||||
* Invoke the given callback if hot module replacement is enabled.
|
||||
*
|
||||
* @remarks
|
||||
* This helper function should be used instead of accessing `import.meta.hot`
|
||||
* directly. CommonJS doesn't support `import.meta` which makes the tests fail.
|
||||
* It's infinitely easier to mock this function out than to run jest on Node
|
||||
* with ES modules :(
|
||||
*
|
||||
* @param callback - The callback to be invoked if HMR is enabled.
|
||||
*
|
||||
* @typeParam T - The type returned by the callback or `void` if HMR is
|
||||
* disabled.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function ifHot<T>(callback: (hot: ViteHotContext) => T): T | void {
|
||||
if (import.meta.hot) {
|
||||
return callback(import.meta.hot);
|
||||
}
|
||||
}
|
||||
@@ -6,3 +6,4 @@ export * from './useScene';
|
||||
export * from './useThread';
|
||||
export * from './useTime';
|
||||
export * from './useContext';
|
||||
export * from './ifHot';
|
||||
|
||||
11
packages/core/src/vite-env.d.ts
vendored
Normal file
11
packages/core/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import 'vite/types/customEvent';
|
||||
|
||||
declare module 'vite/types/customEvent' {
|
||||
interface CustomEventMap {
|
||||
'motion-canvas:meta': {source: string; data: import('./Meta').Metadata};
|
||||
'motion-canvas:meta-ack': {source: string};
|
||||
'motion-canvas:assets': {urls: string[]};
|
||||
}
|
||||
}
|
||||
@@ -18,14 +18,7 @@
|
||||
"paths": {
|
||||
"@motion-canvas/core/lib/jsx-runtime": ["jsx-runtime.ts"]
|
||||
},
|
||||
"types": [
|
||||
"node",
|
||||
"prismjs",
|
||||
"three",
|
||||
"webpack-env",
|
||||
"dom-webcodecs",
|
||||
"jest"
|
||||
]
|
||||
"types": ["node", "prismjs", "three", "dom-webcodecs", "jest"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"module": "esnext",
|
||||
"target": "es2020",
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
||||
@@ -1,27 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
|
||||
"tagDefinitions": [
|
||||
{
|
||||
"tagName": "@module",
|
||||
"syntaxKind": "modifier"
|
||||
},
|
||||
{
|
||||
"tagName": "@ignore",
|
||||
"syntaxKind": "modifier"
|
||||
}
|
||||
],
|
||||
"supportForTags": {
|
||||
"@module": true,
|
||||
"@param": true,
|
||||
"@deprecated": true,
|
||||
"@returns": true,
|
||||
"@remarks": true,
|
||||
"@example": true,
|
||||
"@link": true,
|
||||
"@internal": true,
|
||||
"@eventProperty": true,
|
||||
"@typeParam": true,
|
||||
"@ignore": true,
|
||||
"@inheritDoc": true
|
||||
}
|
||||
"extends": ["../../tsdoc.json"]
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"out": "api",
|
||||
"excludeExternals": true,
|
||||
"entryPoints": [
|
||||
"src",
|
||||
"src/animations",
|
||||
"src/components",
|
||||
"src/decorators",
|
||||
"src/events",
|
||||
"src/flow",
|
||||
"src/helpers",
|
||||
"src/media",
|
||||
"src/player",
|
||||
"src/scenes",
|
||||
"src/styles",
|
||||
"src/themes",
|
||||
"src/threading",
|
||||
"src/transitions",
|
||||
"src/tweening",
|
||||
"src/types",
|
||||
"src/utils"
|
||||
]
|
||||
}
|
||||
4
packages/docs/tsdoc.json
Normal file
4
packages/docs/tsdoc.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
|
||||
"extends": ["../../tsdoc.json"]
|
||||
}
|
||||
@@ -7,11 +7,15 @@
|
||||
"license": "MIT",
|
||||
"main": "src/project.ts",
|
||||
"scripts": {
|
||||
"serve": "motion-canvas src/project.ts",
|
||||
"ui": "motion-canvas src/project.ts --ui-server"
|
||||
"serve": "vite",
|
||||
"build": "tsc && vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@motion-canvas/core": "*",
|
||||
"@motion-canvas/ui": "*"
|
||||
"@motion-canvas/core": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@motion-canvas/ui": "*",
|
||||
"@motion-canvas/vite-plugin": "*",
|
||||
"vite": "^3.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
1
packages/template/src/motion-canvas.d.ts
vendored
Normal file
1
packages/template/src/motion-canvas.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="@motion-canvas/core/project" />
|
||||
@@ -1,9 +1,9 @@
|
||||
import '@motion-canvas/core/lib/patches';
|
||||
import {bootstrap} from '@motion-canvas/core/lib/bootstrap';
|
||||
import {Project} from '@motion-canvas/core/lib';
|
||||
|
||||
import example from './scenes/example.scene';
|
||||
|
||||
bootstrap({
|
||||
name: 'base-project',
|
||||
export default new Project({
|
||||
name: 'project',
|
||||
scenes: [example],
|
||||
background: '#141414',
|
||||
});
|
||||
|
||||
@@ -2,13 +2,7 @@
|
||||
"extends": "@motion-canvas/core/tsconfig.project.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"types": [
|
||||
"node",
|
||||
"prismjs",
|
||||
"three",
|
||||
"webpack-env",
|
||||
"dom-webcodecs",
|
||||
"jest"
|
||||
]
|
||||
}
|
||||
"types": ["node", "prismjs", "three", "dom-webcodecs"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
4
packages/template/tsdoc.json
Normal file
4
packages/template/tsdoc.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
|
||||
"extends": ["../../tsdoc.json"]
|
||||
}
|
||||
6
packages/template/vite.config.ts
Normal file
6
packages/template/vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import {defineConfig} from 'vite';
|
||||
import motionCanvas from '@motion-canvas/vite-plugin';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [motionCanvas()],
|
||||
});
|
||||
17
packages/ui/editor.html
Normal file
17
packages/ui/editor.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAOwAAADsAEnxA+tAAABKklEQVQ4jcWTvUoDURCFv/y0A2tnZ95AW7FZwQdQSO9aRLAJbp0mPkGCTSCBmPQW8Q1SuKTU4AvkEeQO2K6MuSFmN+pCCg9cLhdmzpwzh1tK05RdUN6p+yeCftKK+kmrW4SguvGaNc3PGGRUVEE18x4Dl9dlheP76OZxMQUCIOzVa++/W2jENYaV7oqEWTMETMmhv7fi+w4i4IVhxaaeAq+9es0aL4CJqoaqOsmSrGNsxCZ16ideMeiMVPVrJyISqerC7IhIsN3CoGMeTfYceKARn6/sqOozcADkktmMcU3y5KeZrQQ4ARxwpqpts5O3kIEvGvnJ1vwB7PuquYgc5RVs4tZHeAe8WbOIlIA9r3IJU/DXcc61nXOpcy7I1hb9C5aOLTeHf/6NwCdua48fJxuYPgAAAABJRU5ErkJggg=="
|
||||
/>
|
||||
<title>Motion Canvas</title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module" src="/@id/__x00__virtual:editor"></script>
|
||||
</body>
|
||||
</html>
|
||||
21
packages/ui/index.html
Normal file
21
packages/ui/index.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAOwAAADsAEnxA+tAAABKklEQVQ4jcWTvUoDURCFv/y0A2tnZ95AW7FZwQdQSO9aRLAJbp0mPkGCTSCBmPQW8Q1SuKTU4AvkEeQO2K6MuSFmN+pCCg9cLhdmzpwzh1tK05RdUN6p+yeCftKK+kmrW4SguvGaNc3PGGRUVEE18x4Dl9dlheP76OZxMQUCIOzVa++/W2jENYaV7oqEWTMETMmhv7fi+w4i4IVhxaaeAq+9es0aL4CJqoaqOsmSrGNsxCZ16ideMeiMVPVrJyISqerC7IhIsN3CoGMeTfYceKARn6/sqOozcADkktmMcU3y5KeZrQQ4ARxwpqpts5O3kIEvGvnJ1vwB7PuquYgc5RVs4tZHeAe8WbOIlIA9r3IJU/DXcc61nXOpcy7I1hb9C5aOLTeHf/6NwCdua48fJxuYPgAAAABJRU5ErkJggg=="
|
||||
/>
|
||||
<title>Editor | Motion Canvas</title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module">
|
||||
import project from '@motion-canvas/template/dist/template.mjs';
|
||||
import editor from '/src/main.tsx';
|
||||
editor(project);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,15 +1,16 @@
|
||||
{
|
||||
"name": "@motion-canvas/ui",
|
||||
"version": "9.1.2",
|
||||
"description": "User Interface for Motion Canvas",
|
||||
"description": "A visual editor for Motion Canvas",
|
||||
"main": "dist/main.js",
|
||||
"types": "types/main.d.ts",
|
||||
"author": "motion-canvas",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "webpack",
|
||||
"serve": "webpack serve",
|
||||
"build": "webpack --mode=production",
|
||||
"watch": "webpack -w"
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://npm.pkg.github.com/motion-canvas"
|
||||
@@ -19,20 +20,18 @@
|
||||
"url": "https://github.com/motion-canvas/motion-canvas.git"
|
||||
},
|
||||
"files": [
|
||||
"dist/main.js"
|
||||
"dist",
|
||||
"types",
|
||||
"editor.html"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@motion-canvas/core": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@motion-canvas/core": "^9.1.2",
|
||||
"css-loader": "^6.7.1",
|
||||
"preact": "^10.7.3",
|
||||
"sass": "^1.52.2",
|
||||
"sass-loader": "^13.0.0",
|
||||
"strongly-typed-events": "^3.0.2",
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-loader": "^9.3.0",
|
||||
"typescript": "^4.6.3",
|
||||
"webpack": "^5.73.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.9.1"
|
||||
"@preact/preset-vite": "^2.3.0",
|
||||
"preact": "10.7.3",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^3.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,5 +39,3 @@ export function App() {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const AppNode = <App />;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import styles from './Playback.module.scss';
|
||||
|
||||
import {IconType, IconButton, IconCheckbox} from '../controls';
|
||||
import {useDocumentEvent, usePlayer, usePlayerState} from '../../hooks';
|
||||
import {useDocumentEvent, usePlayerState} from '../../hooks';
|
||||
import {Select, Input} from '../controls';
|
||||
import {Framerate} from './Framerate';
|
||||
import {useCallback} from 'preact/hooks';
|
||||
import {classes} from '../../utils';
|
||||
import {usePlayer} from '../../contexts';
|
||||
|
||||
export function PlaybackControls() {
|
||||
const player = usePlayer();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {usePlayer, usePlayerState, useSubscribable} from '../../hooks';
|
||||
import {usePlayerState, useSubscribable} from '../../hooks';
|
||||
import {Button, Group, Input, Label, Select} from '../controls';
|
||||
import {Pane} from '../tabs';
|
||||
import {usePlayer} from '../../contexts';
|
||||
import type {CanvasColorSpace} from '@motion-canvas/core/lib/types';
|
||||
|
||||
export function Rendering() {
|
||||
@@ -81,8 +82,6 @@ export function Rendering() {
|
||||
onChange={event => {
|
||||
const value = parseInt((event.target as HTMLInputElement).value);
|
||||
player.project.setSize(value, height);
|
||||
player.project.reloadAll();
|
||||
player.reload();
|
||||
}}
|
||||
/>
|
||||
X
|
||||
@@ -93,8 +92,6 @@ export function Rendering() {
|
||||
onChange={event => {
|
||||
const value = parseInt((event.target as HTMLInputElement).value);
|
||||
player.project.setSize(width, value);
|
||||
player.project.reloadAll();
|
||||
player.reload();
|
||||
}}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
@@ -2,18 +2,19 @@ import styles from './Timeline.module.scss';
|
||||
|
||||
import {useContext, useLayoutEffect, useMemo, useRef} from 'preact/hooks';
|
||||
import {TimelineContext} from './TimelineContext';
|
||||
import {usePlayer, useSubscribableValue} from '../../hooks';
|
||||
import {useSubscribableValue} from '../../hooks';
|
||||
import {useProject} from '../../contexts';
|
||||
|
||||
const HEIGHT = 48;
|
||||
|
||||
export function AudioTrack() {
|
||||
const ref = useRef<HTMLCanvasElement>();
|
||||
const {project, audio} = usePlayer();
|
||||
const project = useProject();
|
||||
const context = useMemo(() => ref.current?.getContext('2d'), [ref.current]);
|
||||
const {viewLength, startFrame, endFrame, duration, density} =
|
||||
useContext(TimelineContext);
|
||||
|
||||
const audioData = useSubscribableValue(audio.onDataChanged);
|
||||
const audioData = useSubscribableValue(project.audio.onDataChanged);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!context) return;
|
||||
@@ -26,10 +27,10 @@ export function AudioTrack() {
|
||||
context.moveTo(0, HEIGHT);
|
||||
|
||||
const start =
|
||||
audio.toRelativeTime(project.framesToSeconds(startFrame)) *
|
||||
project.audio.toRelativeTime(project.framesToSeconds(startFrame)) *
|
||||
audioData.sampleRate;
|
||||
const end =
|
||||
audio.toRelativeTime(project.framesToSeconds(endFrame)) *
|
||||
project.audio.toRelativeTime(project.framesToSeconds(endFrame)) *
|
||||
audioData.sampleRate;
|
||||
|
||||
const flooredStart = ~~start;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import styles from './Timeline.module.scss';
|
||||
|
||||
import type {Scene, TimeEvent} from '@motion-canvas/core/lib/scenes';
|
||||
import {useDrag, usePlayer} from '../../hooks';
|
||||
import {useDrag} from '../../hooks';
|
||||
import {useCallback, useContext, useLayoutEffect, useState} from 'preact/hooks';
|
||||
import {TimelineContext} from './TimelineContext';
|
||||
import {usePlayer} from '../../contexts';
|
||||
|
||||
interface LabelProps {
|
||||
event: TimeEvent;
|
||||
@@ -27,7 +28,6 @@ export function Label({event, scene}: LabelProps) {
|
||||
const newFrame = Math.max(0, eventTime);
|
||||
if (event.offset !== newFrame) {
|
||||
scene.timeEvents.set(event.name, newFrame, e.shiftKey);
|
||||
player.reload();
|
||||
}
|
||||
},
|
||||
[event, eventTime],
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import styles from './Timeline.module.scss';
|
||||
|
||||
import {useDrag, usePlayer, usePlayerState} from '../../hooks';
|
||||
import {useDrag, usePlayerState} from '../../hooks';
|
||||
import {useCallback, useContext, useEffect, useState} from 'preact/hooks';
|
||||
import {Icon, IconType} from '../controls';
|
||||
import {TimelineContext} from './TimelineContext';
|
||||
import {usePlayer} from '../../contexts';
|
||||
|
||||
export function RangeTrack() {
|
||||
const {fullLength} = useContext(TimelineContext);
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import styles from './Timeline.module.scss';
|
||||
|
||||
import type {Scene} from '@motion-canvas/core/lib/scenes';
|
||||
import {
|
||||
usePlayer,
|
||||
usePlayerState,
|
||||
useScenes,
|
||||
useSubscribableValue,
|
||||
} from '../../hooks';
|
||||
import {usePlayerState, useScenes, useSubscribableValue} from '../../hooks';
|
||||
import {usePlayer} from '../../contexts';
|
||||
|
||||
export function SceneTrack() {
|
||||
const scenes = useScenes();
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from 'preact/hooks';
|
||||
import {
|
||||
useDocumentEvent,
|
||||
usePlayer,
|
||||
usePlayerState,
|
||||
useSize,
|
||||
useStateChange,
|
||||
@@ -22,6 +21,7 @@ import {RangeTrack} from './RangeTrack';
|
||||
import {TimelineContext, TimelineState} from './TimelineContext';
|
||||
import {clamp} from '@motion-canvas/core/lib/tweening';
|
||||
import {AudioTrack} from './AudioTrack';
|
||||
import {usePlayer} from '../../contexts';
|
||||
|
||||
const ZOOM_SPEED = 0.1;
|
||||
|
||||
@@ -68,7 +68,9 @@ export function Timeline() {
|
||||
if (prevWidth !== 0 && rect.width !== 0) {
|
||||
newScale *= prevWidth / rect.width;
|
||||
}
|
||||
setScale(Math.max(1, newScale));
|
||||
if (!isNaN(newScale)) {
|
||||
setScale(Math.max(1, newScale));
|
||||
}
|
||||
},
|
||||
[duration, rect.width],
|
||||
);
|
||||
@@ -119,8 +121,12 @@ export function Timeline() {
|
||||
);
|
||||
|
||||
containerRef.current.scrollLeft = newOffset;
|
||||
setScale(newScale);
|
||||
setOffset(newOffset);
|
||||
if (!isNaN(newScale)) {
|
||||
setScale(newScale);
|
||||
}
|
||||
if (!isNaN(newOffset)) {
|
||||
setOffset(newOffset);
|
||||
}
|
||||
playheadRef.current.style.left = `${
|
||||
event.x - rect.x + newOffset
|
||||
}px`;
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
useCurrentScene,
|
||||
useDocumentEvent,
|
||||
useDrag,
|
||||
usePlayer,
|
||||
useSize,
|
||||
useStorage,
|
||||
useSubscribable,
|
||||
@@ -16,6 +15,7 @@ import {Grid} from './Grid';
|
||||
import styles from './Viewport.module.scss';
|
||||
import {ViewportContext, ViewportState} from './ViewportContext';
|
||||
import {isInspectable} from '@motion-canvas/core/lib/scenes/Inspectable';
|
||||
import {usePlayer} from '../../contexts';
|
||||
|
||||
const ZOOM_SPEED = 0.1;
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {usePlayer, usePlayerState} from '../../hooks';
|
||||
import {usePlayerState} from '../../hooks';
|
||||
import {PlaybackControls, PlaybackProgress} from '../playback';
|
||||
import {CurrentTime} from '../playback/CurrentTime';
|
||||
import {View} from './View';
|
||||
import styles from './Viewport.module.scss';
|
||||
import {usePlayer} from '../../contexts';
|
||||
|
||||
export function Viewport() {
|
||||
const player = usePlayer();
|
||||
|
||||
2
packages/ui/src/contexts/index.ts
Normal file
2
packages/ui/src/contexts/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './player';
|
||||
export * from './project';
|
||||
21
packages/ui/src/contexts/player.tsx
Normal file
21
packages/ui/src/contexts/player.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import {ComponentChildren, createContext} from 'preact';
|
||||
import {useContext} from 'preact/hooks';
|
||||
import type {Player} from '@motion-canvas/core/lib/player';
|
||||
|
||||
const PlayerContext = createContext<Player | null>(null);
|
||||
|
||||
export function usePlayer() {
|
||||
return useContext(PlayerContext);
|
||||
}
|
||||
|
||||
export function PlayerProvider({
|
||||
player,
|
||||
children,
|
||||
}: {
|
||||
player: Player;
|
||||
children: ComponentChildren;
|
||||
}) {
|
||||
return (
|
||||
<PlayerContext.Provider value={player}>{children}</PlayerContext.Provider>
|
||||
);
|
||||
}
|
||||
23
packages/ui/src/contexts/project.tsx
Normal file
23
packages/ui/src/contexts/project.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import {ComponentChildren, createContext} from 'preact';
|
||||
import {useContext} from 'preact/hooks';
|
||||
import type {Project} from '@motion-canvas/core/lib';
|
||||
|
||||
const ProjectContext = createContext<Project | null>(null);
|
||||
|
||||
export function useProject() {
|
||||
return useContext(ProjectContext);
|
||||
}
|
||||
|
||||
export function ProjectProvider({
|
||||
project,
|
||||
children,
|
||||
}: {
|
||||
project: Project;
|
||||
children: ComponentChildren;
|
||||
}) {
|
||||
return (
|
||||
<ProjectContext.Provider value={project}>
|
||||
{children}
|
||||
</ProjectContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,6 @@ export * from './useCurrentFrame';
|
||||
export * from './useDocumentEvent';
|
||||
export * from './useDrag';
|
||||
export * from './useMeta';
|
||||
export * from './usePlayer';
|
||||
export * from './usePlayerState';
|
||||
export * from './usePlayerTime';
|
||||
export * from './useScenes';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useSubscribableValue} from './useSubscribable';
|
||||
import {usePlayer} from './usePlayer';
|
||||
import {usePlayer} from '../contexts';
|
||||
|
||||
export function useCurrentFrame() {
|
||||
const player = usePlayer();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {Meta, Metadata} from '@motion-canvas/core/lib';
|
||||
import {useCallback} from 'preact/hooks';
|
||||
import {usePlayer} from './usePlayer';
|
||||
import {useSubscribableValue} from './useSubscribable';
|
||||
import {usePlayer} from '../contexts';
|
||||
|
||||
/**
|
||||
* Get a stateful value representing the contents of the given meta file and
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import type {Player} from '@motion-canvas/core/lib/player/Player';
|
||||
|
||||
export function usePlayer(): Player {
|
||||
return (<{player: Player}>(<unknown>window)).player;
|
||||
}
|
||||
@@ -1,18 +1,7 @@
|
||||
import type {PlayerState} from '@motion-canvas/core/lib/player/Player';
|
||||
import {usePlayer} from './usePlayer';
|
||||
import {usePlayer} from '../contexts';
|
||||
import {useSubscribableValue} from './useSubscribable';
|
||||
|
||||
const player = usePlayer();
|
||||
const storageKey = `${player.project.name}-player-state`;
|
||||
const savedState = localStorage.getItem(storageKey);
|
||||
if (savedState) {
|
||||
const state = JSON.parse(savedState) as PlayerState;
|
||||
player.loadState(state);
|
||||
}
|
||||
|
||||
player.onStateChanged.subscribe(state => {
|
||||
localStorage.setItem(storageKey, JSON.stringify(state));
|
||||
});
|
||||
// TODO Save and restore the player state.
|
||||
|
||||
export function usePlayerState() {
|
||||
const player = usePlayer();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {usePlayer} from './usePlayer';
|
||||
import {useSubscribableValue} from './useSubscribable';
|
||||
import {usePlayer} from '../contexts';
|
||||
|
||||
export function useScenes() {
|
||||
const player = usePlayer();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useCallback, useMemo, useState} from 'preact/hooks';
|
||||
import {usePlayer} from './usePlayer';
|
||||
import {usePlayer} from '../contexts';
|
||||
|
||||
export function useStorage<T>(
|
||||
id: string,
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import {render} from 'preact';
|
||||
import {AppNode} from './App';
|
||||
|
||||
const app = document.createElement('main');
|
||||
document.body.appendChild(app);
|
||||
render(AppNode, app);
|
||||
20
packages/ui/src/main.tsx
Normal file
20
packages/ui/src/main.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type {Project} from '@motion-canvas/core/lib';
|
||||
import {Player} from '@motion-canvas/core/lib/player';
|
||||
import {render} from 'preact';
|
||||
import {App} from './App';
|
||||
import {PlayerProvider, ProjectProvider} from './contexts';
|
||||
|
||||
export default (project: Project) => {
|
||||
const app = document.createElement('main');
|
||||
const player = new Player(project);
|
||||
document.body.appendChild(app);
|
||||
document.title = `${project.name} | Motion Canvas`;
|
||||
render(
|
||||
<PlayerProvider player={player}>
|
||||
<ProjectProvider project={project}>
|
||||
<App />
|
||||
</ProjectProvider>
|
||||
</PlayerProvider>,
|
||||
app,
|
||||
);
|
||||
};
|
||||
1
packages/ui/src/vite-env.d.ts
vendored
Normal file
1
packages/ui/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,15 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"outDir": "lib",
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": true,
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"useDefineForClassFields": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact"
|
||||
}
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "preact",
|
||||
"types": ["node", "preact"],
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
4
packages/ui/tsdoc.json
Normal file
4
packages/ui/tsdoc.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
|
||||
"extends": ["../../tsdoc.json"]
|
||||
}
|
||||
3
packages/ui/types/main.d.ts
vendored
Normal file
3
packages/ui/types/main.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import type {Project} from '@motion-canvas/core/lib';
|
||||
declare const _default: (project: Project) => void;
|
||||
export default _default;
|
||||
16
packages/ui/vite.config.ts
Normal file
16
packages/ui/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {defineConfig} from 'vite';
|
||||
import preact from '@preact/preset-vite';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: 'src/main.tsx',
|
||||
formats: ['es'],
|
||||
fileName: 'main',
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['@motion-canvas/core'],
|
||||
},
|
||||
},
|
||||
plugins: [preact()],
|
||||
});
|
||||
29
packages/vite-plugin/package.json
Normal file
29
packages/vite-plugin/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@motion-canvas/vite-plugin",
|
||||
"version": "9.1.1",
|
||||
"description": "A Vite plugin for Motion Canvas projects",
|
||||
"main": "lib/main.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"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"vite": "3.x"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^3.0.5"
|
||||
}
|
||||
}
|
||||
259
packages/vite-plugin/src/main.ts
Normal file
259
packages/vite-plugin/src/main.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import type {Plugin, ResolvedConfig} from 'vite';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import {Readable} from 'stream';
|
||||
|
||||
export interface MotionCanvasPluginConfig {
|
||||
/**
|
||||
* The import path of the project file.
|
||||
*
|
||||
* @remarks
|
||||
* The file must contain a default export exposing an instance of the
|
||||
* {@link Project} class.
|
||||
*
|
||||
* @default './src/project.ts'
|
||||
*/
|
||||
project?: string;
|
||||
/**
|
||||
* Defines which assets should be buffered before being sent to the browser.
|
||||
*
|
||||
* @remarks
|
||||
* Streaming larger assets directly from the drive my cause issues with other
|
||||
* applications. For instance, if an audio file is being used in the project,
|
||||
* Adobe Audition will perceive it as "being used by another application"
|
||||
* and refuse to override it.
|
||||
*
|
||||
* Buffered assets are first loaded to the memory and then streamed from
|
||||
* there. This leaves the original files open for modification with hot module
|
||||
* replacement still working.
|
||||
*
|
||||
* @default /\.(wav|mp3|ogg)$/
|
||||
*/
|
||||
bufferedAssets?: RegExp;
|
||||
editor?: {
|
||||
/**
|
||||
* The import path of the editor factory file.
|
||||
*
|
||||
* @remarks
|
||||
* The file must contain a default export exposing a factory function.
|
||||
* This function will be called with the project as its first argument.
|
||||
* Its task is to create the user interface.
|
||||
*
|
||||
* @default '\@motion-canvas/ui'
|
||||
*/
|
||||
factory?: string;
|
||||
/**
|
||||
* The import path of the editor styles.
|
||||
*
|
||||
* @default '\@motion-canvas/ui/dist/style.css'
|
||||
*/
|
||||
styles?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default ({
|
||||
project = './src/project.ts',
|
||||
bufferedAssets = /\.(wav|mp3|ogg)$/,
|
||||
editor: {
|
||||
styles = '@motion-canvas/ui/dist/style.css',
|
||||
factory = '@motion-canvas/ui',
|
||||
} = {},
|
||||
}: MotionCanvasPluginConfig = {}): Plugin => {
|
||||
const editorPath = path.dirname(require.resolve('@motion-canvas/ui'));
|
||||
const editorId = 'virtual:editor';
|
||||
const resolvedEditorId = '\0' + editorId;
|
||||
const timeStamps: Record<string, number> = {};
|
||||
const projectName = path.parse(project).name;
|
||||
|
||||
let viteConfig: ResolvedConfig;
|
||||
|
||||
function source(...lines: string[]) {
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
async function createMeta(metaPath: string) {
|
||||
if (!fs.existsSync(metaPath)) {
|
||||
await fs.promises.writeFile(
|
||||
metaPath,
|
||||
JSON.stringify({version: 0}, undefined, 2),
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'motion-canvas',
|
||||
async configResolved(resolvedConfig) {
|
||||
viteConfig = resolvedConfig;
|
||||
},
|
||||
resolveId(id) {
|
||||
if (id === editorId) {
|
||||
return resolvedEditorId;
|
||||
}
|
||||
},
|
||||
load(id) {
|
||||
if (id === resolvedEditorId) {
|
||||
return source(
|
||||
`import '${styles}';`,
|
||||
`import editor from '${factory}';`,
|
||||
`import project from '${project}';`,
|
||||
`editor(project);`,
|
||||
);
|
||||
}
|
||||
},
|
||||
async transform(code, id) {
|
||||
const [base, query] = id.split('?');
|
||||
const {name, dir, ext} = path.posix.parse(base);
|
||||
|
||||
if (query) {
|
||||
const params = new URLSearchParams(query);
|
||||
if (params.has('img')) {
|
||||
return source(
|
||||
`import {loadImage} from '@motion-canvas/core/lib/media';`,
|
||||
`import image from '/@fs/${base}';`,
|
||||
`export default loadImage(image);`,
|
||||
);
|
||||
}
|
||||
|
||||
if (params.has('anim')) {
|
||||
const nameRegex = /\D*(\d+)\./;
|
||||
let urls: string[] = [];
|
||||
for (const file of await fs.promises.readdir(dir)) {
|
||||
const match = nameRegex.exec(file);
|
||||
if (!match) continue;
|
||||
const index = parseInt(match[1]);
|
||||
urls[index] = path.posix.join(dir, file);
|
||||
}
|
||||
urls = urls.filter(Boolean);
|
||||
|
||||
return source(
|
||||
`import {loadAnimation} from '@motion-canvas/core/lib/media';`,
|
||||
...urls.map(
|
||||
(url, index) => `import image${index} from '/@fs/${url}';`,
|
||||
),
|
||||
`export default loadAnimation([${urls
|
||||
.map((_, index) => `image${index}`)
|
||||
.join(', ')}]);`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (ext === '.meta') {
|
||||
return source(
|
||||
`import {Meta} from '@motion-canvas/core/lib';`,
|
||||
`Meta.register(`,
|
||||
` '${name}',`,
|
||||
` ${viteConfig.command === 'build' ? false : `'${id}'`},`,
|
||||
` \`${code}\``,
|
||||
`);`,
|
||||
`if (import.meta.hot) {`,
|
||||
` import.meta.hot.accept();`,
|
||||
`}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (name === projectName && (await this.resolve(project))?.id === id) {
|
||||
const metaFile = `${name}.meta`;
|
||||
await createMeta(path.join(dir, metaFile));
|
||||
|
||||
const imports =
|
||||
`import '@motion-canvas/core/lib/patches/Factory';` +
|
||||
`import '@motion-canvas/core/lib/patches/Node';` +
|
||||
`import '@motion-canvas/core/lib/patches/Shape';` +
|
||||
`import '@motion-canvas/core/lib/patches/Container';` +
|
||||
`import './${metaFile}';`;
|
||||
|
||||
return imports + code;
|
||||
}
|
||||
|
||||
if (name.endsWith('.scene')) {
|
||||
const metaFile = `${name}.meta`;
|
||||
await createMeta(path.join(dir, metaFile));
|
||||
|
||||
const imports =
|
||||
`import './${metaFile}';` +
|
||||
`import {useProject as __useProject} from '@motion-canvas/core/lib/utils';`;
|
||||
const hmr = source(
|
||||
`if (import.meta.hot) {`,
|
||||
` import.meta.hot.accept(module => {`,
|
||||
` __useProject()?.updateScene(module.default);`,
|
||||
` });`,
|
||||
`}`,
|
||||
);
|
||||
return imports + code + '\n' + hmr;
|
||||
}
|
||||
},
|
||||
handleHotUpdate(ctx) {
|
||||
const now = Date.now();
|
||||
const urls = [];
|
||||
const modules = [];
|
||||
|
||||
for (const module of ctx.modules) {
|
||||
if (
|
||||
module.file !== null &&
|
||||
timeStamps[module.file] &&
|
||||
timeStamps[module.file] + 1000 > now
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
urls.push(module.url);
|
||||
modules.push(module);
|
||||
}
|
||||
|
||||
if (urls.length > 0) {
|
||||
ctx.server.ws.send('motion-canvas:assets', {urls});
|
||||
}
|
||||
|
||||
return modules;
|
||||
},
|
||||
configureServer(server) {
|
||||
server.middlewares.use((req, res, next) => {
|
||||
if (req.url && bufferedAssets.test(req.url)) {
|
||||
const file = fs.readFileSync(
|
||||
path.resolve(viteConfig.root, req.url.slice(1)),
|
||||
);
|
||||
const stream = Readable.from(file);
|
||||
stream.on('end', console.log).pipe(res);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url === '/') {
|
||||
const stream = fs.createReadStream(
|
||||
path.resolve(editorPath, '../editor.html'),
|
||||
);
|
||||
stream.pipe(res);
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
server.ws.on('motion-canvas:meta', async ({source, data}, client) => {
|
||||
timeStamps[source] = Date.now();
|
||||
await fs.promises.writeFile(
|
||||
source,
|
||||
JSON.stringify(data, undefined, 2),
|
||||
'utf8',
|
||||
);
|
||||
client.send('motion-canvas:meta-ack', {source});
|
||||
});
|
||||
},
|
||||
config() {
|
||||
return {
|
||||
esbuild: {
|
||||
jsx: 'automatic',
|
||||
jsxImportSource: '@motion-canvas/core/lib',
|
||||
},
|
||||
define: {
|
||||
PROJECT_FILE_NAME: `'${projectName}'`,
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: project,
|
||||
formats: ['es'],
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
14
packages/vite-plugin/tsconfig.json
Normal file
14
packages/vite-plugin/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"outDir": "lib",
|
||||
"strict": true,
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"target": "es2021",
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
4
packages/vite-plugin/tsdoc.json
Normal file
4
packages/vite-plugin/tsdoc.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
|
||||
"extends": ["../../tsdoc.json"]
|
||||
}
|
||||
32
tsdoc.json
Normal file
32
tsdoc.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
|
||||
"tagDefinitions": [
|
||||
{
|
||||
"tagName": "@module",
|
||||
"syntaxKind": "modifier"
|
||||
},
|
||||
{
|
||||
"tagName": "@ignore",
|
||||
"syntaxKind": "modifier"
|
||||
},
|
||||
{
|
||||
"tagName": "@default",
|
||||
"syntaxKind": "modifier"
|
||||
}
|
||||
],
|
||||
"supportForTags": {
|
||||
"@module": true,
|
||||
"@param": true,
|
||||
"@deprecated": true,
|
||||
"@returns": true,
|
||||
"@remarks": true,
|
||||
"@example": true,
|
||||
"@link": true,
|
||||
"@internal": true,
|
||||
"@eventProperty": true,
|
||||
"@typeParam": true,
|
||||
"@ignore": true,
|
||||
"@inheritDoc": true,
|
||||
"@default": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user