feat: support multiple players (#128)

This commit is contained in:
Jacob
2023-01-12 22:45:38 +01:00
committed by GitHub
parent 03797302a3
commit 24f75cf7cd
22 changed files with 142 additions and 60 deletions

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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}) => {

View File

@@ -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;

View File

@@ -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.

View File

@@ -1,3 +0,0 @@
{
"version": 0
}

View File

@@ -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],
});

View File

@@ -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),

View File

@@ -1,3 +1,4 @@
{
"version": 0
"version": 1,
"seed": 2953611296
}

View File

@@ -1,3 +1,4 @@
{
"version": 0
"version": 1,
"seed": 1564249406
}

View File

@@ -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],
});

View File

@@ -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],
});

View File

@@ -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],
});

View File

@@ -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],
});

View File

@@ -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;

View File

@@ -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',
});

View File

@@ -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;

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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 => {

View File

@@ -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');
}

View File

@@ -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;`,
);
}
}