mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-11 06:48:12 -05:00
feat: support multiple players (#128)
This commit is contained in:
2
packages/core/project.d.ts
vendored
2
packages/core/project.d.ts
vendored
@@ -22,7 +22,7 @@ declare module '*.glsl' {
|
||||
}
|
||||
|
||||
declare module '*?scene' {
|
||||
const value: import('./lib/scenes/Scene').Scene;
|
||||
const value: import('./lib/scenes/Scene').FullSceneDescription;
|
||||
export = value;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,11 +27,13 @@ export class Logger {
|
||||
return this.logged.subscribable;
|
||||
}
|
||||
private readonly logged = new EventDispatcher<LogPayload>();
|
||||
public readonly history: LogPayload[] = [];
|
||||
|
||||
private profilers: Record<string, number> = {};
|
||||
|
||||
public log(payload: LogPayload) {
|
||||
this.logged.dispatch(payload);
|
||||
this.history.push(payload);
|
||||
}
|
||||
|
||||
public error(payload: string | LogPayload) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Scene} from './scenes';
|
||||
import {FullSceneDescription, Scene, SceneDescription} from './scenes';
|
||||
import {Meta, Metadata} from './Meta';
|
||||
import {EventDispatcher, ValueDispatcher} from './events';
|
||||
import {CanvasColorSpace, CanvasOutputMimeType, Vector2} from './types';
|
||||
@@ -16,7 +16,7 @@ export const ProjectSize: Record<string, Vector2> = {
|
||||
|
||||
export interface ProjectConfig {
|
||||
name?: string;
|
||||
scenes: Scene[];
|
||||
scenes: FullSceneDescription[];
|
||||
audio?: string;
|
||||
audioOffset?: number;
|
||||
canvas?: HTMLCanvasElement;
|
||||
@@ -33,6 +33,10 @@ export enum PlaybackState {
|
||||
|
||||
export type ProjectMetadata = Metadata;
|
||||
|
||||
export function makeProject(config: ProjectConfig) {
|
||||
return config;
|
||||
}
|
||||
|
||||
export class Project {
|
||||
/**
|
||||
* Triggered after the scenes were recalculated.
|
||||
@@ -58,7 +62,7 @@ export class Project {
|
||||
}
|
||||
private readonly reloaded = new EventDispatcher<void>();
|
||||
|
||||
public declare readonly meta: Meta<ProjectMetadata>;
|
||||
public readonly meta: Meta<ProjectMetadata>;
|
||||
public frame = 0;
|
||||
|
||||
public get time(): number {
|
||||
@@ -147,10 +151,11 @@ export class Project {
|
||||
return new Vector2(this.width, this.height);
|
||||
}
|
||||
|
||||
public declare readonly name: string;
|
||||
public readonly name: string;
|
||||
public readonly audio = new AudioManager();
|
||||
public readonly logger = new Logger();
|
||||
public readonly background: string | false;
|
||||
public readonly creationStack: string;
|
||||
public playbackState = createSignal(PlaybackState.Paused);
|
||||
private readonly renderLookup: Record<number, Callback> = {};
|
||||
private _resolutionScale = 1;
|
||||
@@ -171,14 +176,35 @@ export class Project {
|
||||
private width = 0;
|
||||
private height = 0;
|
||||
|
||||
public constructor({
|
||||
scenes,
|
||||
audio,
|
||||
audioOffset,
|
||||
canvas,
|
||||
size = ProjectSize.FullHD,
|
||||
background = false,
|
||||
}: ProjectConfig) {
|
||||
/**
|
||||
* @deprecated Use {@link makeProject} instead.
|
||||
*
|
||||
* @param config - The project configuration.
|
||||
*/
|
||||
public constructor(config: ProjectConfig);
|
||||
public constructor(
|
||||
name: string,
|
||||
meta: Meta<ProjectMetadata>,
|
||||
config: ProjectConfig,
|
||||
);
|
||||
public constructor(
|
||||
name: string | ProjectConfig,
|
||||
meta?: Meta<ProjectMetadata>,
|
||||
config?: ProjectConfig,
|
||||
) {
|
||||
const {
|
||||
scenes,
|
||||
audio,
|
||||
audioOffset,
|
||||
canvas,
|
||||
size = ProjectSize.FullHD,
|
||||
background = false,
|
||||
} = typeof name === 'string' ? config! : name;
|
||||
|
||||
this.name = typeof name === 'string' ? name : '';
|
||||
this.meta = meta!;
|
||||
|
||||
this.creationStack = new Error().stack ?? '';
|
||||
this.setCanvas(canvas);
|
||||
this.setSize(size);
|
||||
this.background = background;
|
||||
@@ -190,11 +216,19 @@ export class Project {
|
||||
this.audio.setOffset(audioOffset);
|
||||
}
|
||||
|
||||
for (const scene of scenes) {
|
||||
const instances: Scene[] = [];
|
||||
for (const description of scenes) {
|
||||
const scene = new description.klass(
|
||||
description.name,
|
||||
description.meta,
|
||||
description.config,
|
||||
);
|
||||
scene.project = this;
|
||||
description.onReplaced?.subscribe(config => scene.reload(config), false);
|
||||
scene.onReloaded.subscribe(() => this.reloaded.dispatch());
|
||||
instances.push(scene);
|
||||
}
|
||||
this.scenes.current = [...scenes];
|
||||
this.scenes.current = instances;
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.on('motion-canvas:export-ack', ({frame}) => {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type {Project} from '../Project';
|
||||
import {Meta, Metadata} from '../Meta';
|
||||
import {SavedTimeEvent, TimeEvents} from './TimeEvents';
|
||||
import {SubscribableEvent, SubscribableValueEvent} from '../events';
|
||||
import {
|
||||
SubscribableEvent,
|
||||
SubscribableValueEvent,
|
||||
ValueDispatcher,
|
||||
} from '../events';
|
||||
import {Vector2} from '../types';
|
||||
import {LifecycleEvents} from './LifecycleEvents';
|
||||
import {Random} from './Random';
|
||||
@@ -27,7 +31,7 @@ export interface SceneConstructor<T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a scene exposed by a `*.scene.tsx` file.
|
||||
* Describes a scene exposed by scene files.
|
||||
*
|
||||
* @typeParam T - The type of the configuration object.
|
||||
*/
|
||||
@@ -42,6 +46,17 @@ export interface SceneDescription<T = unknown> {
|
||||
config: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a complete scene together with the meta file.
|
||||
*
|
||||
* @typeParam T - The type of the configuration object.
|
||||
*/
|
||||
export interface FullSceneDescription<T = unknown> extends SceneDescription<T> {
|
||||
name: string;
|
||||
meta: Meta<SceneMetadata>;
|
||||
onReplaced: ValueDispatcher<T>;
|
||||
}
|
||||
|
||||
export type DescriptionOf<TScene> = TScene extends Scene<infer TConfig>
|
||||
? SceneDescription<TConfig>
|
||||
: never;
|
||||
|
||||
@@ -92,7 +92,7 @@ It should lead to a directory containing the following files:
|
||||
|
||||
`main.js` should export the following functions:
|
||||
|
||||
- `editor` - Receives the project as its first argument and creates the
|
||||
- `editor` - Receives the project factory as its first argument and creates the
|
||||
user interface.
|
||||
- `index` - Receives a list of all projects as its first argument and
|
||||
creates the initial page for selecting a project.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"version": 0
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Project} from '@motion-canvas/core/lib';
|
||||
import {makeProject} from '@motion-canvas/core';
|
||||
|
||||
import quickstart from './scenes/quickstart?scene';
|
||||
import scene from './scenes/quickstart?scene';
|
||||
|
||||
export default new Project({
|
||||
scenes: [quickstart],
|
||||
export default makeProject({
|
||||
scenes: [scene],
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {Project} from '@motion-canvas/core/lib';
|
||||
import {makeProject} from '@motion-canvas/core';
|
||||
|
||||
import scene from './scenes/random?scene';
|
||||
import {Vector2} from '@motion-canvas/core/lib/types';
|
||||
|
||||
export default new Project({
|
||||
export default makeProject({
|
||||
scenes: [scene],
|
||||
background: '#141414',
|
||||
size: new Vector2(960, 540),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"version": 0
|
||||
"version": 1,
|
||||
"seed": 2953611296
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"version": 0
|
||||
"version": 1,
|
||||
"seed": 1564249406
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Project} from '@motion-canvas/core/lib';
|
||||
import {makeProject} from '@motion-canvas/core';
|
||||
|
||||
import scene from './scenes/tweening-color?scene';
|
||||
|
||||
export default new Project({
|
||||
export default makeProject({
|
||||
scenes: [scene],
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Project} from '@motion-canvas/core/lib';
|
||||
import {makeProject} from '@motion-canvas/core';
|
||||
|
||||
import scene from './scenes/tweening-cubic?scene';
|
||||
|
||||
export default new Project({
|
||||
export default makeProject({
|
||||
scenes: [scene],
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Project} from '@motion-canvas/core/lib';
|
||||
import {makeProject} from '@motion-canvas/core';
|
||||
|
||||
import scene from './scenes/tweening-linear?scene';
|
||||
|
||||
export default new Project({
|
||||
export default makeProject({
|
||||
scenes: [scene],
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Project} from '@motion-canvas/core/lib';
|
||||
import {makeProject} from '@motion-canvas/core';
|
||||
|
||||
import scene from './scenes/tweening-vector?scene';
|
||||
|
||||
export default new Project({
|
||||
export default makeProject({
|
||||
scenes: [scene],
|
||||
});
|
||||
|
||||
@@ -169,7 +169,7 @@ class MotionCanvasPlayer extends HTMLElement {
|
||||
const delay = new Promise(resolve => setTimeout(resolve, 200));
|
||||
await Promise.any([delay, promise]);
|
||||
this.setState(State.Loading);
|
||||
project = (await promise).default;
|
||||
project = (await promise).default();
|
||||
} catch (e) {
|
||||
this.setState(State.Error);
|
||||
return;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {Project} from '@motion-canvas/core/lib';
|
||||
import {makeProject} from '@motion-canvas/core';
|
||||
|
||||
import example from './scenes/example?scene';
|
||||
|
||||
export default new Project({
|
||||
export default makeProject({
|
||||
scenes: [example],
|
||||
background: '#141414',
|
||||
});
|
||||
|
||||
@@ -59,6 +59,7 @@ $colors: (
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.remarks pre,
|
||||
.code {
|
||||
background-color: var(--surface-color);
|
||||
border-radius: 4px;
|
||||
@@ -76,6 +77,14 @@ $colors: (
|
||||
--scrollbar-background: var(--background-color-dark);
|
||||
}
|
||||
|
||||
.remarks {
|
||||
margin-top: 16px;
|
||||
|
||||
p {
|
||||
margin: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.entry {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
|
||||
@@ -28,7 +28,7 @@ export function Log({payload}: LogProps) {
|
||||
return entries?.find(entry => !entry.isExternal) ?? null;
|
||||
}, [entries]);
|
||||
|
||||
const hasBody = !!object || !!entries;
|
||||
const hasBody = !!object || !!entries || !!payload.remarks;
|
||||
|
||||
useEffect(() => {
|
||||
if (payload.stack) {
|
||||
@@ -61,8 +61,14 @@ export function Log({payload}: LogProps) {
|
||||
{hasBody && open && (
|
||||
<div>
|
||||
{userEntry && <SourceCodeFrame entry={userEntry} />}
|
||||
{object && <pre className={styles.code}>{object}</pre>}
|
||||
{entries && <StackTrace entries={entries} />}
|
||||
{object && <pre className={styles.code}>{object}</pre>}
|
||||
{payload.remarks && (
|
||||
<div
|
||||
className={styles.remarks}
|
||||
dangerouslySetInnerHTML={{__html: payload.remarks}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ export class LoggerManager {
|
||||
|
||||
public constructor(project: Project) {
|
||||
this.logger = project.logger;
|
||||
this.logs.current = [];
|
||||
this.logs.current = project.logger.history;
|
||||
this.logger.onLogged.subscribe(this.handleLog);
|
||||
project.onReloaded.subscribe(this.clear);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ function renderRoot(vnode: ComponentChild) {
|
||||
render(vnode, root);
|
||||
}
|
||||
|
||||
export function editor(project: Project) {
|
||||
export function editor(factory: () => Project) {
|
||||
const project = factory();
|
||||
Error.stackTraceLimit = Infinity;
|
||||
|
||||
project.logger.onLogged.subscribe(log => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {withLoader} from './withLoader';
|
||||
|
||||
const externalFileRegex = /^\/(@fs|@id|node_modules)\//;
|
||||
const stackTraceRegex = navigator.userAgent.toLowerCase().includes('chrome')
|
||||
? /^ +at.+\((.*):([0-9]+):([0-9]+)/
|
||||
? /^ +at.* \(?(.*):([0-9]+):([0-9]+)/
|
||||
: /@(.*):([0-9]+):([0-9]+)/;
|
||||
|
||||
async function getSourceMap(file: string): Promise<SourceMapConsumer> {
|
||||
@@ -102,10 +102,12 @@ export function getSourceCodeFrame(entry: StackTraceEntry): string | null {
|
||||
|
||||
const sourceLines = source.split('\n');
|
||||
const {line, column} = entry;
|
||||
const lastLine = line + 2;
|
||||
const spacing = lastLine.toString().length;
|
||||
const formatted = sourceLines
|
||||
.slice(line - 1, line + 2)
|
||||
.slice(line - 1, lastLine)
|
||||
.map((text, index) => `${line + index} | ${text}`);
|
||||
formatted.splice(1, 0, ` | ${' '.repeat(column)}^`);
|
||||
formatted.splice(1, 0, `${' '.repeat(spacing)} | ${' '.repeat(column)}^`);
|
||||
return formatted.join('\n');
|
||||
}
|
||||
|
||||
|
||||
@@ -58,8 +58,8 @@ export interface MotionCanvasPluginConfig {
|
||||
* - `main.js` - A module exporting necessary factory functions.
|
||||
*
|
||||
* `main.js` should export the following functions:
|
||||
* - `editor` - Receives the project as its first argument and creates the
|
||||
* user interface.
|
||||
* - `editor` - Receives the project factory as its first argument and creates
|
||||
* the user interface.
|
||||
* - `index` - Receives a list of all projects as its first argument and
|
||||
* creates the initial page for selecting a project.
|
||||
*
|
||||
@@ -159,22 +159,24 @@ export default ({
|
||||
const sceneFile = `${name}`;
|
||||
|
||||
return source(
|
||||
`import {ValueDispatcher} from '@motion-canvas/core/lib/events';`,
|
||||
`import meta from './${metaFile}';`,
|
||||
`import description from './${sceneFile}';`,
|
||||
`let scene;`,
|
||||
`description.name = '${name}';`,
|
||||
`description.meta = meta;`,
|
||||
`if (import.meta.hot) {`,
|
||||
` scene = import.meta.hot.data.scene;`,
|
||||
` description.onReplaced = import.meta.hot.data.onReplaced;`,
|
||||
`}`,
|
||||
`scene ??= new description.klass('${name}', meta, description.config);`,
|
||||
`description.onReplaced ??= new ValueDispatcher(description.config);`,
|
||||
`if (import.meta.hot) {`,
|
||||
` import.meta.hot.accept();`,
|
||||
` if (import.meta.hot.data.scene) {`,
|
||||
` scene.reload(description.config);`,
|
||||
` if (import.meta.hot.data.onReplaced) {`,
|
||||
` description.onReplaced.current = description.config;`,
|
||||
` } else {`,
|
||||
` import.meta.hot.data.scene = scene;`,
|
||||
` import.meta.hot.data.onReplaced = description.onReplaced;`,
|
||||
` }`,
|
||||
`}`,
|
||||
`export default scene;`,
|
||||
`export default description;`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -183,11 +185,23 @@ export default ({
|
||||
await createMeta(path.join(dir, metaFile));
|
||||
|
||||
return source(
|
||||
`import {Project} from '@motion-canvas/core';`,
|
||||
`import meta from './${metaFile}';`,
|
||||
`import project from './${name}';`,
|
||||
`project.meta = meta`,
|
||||
`project.name = '${name}'`,
|
||||
`export default project;`,
|
||||
`import config from './${name}';`,
|
||||
`const factory = () => {`,
|
||||
` if (config instanceof Project) {`,
|
||||
` config.meta = meta;`,
|
||||
` config.name = '${name}';`,
|
||||
` config.logger.warn({`,
|
||||
` message: 'A project instance was exported instead of a project factory.',`,
|
||||
` remarks: \`<p>Use the "makeProject()" function instead:</p><pre>import {makeProject} from '@motion-canvas/core';\nexport default makeProject({\n // Configuration and scenes go here.\n});</pre>\`,`,
|
||||
` stack: config.creationStack,`,
|
||||
` });`,
|
||||
` return config;`,
|
||||
` }`,
|
||||
` return new Project('${name}', meta, config);`,
|
||||
`}`,
|
||||
`export default factory;`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user