feat: add meta files (#28)

Meta files are additional JSON files containing information about
scenes and projects. They can be edited both programmatically and
via IDE making them perfect for storing data edited through the
user interface.

Each meta file has the same name as the file it relates to but
with extension changed to .meta. It's generated and imported
automatically and can be safely added to version control.

BREAKING CHANGE: change time events API

Fixes: #7
This commit is contained in:
Jacob
2022-06-16 19:21:58 +02:00
committed by GitHub
parent fb4e0f5530
commit e29f7d0ed0
12 changed files with 540 additions and 116 deletions

View File

@@ -9,6 +9,7 @@ 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,
@@ -33,16 +34,19 @@ if (cli.flags.uiServer) {
cli.flags.uiPath ||= 'http://localhost:9001/main.js';
} else {
if (cli.flags.uiPath) {
cli.flags.uiPath = path.resolve(process.cwd(), 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 projectFile = path.resolve(process.cwd(), cli.input[0]);
const renderOutput = path.resolve(process.cwd(), cli.flags.output);
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},
@@ -53,6 +57,16 @@ const compiler = webpack({
{
test: /\.tsx?$/,
loader: 'ts-loader',
options: {
getCustomTransformers: () => ({
before: [
metaImportTransformer({
project: projectFile,
version: packageJSON.version,
}),
],
}),
},
},
{
test: /\.glsl$/i,
@@ -62,6 +76,10 @@ const compiler = webpack({
test: /\.mp4/i,
type: 'asset',
},
{
test: /\.meta/i,
loader: 'meta-loader',
},
{
test: /\.wav$/i,
type: 'asset',
@@ -108,6 +126,10 @@ const compiler = webpack({
// Required to load additional languages for Prism
Prism: 'prismjs',
}),
new webpack.DefinePlugin({
PROJECT_FILE_NAME: `'${path.parse(projectFile).name}'`,
CORE_VERSION: `'${packageJSON.version}'`,
}),
new HtmlWebpackPlugin({title: 'Motion Canvas'}),
new UIPlugin(cli.flags),
],
@@ -141,6 +163,19 @@ const server = new WebpackDevServer(
},
});
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',

View File

@@ -0,0 +1,39 @@
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: 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}',
'${encodeURI(relative)}',
${source}
);
module.hot.accept(console.error);`;
this.callback(null, content, map);
}
module.exports = metaLoader;

View File

@@ -0,0 +1,68 @@
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,
]);
}

66
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@motion-canvas/core",
"version": "1.1.0",
"version": "2.2.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@motion-canvas/core",
"version": "1.1.0",
"version": "2.2.0",
"license": "MIT",
"dependencies": {
"@types/prismjs": "^1.26.0",
@@ -20,6 +20,7 @@
"mix-color": "^1.1.2",
"mp4box": "^0.5.2",
"prismjs": "^1.28.0",
"source-map": "^0.7.4",
"strongly-typed-events": "^3.0.2",
"three": "^0.141.0",
"ts-loader": "^9.3.0",
@@ -2174,6 +2175,14 @@
"node": ">= 10.0"
}
},
"node_modules/clean-css/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -4392,6 +4401,15 @@
"uglify-js": "^3.1.4"
}
},
"node_modules/handlebars/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/hard-rejection": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz",
@@ -9728,11 +9746,11 @@
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
"engines": {
"node": ">=0.10.0"
"node": ">= 8"
}
},
"node_modules/source-map-support": {
@@ -9744,6 +9762,14 @@
"source-map": "^0.6.0"
}
},
"node_modules/source-map-support/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/spawn-error-forwarder": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/spawn-error-forwarder/-/spawn-error-forwarder-1.0.0.tgz",
@@ -12857,6 +12883,13 @@
"integrity": "sha512-YYuuxv4H/iNb1Z/5IbMRoxgrzjWGhOEFfd+groZ5dMCVkpENiMZmwspdrzBo9286JjM1gZJPAyL7ZIdzuvu2AQ==",
"requires": {
"source-map": "~0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"clean-stack": {
@@ -14540,6 +14573,14 @@
"source-map": "^0.6.1",
"uglify-js": "^3.1.4",
"wordwrap": "^1.0.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"hard-rejection": {
@@ -18335,9 +18376,9 @@
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="
},
"source-map-support": {
"version": "0.5.21",
@@ -18346,6 +18387,13 @@
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"spawn-error-forwarder": {

View File

@@ -44,6 +44,7 @@
"mix-color": "^1.1.2",
"mp4box": "^0.5.2",
"prismjs": "^1.28.0",
"source-map": "^0.7.4",
"strongly-typed-events": "^3.0.2",
"three": "^0.141.0",
"ts-loader": "^9.3.0",

109
src/Meta.ts Normal file
View File

@@ -0,0 +1,109 @@
import {SimpleEventDispatcher} from 'strongly-typed-events';
/**
* Represents the contents of a meta file.
*/
export interface Metadata {
version: number;
}
/**
* Represents the meta file of a given entity.
*
* @template T Type of the data stored in the meta file.
*/
export class Meta<T extends Metadata = Metadata> {
/**
* Triggered when metadata changes.
*
* @event T
*/
public get Changed() {
return this.changed.asEvent();
}
private data: T;
private source: string;
private ignoreReload = false;
private changed = new SimpleEventDispatcher<T>();
private constructor() {
this.data = <T>{version: Meta.currentVersion};
}
public getData(): T {
return this.data;
}
/**
* Set data without waiting for confirmation.
*
* Any possible errors will be logged to the console.
*
* @param data
*/
public setDataSync(data: Partial<T>) {
this.setData(data).catch(console.error);
}
public async setData(data: Partial<T>) {
this.data = {
...this.data,
...data,
};
this.changed.dispatch(this.data);
this.ignoreReload = true;
const response = await fetch(`/meta/${this.source}`, {
method: 'POST',
body: JSON.stringify(this.data, undefined, 2),
});
if (!response.ok) {
throw new Error(response.statusText);
}
}
private static currentVersion = 1;
private static metaLookup: Record<string, Meta> = {};
/**
* Get the {@link Meta} object for the given entity.
*
* @param name Name of the entity the metadata refers to.
* @template T Concrete type of the metadata. Depends on the entity.
* See {@link SceneMetadata} and {@link ProjectMetadata} for sample
* types.
*
* @internal
*/
public static getMetaFor<T extends Metadata = Metadata>(
name: string,
): Meta<T> {
this.metaLookup[name] ??= new Meta<T>();
return <Meta<T>>this.metaLookup[name];
}
/**
* Register a new version of metadata.
*
* Called directly by meta files themselves.
* Occurs during the initial load as well as during hot reloads.
*
* @param name Name of the entity this metadata refers to.
* @param source Path to the source file relative to the compilation context.
* @param data New metadata.
*
* @internal
*/
public static register(name: string, source: string, data: Metadata) {
const meta = Meta.getMetaFor(name);
if (meta.ignoreReload) {
meta.ignoreReload = false;
return;
}
data.version ??= Meta.currentVersion;
meta.source = source;
meta.data = data;
meta.changed.dispatch(data);
}
}

View File

@@ -9,6 +9,7 @@ import {Thread, ThreadsCallback} from './threading';
import {Scene, SceneRunner} from './Scene';
import {SimpleEventDispatcher} from 'strongly-typed-events';
import {KonvaNode} from './decorators';
import {Meta, Metadata} from './Meta';
Konva.autoDrawEnabled = false;
@@ -16,17 +17,21 @@ export const ProjectSize = {
FullHD: {width: 1920, height: 1080},
};
interface ProjectConfig extends Partial<StageConfig> {
export interface ProjectConfig extends Partial<StageConfig> {
scenes: SceneRunner[];
background: string | false;
}
export type ProjectMetadata = Metadata;
@KonvaNode()
export class Project extends Stage {
public get ScenesChanged() {
return this.scenesChanged.asEvent();
}
public readonly version = CORE_VERSION;
public readonly meta: Meta<ProjectMetadata>;
public readonly background: Rect;
public readonly master: Layer;
public readonly center: Vector2d;
@@ -74,6 +79,7 @@ export class Project extends Stage {
...rest,
});
this.meta = Meta.getMetaFor(PROJECT_FILE_NAME);
this.offset({
x: this.width() / -2,
y: this.height() / -2,

View File

@@ -10,8 +10,9 @@ import {Group} from 'konva/lib/Group';
import {Shape} from 'konva/lib/Shape';
import {SceneTransition} from './transitions';
import {decorate, KonvaNode, threadable} from './decorators';
import {SimpleEventDispatcher} from 'strongly-typed-events';
import {setScene} from './utils';
import {Meta, Metadata} from './Meta';
import {SavedTimeEvent, TimeEvents} from './TimeEvents';
export interface SceneRunner {
(layer: Scene, project: Project): ThreadGenerator;
@@ -24,11 +25,8 @@ export enum SceneState {
Finished,
}
export interface TimeEvent {
name: string;
initialTime: number;
targetTime: number;
offset: number;
export interface SceneMetadata extends Metadata {
timeEvents: SavedTimeEvent[];
}
@KonvaNode()
@@ -37,6 +35,8 @@ export class Scene extends Group {
public firstFrame = 0;
public transitionDuration = 0;
public duration = 0;
public readonly meta: Meta<SceneMetadata>;
public readonly timeEvents: TimeEvents;
public get lastFrame() {
return this.firstFrame + this.duration;
@@ -45,23 +45,10 @@ export class Scene extends Group {
this.duration = value - this.firstFrame;
}
public get timeEvents(): TimeEvent[] {
return Object.values(this.timeEventLookup);
}
public get TimeEventsChanged() {
return this.timeEventsChanged.asEvent();
}
private readonly storageKey: string;
private readonly timeEventsChanged = new SimpleEventDispatcher<TimeEvent[]>();
private timeEventLookup: Record<string, TimeEvent> = {};
private storedEventLookup: Record<string, TimeEvent> = {};
private previousScene: Scene = null;
private runner: ThreadGenerator;
private state: SceneState = SceneState.Initial;
private cached = false;
private preserveEvents = false;
private counters: Record<string, number> = {};
public constructor(
@@ -77,22 +64,17 @@ export class Scene extends Group {
});
decorate(runnerFactory, threadable());
this.storageKey = `scene-${this.project.name()}-${this.name()}`;
const storedEvents = localStorage.getItem(this.storageKey);
if (storedEvents) {
for (const event of Object.values<TimeEvent>(JSON.parse(storedEvents))) {
this.storedEventLookup[event.name] = event;
}
}
this.meta = Meta.getMetaFor(`${this.name()}.scene`);
this.timeEvents = new TimeEvents(this);
}
public invalidate() {
this.cached = false;
}
public markAsCached() {
this.cached = true;
this.preserveEvents = false;
localStorage.setItem(
this.storageKey,
JSON.stringify(this.storedEventLookup),
);
this.timeEvents.preserveTiming = true;
}
public isMarkedAsCached() {
@@ -104,12 +86,6 @@ export class Scene extends Group {
this.runnerFactory = runnerFactory;
}
this.cached = false;
this.storedEventLookup = {
...this.storedEventLookup,
...this.timeEventLookup,
};
this.timeEventLookup = {};
this.timeEventsChanged.dispatch([]);
}
public async reset(previousScene: Scene = null) {
@@ -220,69 +196,4 @@ export class Scene extends Group {
return `${this.name()}.${type}.${id}`;
}
public getFrameEvent(name: string): number {
const initialTime = this.project.framesToSeconds(
this.project.frame - this.firstFrame,
);
if (this.timeEventLookup[name] === undefined) {
const event: TimeEvent = {
name,
initialTime,
targetTime: this.storedEventLookup[name]?.targetTime ?? initialTime,
offset: this.storedEventLookup[name]?.offset ?? 0,
};
if (this.storedEventLookup[name]) {
if (this.preserveEvents) {
event.targetTime = event.initialTime + event.offset;
} else {
event.offset = Math.max(0, event.targetTime - event.initialTime);
}
}
this.timeEventLookup[name] = event;
this.timeEventsChanged.dispatch(this.timeEvents);
} else if (this.timeEventLookup[name].initialTime !== initialTime) {
const event: TimeEvent = {
...this.timeEventLookup[name],
initialTime,
};
if (this.preserveEvents) {
event.targetTime = event.initialTime + event.offset;
} else {
event.offset = Math.max(0, event.targetTime - event.initialTime);
}
this.timeEventLookup[name] = event;
this.storedEventLookup[name] = event;
this.timeEventsChanged.dispatch(this.timeEvents);
}
return (
this.firstFrame +
this.project.secondsToFrames(
this.timeEventLookup[name].initialTime +
this.timeEventLookup[name].offset,
)
);
}
public setFrameEvent(name: string, offset: number, preserve = true) {
if (
!this.timeEventLookup[name] ||
this.timeEventLookup[name].offset === offset
) {
return;
}
this.cached = false;
this.preserveEvents = preserve;
this.storedEventLookup[name] = this.timeEventLookup[name] = {
...this.timeEventLookup[name],
targetTime: this.timeEventLookup[name].initialTime + offset,
offset,
};
this.timeEventsChanged.dispatch(this.timeEvents);
}
}

201
src/TimeEvents.ts Normal file
View File

@@ -0,0 +1,201 @@
import type {Scene} from './Scene';
import {SimpleEventDispatcher} from 'strongly-typed-events';
/**
* Represents a time event at runtime.
*/
export interface TimeEvent {
/**
* Name of the event.
*/
name: string;
/**
* Time in seconds, relative to the beginning of the scene, at which the event
* was registered.
*
* In other words, the moment at which {@link waitUntil} for this event was
* invoked.
*/
initialTime: number;
/**
* Time in seconds, relative to the beginning of the scene, at which the event
* should end.
*/
targetTime: number;
/**
* Duration of the event in seconds.
*/
offset: number;
}
/**
* Represents a time event stored in a meta file.
*/
export interface SavedTimeEvent {
name: string;
targetTime: number;
}
/**
* Manages time events for a given scene.
*/
export class TimeEvents {
/**
* Triggered when time events change.
*
* @event TimeEvent[]
*/
public get Changed() {
return this.changed.asEvent();
}
/**
* Whether the timing of events should be preserved.
*
* When set to `true` the offsets of events will be adjusted to keep them in
* place.
*
* @internal
*/
public preserveTiming = true;
private readonly changed = new SimpleEventDispatcher<TimeEvent[]>();
private lookup: Record<string, TimeEvent> = {};
private previousReference: SavedTimeEvent[];
public constructor(private readonly scene: Scene) {
const storageKey = `scene-${scene.project.name()}-${scene.name()}`;
const storedEvents = localStorage.getItem(storageKey);
if (storedEvents) {
console.info('Migrating localStorage to meta files');
localStorage.setItem(`${storageKey}-backup`, storedEvents);
localStorage.removeItem(storageKey);
this.load(Object.values<TimeEvent>(JSON.parse(storedEvents)));
this.save();
} else {
this.load(scene.meta.getData().timeEvents ?? []);
}
scene.meta.Changed.subscribe(event => {
// Ignore the event if `timeEvents` hasn't changed.
// This may happen when another part of metadata has changed triggering
// this event.
if (event.timeEvents === this.previousReference) return;
this.previousReference = event.timeEvents;
this.load(event.timeEvents ?? []);
scene.reload();
window.player.reload();
});
}
public toArray(): TimeEvent[] {
return Object.values(this.lookup);
}
public get(name: string) {
return this.lookup[name] ?? null;
}
/**
* Change the time offset of the given event.
*
* @param name Name of the event.
* @param offset Time offset in seconds.
* @param preserve Whether the timing of the consecutive events should be
* preserved. See {@link TimeEvents.preserveTiming}.
*/
public set(name: string, offset: number, preserve = true) {
if (!this.lookup[name] || this.lookup[name].offset === offset) {
return;
}
this.scene.invalidate();
this.preserveTiming = preserve;
this.lookup[name] = {
...this.lookup[name],
targetTime: this.lookup[name].initialTime + offset,
offset,
};
this.changed.dispatch(this.toArray());
this.save();
}
/**
* Register a time event.
*
* @param name Name of the event.
*
* @return The absolute frame at which the event should occur.
*
* @internal
*/
public register(name: string): number {
const initialTime = this.scene.project.framesToSeconds(
this.scene.project.frame - this.scene.firstFrame,
);
if (!this.lookup[name]) {
this.lookup[name] = {
name,
initialTime,
targetTime: initialTime,
offset: 0,
};
this.changed.dispatch(this.toArray());
} else {
let changed = false;
const event = {...this.lookup[name]};
if (event.initialTime !== initialTime) {
event.initialTime = initialTime;
changed = true;
}
const offset = Math.max(0, event.targetTime - event.initialTime);
if (!this.preserveTiming && event.offset !== offset) {
event.offset = offset;
changed = true;
}
const target = event.initialTime + event.offset;
if (this.preserveTiming && event.targetTime !== target) {
event.targetTime = target;
changed = true;
}
if (changed) {
this.lookup[name] = event;
this.changed.dispatch(this.toArray());
}
}
return (
this.scene.firstFrame +
this.scene.project.secondsToFrames(this.lookup[name].targetTime)
);
}
private save() {
this.scene.meta.setDataSync({
timeEvents: Object.values(this.lookup).map(event => ({
name: event.name,
targetTime: event.targetTime,
})),
});
}
private load(events: SavedTimeEvent[]) {
const previousEvents = this.lookup;
this.lookup = {};
for (const event of events) {
const previous = previousEvents[event.name] ?? {
name: event.name,
initialTime: 0,
offset: 0,
};
this.lookup[event.name] = {
...previous,
targetTime: event.targetTime,
};
}
}
}

View File

@@ -47,7 +47,7 @@ export function* waitUntil(
const scene = useScene();
const frames =
typeof targetTime === 'string'
? scene.getFrameEvent(targetTime)
? scene.timeEvents.register(targetTime)
: scene.project.secondsToFrames(targetTime);
while (scene.project.frame < frames) {

View File

@@ -53,3 +53,7 @@ declare namespace JSX {
}
declare type Callback = (...args: unknown[]) => void;
declare const PROJECT_FILE_NAME: string;
declare const CORE_VERSION: string;

View File

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