mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-12 15:28:03 -05:00
feat: decouple Konva from core (#54)
Konva has been decoupled from the core entirely. It is now possible to supply the project with custom implementations of the `Scene` interface.
Because of that, scene files need to export a special `SceneDescription` object instead of a simple generator function.
The preferred way of doing it is by using dedicated "make" functions that generate description objects for specific scenes.
For example this scene:
```ts
export default function* example(scene: Scene): ThreadGenerator {
yield* scene.transition();
// perform animation
}
```
should now be written as:
```ts
export default makeKonvaScene(function* example(view) {
yield* view.transition();
// perform animation
});
```
The core provides an abstract implementation of the `Scene` interface called `GeneratorScene`.
It takes care of all the logic related to generators, life cycles, caching, etc. And can be extended to easily provide custom rendering logic. See `KonvaScene` for an example of extending the `GeneratorScene`.
Alternatively, the `Scene` interface can be implemented from the ground up allowing for even more flexibility.
For the time being, features related to Konva are kept in the core repo.
After #49 they will be extracted to a separate package.
BREAKING CHANGE: change the type exported by scene files
Scene files need to export a special `SceneDescription` object instead of a simple generator function.
Closes: #31
This commit is contained in:
14
README.md
14
README.md
@@ -74,16 +74,16 @@ Otherwise, below are the steps to set it up manually:
|
||||
6. Create a simple scene in `src/scenes/example.scene.tsx`:
|
||||
|
||||
```ts
|
||||
import type {Scene} from '@motion-canvas/core/lib/Scene';
|
||||
import {ThreadGenerator} from '@motion-canvas/core/lib/threading';
|
||||
import {makeKonvaScene} from '@motion-canvas/core/lib/scenes';
|
||||
import {waitFor} from '@motion-canvas/core/lib/flow';
|
||||
|
||||
export default function* example(scene: Scene): ThreadGenerator {
|
||||
export default makeKonvaScene(function* example(view) {
|
||||
yield* scene.transition();
|
||||
|
||||
|
||||
// animation code goes here
|
||||
|
||||
yield* waitFor(5);
|
||||
scene.canFinish();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
7. Initialize the project with your scene in `src/project.ts`:
|
||||
@@ -132,6 +132,8 @@ npm run serve
|
||||
|
||||
You can now open Motion Canvas in your browser by visiting [http://localhost:9000/](http://localhost:9000/).
|
||||
|
||||
The API documentation is available at [http://localhost:9000/api/](http://localhost:9000/api/).
|
||||
|
||||
In case of any problems, please visit [our discord server](https://www.patreon.com/posts/53003221).
|
||||
|
||||
## Developing Motion Canvas locally
|
||||
|
||||
299
src/Project.ts
299
src/Project.ts
@@ -1,52 +1,44 @@
|
||||
import './patches';
|
||||
|
||||
import {Stage, StageConfig} from 'konva/lib/Stage';
|
||||
import {Rect} from 'konva/lib/shapes/Rect';
|
||||
import {Layer} from 'konva/lib/Layer';
|
||||
import {Vector2d} from 'konva/lib/types';
|
||||
import {Konva} from 'konva/lib/Global';
|
||||
import {Thread, ThreadsCallback} from './threading';
|
||||
import {Scene, SceneRunner} from './Scene';
|
||||
import {KonvaNode} from './decorators';
|
||||
import {Scene, SceneDescription} from './scenes';
|
||||
import {Meta, Metadata} from './Meta';
|
||||
import {ValueDispatcher} from './events';
|
||||
|
||||
Konva.autoDrawEnabled = false;
|
||||
import {Size} from './types';
|
||||
|
||||
export const ProjectSize = {
|
||||
FullHD: {width: 1920, height: 1080},
|
||||
};
|
||||
|
||||
export interface ProjectConfig extends Partial<StageConfig> {
|
||||
scenes: SceneRunner[];
|
||||
background: string | false;
|
||||
export interface ProjectConfig {
|
||||
name: string;
|
||||
scenes: SceneDescription[];
|
||||
canvas?: HTMLCanvasElement;
|
||||
background?: string | false;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export type ProjectMetadata = Metadata;
|
||||
|
||||
@KonvaNode()
|
||||
export class Project extends Stage {
|
||||
export class Project {
|
||||
public get onScenesChanged() {
|
||||
return this.scenes.subscribable;
|
||||
}
|
||||
private readonly scenes = new ValueDispatcher<Scene[]>([]);
|
||||
|
||||
public get onCurrentSceneChanged() {
|
||||
return this.currentScene.subscribable;
|
||||
}
|
||||
private readonly currentScene = new ValueDispatcher<Scene>(null);
|
||||
|
||||
public readonly version = CORE_VERSION;
|
||||
public readonly meta: Meta<ProjectMetadata>;
|
||||
public readonly background: Rect;
|
||||
public readonly master: Layer;
|
||||
public readonly center: Vector2d;
|
||||
public threadsCallback: ThreadsCallback;
|
||||
public frame = 0;
|
||||
|
||||
public get time(): number {
|
||||
return this.framesToSeconds(this.frame);
|
||||
}
|
||||
|
||||
public get thread(): Thread {
|
||||
return this.currentThread;
|
||||
}
|
||||
|
||||
public get framerate(): number {
|
||||
return this.framesPerSeconds;
|
||||
}
|
||||
@@ -57,78 +49,120 @@ export class Project extends Stage {
|
||||
}
|
||||
|
||||
public set resolutionScale(value: number) {
|
||||
this.master.canvas.setPixelRatio(value);
|
||||
this._resolutionScale = value;
|
||||
this.updateCanvas();
|
||||
}
|
||||
|
||||
private framesPerSeconds = 30;
|
||||
private readonly sceneLookup: Record<string, Scene> = {};
|
||||
private previousScene: Scene = null;
|
||||
private currentScene: Scene = null;
|
||||
private currentThread: Thread = null;
|
||||
|
||||
public constructor(config: ProjectConfig) {
|
||||
const {scenes, ...rest} = config;
|
||||
super({
|
||||
listening: false,
|
||||
container: document.createElement('div'),
|
||||
...ProjectSize.FullHD,
|
||||
...rest,
|
||||
});
|
||||
|
||||
this.meta = Meta.getMetaFor(PROJECT_FILE_NAME);
|
||||
this.offset({
|
||||
x: this.width() / -2,
|
||||
y: this.height() / -2,
|
||||
});
|
||||
|
||||
this.center = {
|
||||
x: this.width() / 2,
|
||||
y: this.height() / 2,
|
||||
};
|
||||
|
||||
this.background = new Rect({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: this.width(),
|
||||
height: this.height(),
|
||||
fill: config.background || '#ff00ff',
|
||||
visible: config.background !== false,
|
||||
});
|
||||
|
||||
this.master = new Layer({name: 'master'});
|
||||
this.master.add(this.background);
|
||||
this.add(this.master);
|
||||
|
||||
for (const scene of scenes) {
|
||||
if (
|
||||
scene.name === undefined ||
|
||||
scene.name === '__WEBPACK_DEFAULT_EXPORT__'
|
||||
) {
|
||||
console.warn('Runner without a name: ', scene);
|
||||
}
|
||||
const handle = new Scene(this, scene);
|
||||
if (this.sceneLookup[scene.name]) {
|
||||
console.warn('Duplicated scene name: ', scene.name);
|
||||
handle.name(scene.name + Math.random());
|
||||
}
|
||||
this.sceneLookup[handle.name()] = handle;
|
||||
handle.threadsCallback = thread => {
|
||||
if (this.currentScene === handle) {
|
||||
this.currentThread = thread;
|
||||
this.threadsCallback?.(thread);
|
||||
}
|
||||
};
|
||||
private updateCanvas() {
|
||||
if (this.canvas) {
|
||||
this.canvas.width = this.width * this._resolutionScale;
|
||||
this.canvas.height = this.height * this._resolutionScale;
|
||||
}
|
||||
}
|
||||
|
||||
public draw(): this {
|
||||
this.master.drawScene();
|
||||
return this;
|
||||
public setCanvas(value: HTMLCanvasElement) {
|
||||
this.canvas = value;
|
||||
this.context = value?.getContext('2d');
|
||||
this.updateCanvas();
|
||||
}
|
||||
|
||||
public reload(runners: SceneRunner[]) {
|
||||
public setSize(size: Size): void;
|
||||
public setSize(width: number, height: number): void;
|
||||
public setSize(value: Size | number, height?: number): void {
|
||||
if (typeof value === 'object') {
|
||||
this.width = value.width;
|
||||
this.height = value.height;
|
||||
} else {
|
||||
this.width = value;
|
||||
this.height = height;
|
||||
}
|
||||
this.updateCanvas();
|
||||
}
|
||||
|
||||
public getSize(): Size {
|
||||
return {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
};
|
||||
}
|
||||
|
||||
public readonly name: string;
|
||||
private _resolutionScale = 1;
|
||||
private framesPerSeconds = 30;
|
||||
private readonly sceneLookup: Record<string, Scene> = {};
|
||||
private previousScene: Scene = null;
|
||||
private background: string | false;
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
|
||||
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;
|
||||
this.meta = Meta.getMetaFor(PROJECT_FILE_NAME);
|
||||
|
||||
for (const scene of config.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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public transformCanvas(context: CanvasRenderingContext2D) {
|
||||
context.setTransform(
|
||||
this._resolutionScale,
|
||||
0,
|
||||
0,
|
||||
this._resolutionScale,
|
||||
(this.width * this._resolutionScale) / 2,
|
||||
(this.height * this._resolutionScale) / 2,
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (!this.canvas) return;
|
||||
|
||||
this.transformCanvas(this.context);
|
||||
if (this.background) {
|
||||
this.context.save();
|
||||
this.context.fillStyle = this.background;
|
||||
this.context.fillRect(
|
||||
this.width / -2,
|
||||
this.height / -2,
|
||||
this.width,
|
||||
this.height,
|
||||
);
|
||||
this.context.restore();
|
||||
} else {
|
||||
this.context.clearRect(
|
||||
this.width / -2,
|
||||
this.height / -2,
|
||||
this.width,
|
||||
this.height,
|
||||
);
|
||||
}
|
||||
|
||||
this.previousScene?.render(this.context, this.canvas);
|
||||
this.currentScene.current?.render(this.context, this.canvas);
|
||||
}
|
||||
|
||||
public reload(runners: SceneDescription[]) {
|
||||
for (const runner of runners) {
|
||||
this.sceneLookup[runner.name]?.reload(runner);
|
||||
this.sceneLookup[runner.name]?.reload(runner.config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,88 +175,66 @@ export class Project extends Stage {
|
||||
public async next(speed = 1): Promise<boolean> {
|
||||
if (this.previousScene) {
|
||||
await this.previousScene.next();
|
||||
if (!this.currentScene || this.currentScene.isAfterTransitionIn()) {
|
||||
this.previousScene.remove();
|
||||
if (
|
||||
!this.currentScene.current ||
|
||||
this.currentScene.current.isAfterTransitionIn()
|
||||
) {
|
||||
this.previousScene = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.frame += speed;
|
||||
|
||||
if (this.currentScene) {
|
||||
await this.currentScene.next();
|
||||
if (this.currentScene.canTransitionOut()) {
|
||||
this.previousScene = this.currentScene;
|
||||
this.currentScene = this.getNextScene(this.previousScene);
|
||||
if (this.currentScene) {
|
||||
await this.currentScene.reset(this.previousScene);
|
||||
this.master.add(this.currentScene);
|
||||
if (this.currentScene.current) {
|
||||
await this.currentScene.current.next();
|
||||
if (this.currentScene.current.canTransitionOut()) {
|
||||
this.previousScene = this.currentScene.current;
|
||||
this.currentScene.current = this.getNextScene(this.previousScene);
|
||||
if (this.currentScene.current) {
|
||||
await this.currentScene.current.reset(this.previousScene);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !this.currentScene || this.currentScene.isFinished();
|
||||
return !this.currentScene.current || this.currentScene.current.isFinished();
|
||||
}
|
||||
|
||||
public async recalculate() {
|
||||
const initialScene = this.currentScene;
|
||||
this.previousScene?.remove();
|
||||
this.previousScene = null;
|
||||
this.currentScene = null;
|
||||
|
||||
this.frame = 0;
|
||||
let offset = 0;
|
||||
const scenes = Object.values(this.sceneLookup);
|
||||
for (const scene of scenes) {
|
||||
if (scene.isMarkedAsCached()) {
|
||||
scene.firstFrame += offset;
|
||||
this.frame = scene.lastFrame;
|
||||
} else {
|
||||
this.currentScene = scene;
|
||||
scene.firstFrame = this.frame;
|
||||
scene.transitionDuration = -1;
|
||||
await scene.reset();
|
||||
while (!scene.canTransitionOut()) {
|
||||
if (scene.transitionDuration < 0 && scene.isAfterTransitionIn()) {
|
||||
scene.transitionDuration = this.frame - scene.firstFrame;
|
||||
}
|
||||
this.frame++;
|
||||
await scene.next();
|
||||
}
|
||||
offset += this.frame - scene.lastFrame;
|
||||
scene.lastFrame = this.frame;
|
||||
scene.markAsCached();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
}
|
||||
await scene.recalculate();
|
||||
}
|
||||
|
||||
this.currentScene = initialScene;
|
||||
this.scenes.current = scenes;
|
||||
}
|
||||
|
||||
public async seek(frame: number, speed = 1): Promise<boolean> {
|
||||
if (this.currentScene.current && !this.currentScene.current.isCached()) {
|
||||
console.warn(
|
||||
'Attempting to seek a project with an invalidated scene:',
|
||||
this.currentScene.current.name,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
frame <= this.frame ||
|
||||
!this.currentScene ||
|
||||
(this.currentScene.isMarkedAsCached() &&
|
||||
this.currentScene.lastFrame < frame)
|
||||
!this.currentScene.current ||
|
||||
(this.currentScene.current.isCached() &&
|
||||
this.currentScene.current.lastFrame < frame)
|
||||
) {
|
||||
const scene = this.findBestScene(frame);
|
||||
if (scene !== this.currentScene) {
|
||||
this.previousScene?.remove();
|
||||
if (scene !== this.currentScene.current) {
|
||||
this.previousScene = null;
|
||||
this.currentScene?.remove();
|
||||
this.currentScene = scene;
|
||||
this.currentScene.current = scene;
|
||||
|
||||
this.frame = this.currentScene.firstFrame;
|
||||
this.master.add(this.currentScene);
|
||||
await this.currentScene.reset();
|
||||
this.frame = this.currentScene.current.firstFrame;
|
||||
await this.currentScene.current.reset();
|
||||
} else if (this.frame >= frame) {
|
||||
this.previousScene?.remove();
|
||||
this.previousScene = null;
|
||||
this.frame = this.currentScene.firstFrame;
|
||||
await this.currentScene.reset();
|
||||
this.frame = this.currentScene.current.firstFrame;
|
||||
await this.currentScene.current.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,7 +249,14 @@ export class Project extends Stage {
|
||||
private findBestScene(frame: number): Scene {
|
||||
let lastScene = null;
|
||||
for (const scene of Object.values(this.sceneLookup)) {
|
||||
if (!scene.isMarkedAsCached() || scene.lastFrame > frame) {
|
||||
if (!scene.isCached()) {
|
||||
console.warn(
|
||||
'Attempting to seek a project with an invalidated scene:',
|
||||
scene.name,
|
||||
);
|
||||
return scene;
|
||||
}
|
||||
if (scene.lastFrame > frame) {
|
||||
return scene;
|
||||
}
|
||||
lastScene = scene;
|
||||
@@ -269,7 +288,7 @@ export class Project extends Stage {
|
||||
|
||||
public async getBlob(): Promise<Blob> {
|
||||
return new Promise<Blob>(resolve =>
|
||||
this.master.getNativeCanvasElement().toBlob(resolve, 'image/png'),
|
||||
this.canvas.toBlob(resolve, 'image/png'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
196
src/Scene.ts
196
src/Scene.ts
@@ -1,196 +0,0 @@
|
||||
import type {Project} from './Project';
|
||||
import {LayerConfig} from 'konva/lib/Layer';
|
||||
import {
|
||||
isPromise,
|
||||
ThreadGenerator,
|
||||
threads,
|
||||
ThreadsCallback,
|
||||
} from './threading';
|
||||
import {Group} from 'konva/lib/Group';
|
||||
import {Shape} from 'konva/lib/Shape';
|
||||
import {SceneTransition} from './transitions';
|
||||
import {decorate, KonvaNode, threadable} from './decorators';
|
||||
import {setScene} from './utils';
|
||||
import {Meta, Metadata} from './Meta';
|
||||
import {SavedTimeEvent, TimeEvents} from './TimeEvents';
|
||||
|
||||
export interface SceneRunner {
|
||||
(layer: Scene, project: Project): ThreadGenerator;
|
||||
}
|
||||
|
||||
export enum SceneState {
|
||||
Initial,
|
||||
AfterTransitionIn,
|
||||
CanTransitionOut,
|
||||
Finished,
|
||||
}
|
||||
|
||||
export interface SceneMetadata extends Metadata {
|
||||
timeEvents: SavedTimeEvent[];
|
||||
}
|
||||
|
||||
@KonvaNode()
|
||||
export class Scene extends Group {
|
||||
public threadsCallback: ThreadsCallback = null;
|
||||
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;
|
||||
}
|
||||
public set lastFrame(value: number) {
|
||||
this.duration = value - this.firstFrame;
|
||||
}
|
||||
|
||||
private previousScene: Scene = null;
|
||||
private runner: ThreadGenerator;
|
||||
private state: SceneState = SceneState.Initial;
|
||||
private cached = false;
|
||||
private counters: Record<string, number> = {};
|
||||
|
||||
public constructor(
|
||||
public readonly project: Project,
|
||||
private runnerFactory: SceneRunner,
|
||||
config: LayerConfig = {},
|
||||
) {
|
||||
super({
|
||||
name: runnerFactory.name,
|
||||
width: project.width(),
|
||||
height: project.height(),
|
||||
...config,
|
||||
});
|
||||
decorate(runnerFactory, threadable());
|
||||
|
||||
this.meta = Meta.getMetaFor(`${this.name()}.scene`);
|
||||
this.timeEvents = new TimeEvents(this);
|
||||
}
|
||||
|
||||
public markAsCached() {
|
||||
this.cached = true;
|
||||
this.timeEvents.onCache();
|
||||
}
|
||||
|
||||
public isMarkedAsCached() {
|
||||
return this.cached;
|
||||
}
|
||||
|
||||
public reload(runnerFactory?: SceneRunner) {
|
||||
if (runnerFactory) {
|
||||
this.runnerFactory = runnerFactory;
|
||||
}
|
||||
this.cached = false;
|
||||
this.timeEvents.onReload();
|
||||
}
|
||||
|
||||
public async reset(previousScene: Scene = null) {
|
||||
this.x(0).y(0).opacity(1).show();
|
||||
this.counters = {};
|
||||
this.destroyChildren();
|
||||
this.previousScene = previousScene;
|
||||
this.runner = threads(
|
||||
() => this.runnerFactory(this, this.project),
|
||||
(...args) => this.threadsCallback?.(...args),
|
||||
);
|
||||
this.state = SceneState.Initial;
|
||||
await this.next();
|
||||
}
|
||||
|
||||
public async next() {
|
||||
setScene(this);
|
||||
let result = this.runner.next();
|
||||
this.updateLayout();
|
||||
while (result.value) {
|
||||
if (isPromise(result.value)) {
|
||||
const value = await result.value;
|
||||
result = this.runner.next(value);
|
||||
} else {
|
||||
console.warn('Invalid value: ', result.value);
|
||||
result = this.runner.next();
|
||||
}
|
||||
this.updateLayout();
|
||||
}
|
||||
|
||||
if (result.done) {
|
||||
this.state = SceneState.Finished;
|
||||
}
|
||||
}
|
||||
|
||||
public updateLayout(): boolean {
|
||||
super.updateLayout();
|
||||
const result = this.wasDirty();
|
||||
let limit = 10;
|
||||
while (this.wasDirty() && limit > 0) {
|
||||
super.updateLayout();
|
||||
limit--;
|
||||
}
|
||||
|
||||
if (limit === 0) {
|
||||
console.warn('Layout iteration limit exceeded');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@threadable()
|
||||
public *transition(transitionRunner?: SceneTransition) {
|
||||
if (transitionRunner) {
|
||||
yield* transitionRunner(this, this.previousScene);
|
||||
} else {
|
||||
this.previousScene?.hide();
|
||||
}
|
||||
if (this.state === SceneState.Initial) {
|
||||
this.state = SceneState.AfterTransitionIn;
|
||||
} else {
|
||||
console.warn(
|
||||
`Scene ${this.name} transitioned in an unexpected state: `,
|
||||
this.state,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public canFinish() {
|
||||
if (this.state === SceneState.AfterTransitionIn) {
|
||||
this.state = SceneState.CanTransitionOut;
|
||||
} else {
|
||||
console.warn(
|
||||
`Scene ${this.name} was marked as finished in an unexpected state: `,
|
||||
this.state,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public isAfterTransitionIn(): boolean {
|
||||
return this.state === SceneState.AfterTransitionIn;
|
||||
}
|
||||
|
||||
public isFinished(): boolean {
|
||||
return this.state === SceneState.Finished;
|
||||
}
|
||||
|
||||
public canTransitionOut(): boolean {
|
||||
return (
|
||||
this.state === SceneState.CanTransitionOut ||
|
||||
this.state === SceneState.Finished
|
||||
);
|
||||
}
|
||||
|
||||
public add(...children: (Shape | Group)[]): this {
|
||||
super.add(...children.flat());
|
||||
this.updateLayout();
|
||||
return this;
|
||||
}
|
||||
|
||||
public generateNodeId(type: string): string {
|
||||
let id = 0;
|
||||
if (type in this.counters) {
|
||||
id = ++this.counters[type];
|
||||
} else {
|
||||
this.counters[type] = id;
|
||||
}
|
||||
|
||||
return `${this.name()}.${type}.${id}`;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import './globals';
|
||||
import type {Size} from './types';
|
||||
import type {SceneRunner} from './Scene';
|
||||
import type {SceneDescription} from './scenes';
|
||||
import {Project, ProjectSize} from './Project';
|
||||
import {Player} from './player';
|
||||
import {hot} from './hot';
|
||||
@@ -8,7 +8,7 @@ import {AudioManager} from './media';
|
||||
|
||||
interface BootstrapConfig {
|
||||
name: string;
|
||||
scenes: SceneRunner[];
|
||||
scenes: SceneDescription[];
|
||||
size?: Size;
|
||||
background?: string | false;
|
||||
audio?: string;
|
||||
|
||||
@@ -6,7 +6,7 @@ import {Group} from 'konva/lib/Group';
|
||||
import {ContainerConfig} from 'konva/lib/Container';
|
||||
import {Arrow} from './Arrow';
|
||||
import {map} from '../tweening';
|
||||
import {useScene} from '../utils';
|
||||
import {useKonvaView} from '../scenes';
|
||||
|
||||
export interface ConnectionConfig extends ContainerConfig {
|
||||
start?: Pin;
|
||||
@@ -180,17 +180,17 @@ export class Connection extends Group {
|
||||
return;
|
||||
}
|
||||
|
||||
const scene = useScene();
|
||||
const view = useKonvaView();
|
||||
const crossing = this.crossing
|
||||
? this.crossing.getAbsolutePosition(scene)
|
||||
? this.crossing.getAbsolutePosition(view)
|
||||
: {x: 0.5, y: 0.5};
|
||||
|
||||
const fromRect = this.start.getClientRect({relativeTo: scene});
|
||||
const fromRect = this.start.getClientRect({relativeTo: view});
|
||||
fromRect.width /= 2;
|
||||
fromRect.height /= 2;
|
||||
fromRect.x += fromRect.width;
|
||||
fromRect.y += fromRect.height;
|
||||
const toRect = this.end.getClientRect({relativeTo: scene});
|
||||
const toRect = this.end.getClientRect({relativeTo: view});
|
||||
|
||||
toRect.width /= 2;
|
||||
toRect.height /= 2;
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Center, flipOrigin, getOriginDelta, Origin} from '../types';
|
||||
import {GetSet, IRect} from 'konva/lib/types';
|
||||
import {getset, KonvaNode} from '../decorators';
|
||||
import {Node} from 'konva/lib/Node';
|
||||
import {useScene} from '../utils';
|
||||
import {useKonvaView} from '../scenes';
|
||||
|
||||
export interface PinConfig extends ContainerConfig {
|
||||
target?: Node;
|
||||
@@ -40,7 +40,9 @@ export class Pin extends Group {
|
||||
const attach = this.attach();
|
||||
if (attach) {
|
||||
const attachDirection = flipOrigin(attach.getOrigin(), this.direction());
|
||||
const rect = this.getClientRect({relativeTo: useScene()});
|
||||
const rect = this.getClientRect({
|
||||
relativeTo: useKonvaView(),
|
||||
});
|
||||
const offset = getOriginDelta(rect, Origin.TopLeft, attachDirection);
|
||||
attach.position({
|
||||
x: rect.x + offset.x,
|
||||
|
||||
@@ -8,7 +8,7 @@ decorate(any, threadable());
|
||||
* Example:
|
||||
* ```
|
||||
* // current time: 0s
|
||||
* yield* all(
|
||||
* yield* any(
|
||||
* rect.fill('#ff0000', 2),
|
||||
* rect.opacity(1, 1),
|
||||
* );
|
||||
|
||||
@@ -9,5 +9,6 @@ export * from './chain';
|
||||
export * from './delay';
|
||||
export * from './every';
|
||||
export * from './loop';
|
||||
export * from './noop';
|
||||
export * from './scheduling';
|
||||
export * from './sequence';
|
||||
|
||||
8
src/flow/noop.ts
Normal file
8
src/flow/noop.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import {ThreadGenerator} from '../threading';
|
||||
|
||||
/**
|
||||
* Do nothing.
|
||||
*/
|
||||
export function* noop(): ThreadGenerator {
|
||||
// do nothing
|
||||
}
|
||||
@@ -3,25 +3,6 @@ import {ThreadGenerator} from '../threading';
|
||||
import {useProject, useScene} from '../utils';
|
||||
|
||||
decorate(waitUntil, threadable());
|
||||
/**
|
||||
* Wait until the given time.
|
||||
*
|
||||
* Example:
|
||||
* ```ts
|
||||
* // current time: 0s
|
||||
* yield waitUntil(2);
|
||||
* // current time: 2s
|
||||
* yield waitUntil(3);
|
||||
* // current time: 3s
|
||||
* ```
|
||||
*
|
||||
* @param time Absolute time in seconds.
|
||||
* @param after
|
||||
*/
|
||||
export function waitUntil(
|
||||
time: number,
|
||||
after?: ThreadGenerator,
|
||||
): ThreadGenerator;
|
||||
/**
|
||||
* Wait until the given time event.
|
||||
*
|
||||
@@ -36,19 +17,12 @@ export function waitUntil(
|
||||
* @param event Name of the time event.
|
||||
* @param after
|
||||
*/
|
||||
export function waitUntil(
|
||||
event: string,
|
||||
after?: ThreadGenerator,
|
||||
): ThreadGenerator;
|
||||
export function* waitUntil(
|
||||
targetTime: number | string = 0,
|
||||
event: string,
|
||||
after?: ThreadGenerator,
|
||||
): ThreadGenerator {
|
||||
const scene = useScene();
|
||||
const frames =
|
||||
typeof targetTime === 'string'
|
||||
? scene.timeEvents.register(targetTime)
|
||||
: scene.project.secondsToFrames(targetTime);
|
||||
const frames = scene.timeEvents.register(event);
|
||||
|
||||
while (scene.project.frame < frames) {
|
||||
yield;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Context} from 'konva/lib/Context';
|
||||
import type {Context} from 'konva/lib/Context';
|
||||
import {PossibleSpacing, Spacing} from '../types';
|
||||
|
||||
export const CanvasHelper = {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
export * from './bootstrap';
|
||||
export * from './Meta';
|
||||
export * from './Project';
|
||||
export * from './Scene';
|
||||
export * from './symbols';
|
||||
export * from './TimeEvents';
|
||||
|
||||
@@ -341,8 +341,8 @@ Node.prototype._getTransform = function (this: Node) {
|
||||
const super_setAttrs = Node.prototype.setAttrs;
|
||||
Node.prototype.setAttrs = function (this: Node, config: unknown) {
|
||||
if (!(NODE_ID in this.attrs)) {
|
||||
const scene = useScene();
|
||||
if (scene) {
|
||||
const scene: any = useScene();
|
||||
if (scene && 'generateNodeId' in scene) {
|
||||
const type = this.className;
|
||||
this.attrs[NODE_ID] = scene.generateNodeId(type);
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ export class Player {
|
||||
public setScale(scale: number) {
|
||||
this.project.resolutionScale = scale;
|
||||
this.updateState({scale});
|
||||
this.project.draw();
|
||||
this.project.render();
|
||||
}
|
||||
|
||||
public requestPreviousFrame(): void {
|
||||
@@ -229,7 +229,7 @@ export class Player {
|
||||
const duration = this.project.frame;
|
||||
const frame = commands.seek < 0 ? this.frame.current : commands.seek;
|
||||
const finished = await this.project.seek(frame);
|
||||
this.project.draw();
|
||||
this.project.render();
|
||||
this.updateState({
|
||||
duration,
|
||||
finished,
|
||||
@@ -260,7 +260,7 @@ export class Player {
|
||||
// Rendering
|
||||
if (state.render) {
|
||||
state.finished = await this.project.next();
|
||||
this.project.draw();
|
||||
this.project.render();
|
||||
await this.frameRendered.dispatch({
|
||||
frame: this.project.frame,
|
||||
data: await this.project.getBlob(),
|
||||
@@ -318,7 +318,7 @@ export class Player {
|
||||
}
|
||||
|
||||
// Draw the project
|
||||
this.project.draw();
|
||||
this.project.render();
|
||||
|
||||
// handle finishing
|
||||
if (state.finished) {
|
||||
|
||||
252
src/scenes/GeneratorScene.ts
Normal file
252
src/scenes/GeneratorScene.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import {isPromise, Thread, ThreadGenerator, threads} from '../threading';
|
||||
import {Meta} from '../Meta';
|
||||
import {TimeEvents} from './TimeEvents';
|
||||
import {EventDispatcher, ValueDispatcher} from '../events';
|
||||
import {Project} from '../Project';
|
||||
import {decorate, threadable} from '../decorators';
|
||||
import {setScene} from '../utils';
|
||||
import {CachedSceneData, Scene, SceneMetadata} from './Scene';
|
||||
import {
|
||||
isTransitionable,
|
||||
SceneTransition,
|
||||
Transitionable,
|
||||
TransitionContext,
|
||||
} from './Transitionable';
|
||||
import {Threadable} from './Threadable';
|
||||
import {Size} from '../types';
|
||||
import {SceneState} from './SceneState';
|
||||
|
||||
export interface ThreadGeneratorFactory<T> {
|
||||
(view: T): ThreadGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default implementation of the {@link Scene} interface.
|
||||
*
|
||||
* Uses generators to control the animation.
|
||||
*/
|
||||
export abstract class GeneratorScene<T>
|
||||
implements Scene<ThreadGeneratorFactory<T>>, Transitionable, Threadable
|
||||
{
|
||||
public readonly timeEvents: TimeEvents;
|
||||
public readonly meta: Meta<SceneMetadata>;
|
||||
|
||||
public get firstFrame() {
|
||||
return this.cache.current.firstFrame;
|
||||
}
|
||||
|
||||
public get lastFrame() {
|
||||
return this.firstFrame + this.cache.current.duration;
|
||||
}
|
||||
|
||||
public get onCacheChanged() {
|
||||
return this.cache.subscribable;
|
||||
}
|
||||
private readonly cache = new ValueDispatcher<CachedSceneData>({
|
||||
firstFrame: 0,
|
||||
transitionDuration: 0,
|
||||
duration: 0,
|
||||
lastFrame: 0,
|
||||
});
|
||||
|
||||
public get onReloaded() {
|
||||
return this.reloaded.subscribable;
|
||||
}
|
||||
private readonly reloaded = new EventDispatcher<void>();
|
||||
|
||||
public get onRecalculated() {
|
||||
return this.recalculated.subscribable;
|
||||
}
|
||||
private readonly recalculated = new EventDispatcher<void>();
|
||||
|
||||
public get onThreadChanged() {
|
||||
return this.thread.subscribable;
|
||||
}
|
||||
private readonly thread = new ValueDispatcher<Thread>(null);
|
||||
|
||||
private previousScene: Scene = null;
|
||||
private runner: ThreadGenerator;
|
||||
private state: SceneState = SceneState.Initial;
|
||||
private cached = false;
|
||||
private counters: Record<string, number> = {};
|
||||
|
||||
public constructor(
|
||||
public readonly project: Project,
|
||||
public readonly name: string,
|
||||
private runnerFactory: ThreadGeneratorFactory<T>,
|
||||
) {
|
||||
decorate(this.runnerFactory, threadable(name));
|
||||
|
||||
this.meta = Meta.getMetaFor(`${name}.scene`);
|
||||
this.timeEvents = new TimeEvents(this);
|
||||
}
|
||||
|
||||
public abstract getView(): T;
|
||||
|
||||
/**
|
||||
* Update the view.
|
||||
*
|
||||
* Invoked after each step of the main generator.
|
||||
* Can be used for calculating layout.
|
||||
*
|
||||
* Can modify the state of the view.
|
||||
*/
|
||||
public update() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public abstract render(
|
||||
context: CanvasRenderingContext2D,
|
||||
canvas: HTMLCanvasElement,
|
||||
): void;
|
||||
|
||||
public reload(runnerFactory?: ThreadGeneratorFactory<T>) {
|
||||
if (runnerFactory) {
|
||||
this.runnerFactory = runnerFactory;
|
||||
}
|
||||
this.cached = false;
|
||||
this.reloaded.dispatch();
|
||||
}
|
||||
|
||||
public async recalculate() {
|
||||
const cached = this.cache.current;
|
||||
cached.firstFrame = this.project.frame;
|
||||
cached.lastFrame = cached.firstFrame + cached.duration;
|
||||
|
||||
if (this.isCached()) {
|
||||
this.project.frame = cached.lastFrame;
|
||||
this.cache.current = {...cached};
|
||||
return;
|
||||
}
|
||||
|
||||
cached.transitionDuration = -1;
|
||||
await this.reset();
|
||||
while (!this.canTransitionOut()) {
|
||||
if (cached.transitionDuration < 0 && this.isAfterTransitionIn()) {
|
||||
cached.transitionDuration = this.project.frame - cached.firstFrame;
|
||||
}
|
||||
this.project.frame++;
|
||||
await this.next();
|
||||
}
|
||||
|
||||
cached.lastFrame = this.project.frame;
|
||||
cached.duration = cached.lastFrame - cached.firstFrame;
|
||||
// Prevent the page from becoming unresponsive.
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
this.cached = true;
|
||||
this.cache.current = {...cached};
|
||||
this.recalculated.dispatch();
|
||||
}
|
||||
|
||||
public async next() {
|
||||
setScene(this);
|
||||
let result = this.runner.next();
|
||||
this.update();
|
||||
while (result.value) {
|
||||
if (isPromise(result.value)) {
|
||||
const value = await result.value;
|
||||
result = this.runner.next(value);
|
||||
} else {
|
||||
console.warn('Invalid value: ', result.value);
|
||||
result = this.runner.next();
|
||||
}
|
||||
this.update();
|
||||
}
|
||||
|
||||
if (result.done) {
|
||||
this.state = SceneState.Finished;
|
||||
}
|
||||
}
|
||||
|
||||
public async reset(previousScene: Scene = null) {
|
||||
this.counters = {};
|
||||
this.previousScene = previousScene;
|
||||
this.runner = threads(
|
||||
() => this.runnerFactory(this.getView()),
|
||||
thread => {
|
||||
this.thread.current = thread;
|
||||
},
|
||||
);
|
||||
this.state = SceneState.Initial;
|
||||
setScene(this);
|
||||
await this.next();
|
||||
}
|
||||
|
||||
public getSize(): Size {
|
||||
return this.project.getSize();
|
||||
}
|
||||
|
||||
public isAfterTransitionIn(): boolean {
|
||||
return this.state === SceneState.AfterTransitionIn;
|
||||
}
|
||||
|
||||
public canTransitionOut(): boolean {
|
||||
return (
|
||||
this.state === SceneState.CanTransitionOut ||
|
||||
this.state === SceneState.Finished
|
||||
);
|
||||
}
|
||||
|
||||
public isFinished(): boolean {
|
||||
return this.state === SceneState.Finished;
|
||||
}
|
||||
|
||||
public enterCanTransitionOut() {
|
||||
if (this.state === SceneState.AfterTransitionIn) {
|
||||
this.state = SceneState.CanTransitionOut;
|
||||
} else {
|
||||
console.warn(
|
||||
`Scene ${this.name} was marked as finished in an unexpected state: `,
|
||||
this.state,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public enterAfterTransitionIn() {
|
||||
if (this.state === SceneState.Initial) {
|
||||
this.state = SceneState.AfterTransitionIn;
|
||||
} else {
|
||||
console.warn(
|
||||
`Scene ${this.name} transitioned in an unexpected state: `,
|
||||
this.state,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public isCached() {
|
||||
return this.cached;
|
||||
}
|
||||
|
||||
public generateNodeId(type: string): string {
|
||||
let id = 0;
|
||||
if (type in this.counters) {
|
||||
id = ++this.counters[type];
|
||||
} else {
|
||||
this.counters[type] = id;
|
||||
}
|
||||
|
||||
return `${this.name}.${type}.${id}`;
|
||||
}
|
||||
|
||||
//#region Transitionable Interface
|
||||
|
||||
@threadable()
|
||||
public *transition(transitionRunner?: SceneTransition): ThreadGenerator {
|
||||
const previous = isTransitionable(this.previousScene)
|
||||
? this.previousScene.getTransitionContext()
|
||||
: null;
|
||||
|
||||
if (!transitionRunner) {
|
||||
previous?.visible(false);
|
||||
this.enterAfterTransitionIn();
|
||||
return;
|
||||
}
|
||||
|
||||
yield* transitionRunner(this.getTransitionContext(), previous);
|
||||
this.enterAfterTransitionIn();
|
||||
}
|
||||
|
||||
public abstract getTransitionContext(): TransitionContext;
|
||||
|
||||
//#endregion
|
||||
}
|
||||
89
src/scenes/Inspectable.ts
Normal file
89
src/scenes/Inspectable.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import {Rect, Vector2} from '../types';
|
||||
|
||||
/**
|
||||
* Represents an element to inspect.
|
||||
*
|
||||
* The type is not important because the UI does not interact with it.
|
||||
* It serves as a key that will be passed back to an Inspectable scene to
|
||||
* receive more information about said element.
|
||||
*/
|
||||
export type InspectedElement = unknown;
|
||||
|
||||
/**
|
||||
* Represents attributes of an inspected element.
|
||||
*/
|
||||
export type InspectedAttributes = Record<string, any>;
|
||||
|
||||
/**
|
||||
* Represents different sizes and/or coordinates of an inspected element.
|
||||
*/
|
||||
export interface InspectedSize {
|
||||
/**
|
||||
* Bounding box of the element (with padding).
|
||||
*/
|
||||
rect?: Rect;
|
||||
|
||||
/**
|
||||
* Bounding box of the content of this element (without padding).
|
||||
*/
|
||||
contentRect?: Rect;
|
||||
|
||||
/**
|
||||
* Bounding box of the element (with margin).
|
||||
*/
|
||||
marginRect?: Rect;
|
||||
|
||||
/**
|
||||
* The absolute position of the object's origin.
|
||||
*/
|
||||
position?: Vector2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenes can implement this interface to make their components
|
||||
* inspectable through the UI.
|
||||
*/
|
||||
export interface Inspectable {
|
||||
/**
|
||||
* Get a possible element to inspect at a given position.
|
||||
*
|
||||
* @param x
|
||||
* @param y
|
||||
*/
|
||||
inspectPosition(x: number, y: number): InspectedElement | null;
|
||||
|
||||
/**
|
||||
* Validate if the inspected element is still valid.
|
||||
*
|
||||
* If a scene destroys and recreates its components upon every reset, the
|
||||
* reference may no longer be valid. Even though the component is still
|
||||
* present. This method should check that and return a new reference.
|
||||
*
|
||||
* See {@link KonvaScene.validateInspection()} for a sample implementation.
|
||||
*
|
||||
* @param element
|
||||
*/
|
||||
validateInspection(element: InspectedElement | null): InspectedElement | null;
|
||||
|
||||
/**
|
||||
* Return the attributes of the inspected element.
|
||||
*
|
||||
* This information will be displayed in the "Properties" panel.
|
||||
*
|
||||
* @param element
|
||||
*/
|
||||
inspectAttributes(element: InspectedElement): InspectedAttributes | null;
|
||||
|
||||
/**
|
||||
* Return the sizes of the inspected element.
|
||||
*
|
||||
* This information will be used to draw the bounding boxes on the screen.
|
||||
*
|
||||
* @param element
|
||||
*/
|
||||
inspectBoundingBox(element: InspectedElement): InspectedSize;
|
||||
}
|
||||
|
||||
export function isInspectable(value: any): value is Inspectable {
|
||||
return value && typeof value === 'object' && 'validateInspection' in value;
|
||||
}
|
||||
192
src/scenes/KonvaScene.ts
Normal file
192
src/scenes/KonvaScene.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import {Container} from 'konva/lib/Container';
|
||||
import {Scene, SceneDescription} from './Scene';
|
||||
import {HitCanvas, SceneCanvas} from 'konva/lib/Canvas';
|
||||
import {Shape, shapes} from 'konva/lib/Shape';
|
||||
import {Group} from 'konva/lib/Group';
|
||||
import {GeneratorScene, ThreadGeneratorFactory} from './GeneratorScene';
|
||||
import {useScene} from '../utils';
|
||||
import {SceneTransition, TransitionContext} from './Transitionable';
|
||||
import {
|
||||
Inspectable,
|
||||
InspectedElement,
|
||||
InspectedAttributes,
|
||||
InspectedSize,
|
||||
} from './Inspectable';
|
||||
import {Util} from 'konva/lib/Util';
|
||||
import {Node} from 'konva/lib/Node';
|
||||
import {Konva} from 'konva/lib/Global';
|
||||
import {NODE_ID} from '../symbols';
|
||||
import {ThreadGenerator} from '../threading';
|
||||
|
||||
Konva.autoDrawEnabled = false;
|
||||
|
||||
const sceneCanvasMap = new Map<HTMLCanvasElement, SceneCanvas>();
|
||||
|
||||
export function useKonvaView(): KonvaView {
|
||||
const scene = useScene();
|
||||
if (scene instanceof KonvaScene) {
|
||||
return scene.view;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a descriptor for a Konva scene.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // example.scene.ts
|
||||
*
|
||||
* export default makeKonvaScene(function* example(view) {
|
||||
* yield* view.transition();
|
||||
* // perform animation
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param factory
|
||||
*/
|
||||
export function makeKonvaScene(
|
||||
factory: ThreadGeneratorFactory<KonvaView>,
|
||||
): SceneDescription {
|
||||
return {
|
||||
name: factory.name,
|
||||
config: factory,
|
||||
klass: KonvaScene,
|
||||
};
|
||||
}
|
||||
|
||||
class KonvaView extends Container implements TransitionContext {
|
||||
public constructor(private readonly scene: KonvaScene) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start transitioning out of the current scene.
|
||||
*/
|
||||
public canFinish() {
|
||||
this.scene.enterCanTransitionOut();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc Transitionable.transition
|
||||
*/
|
||||
public transition(transitionRunner?: SceneTransition): ThreadGenerator {
|
||||
return this.scene.transition(transitionRunner);
|
||||
}
|
||||
|
||||
public updateLayout() {
|
||||
super.updateLayout();
|
||||
let limit = 10;
|
||||
while (this.wasDirty() && limit > 0) {
|
||||
super.updateLayout();
|
||||
limit--;
|
||||
}
|
||||
|
||||
if (limit === 0) {
|
||||
console.warn('Layout iteration limit exceeded');
|
||||
}
|
||||
}
|
||||
|
||||
public add(...children: (Shape | Group)[]): this {
|
||||
super.add(...children.flat());
|
||||
this.updateLayout();
|
||||
return this;
|
||||
}
|
||||
|
||||
public _validateAdd() {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
export class KonvaScene
|
||||
extends GeneratorScene<KonvaView>
|
||||
implements Inspectable
|
||||
{
|
||||
public readonly view = new KonvaView(this);
|
||||
private hitCanvas = new HitCanvas({pixelRatio: 1});
|
||||
|
||||
public getView(): KonvaView {
|
||||
return this.view;
|
||||
}
|
||||
|
||||
public update() {
|
||||
this.view.updateLayout();
|
||||
}
|
||||
|
||||
public render(context: CanvasRenderingContext2D, canvas: HTMLCanvasElement) {
|
||||
let sceneCanvas = sceneCanvasMap.get(canvas);
|
||||
if (!sceneCanvas) {
|
||||
sceneCanvas = new SceneCanvas({
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
pixelRatio: 1,
|
||||
});
|
||||
sceneCanvas._canvas = canvas;
|
||||
sceneCanvas.getContext()._context = context;
|
||||
}
|
||||
|
||||
this.view.drawScene(sceneCanvas);
|
||||
}
|
||||
|
||||
public reset(previousScene: Scene = null) {
|
||||
this.view.x(0).y(0).opacity(1).show();
|
||||
this.view.destroyChildren();
|
||||
return super.reset(previousScene);
|
||||
}
|
||||
|
||||
public getTransitionContext(): TransitionContext {
|
||||
return this.view;
|
||||
}
|
||||
|
||||
//#region Inspectable Interface
|
||||
|
||||
public inspectPosition(x: number, y: number): InspectedElement | null {
|
||||
this.hitCanvas.setSize(this.getSize().width, this.getSize().height);
|
||||
this.project.transformCanvas(this.hitCanvas.context._context);
|
||||
this.view.drawHit(this.hitCanvas, this.view);
|
||||
|
||||
const color = this.hitCanvas.context.getImageData(x, y, 1, 1).data;
|
||||
if (color[3] < 255) return null;
|
||||
const key = Util._rgbToHex(color[0], color[1], color[2]);
|
||||
return shapes[`#${key}`] ?? null;
|
||||
}
|
||||
|
||||
public validateInspection(
|
||||
element: InspectedElement | null,
|
||||
): InspectedElement | null {
|
||||
if (!(element instanceof Node)) return null;
|
||||
if (element.isAncestorOf(this.view)) return element;
|
||||
const id = element.attrs[NODE_ID];
|
||||
return (
|
||||
this.view.findOne((node: Node) => node.attrs[NODE_ID] === id) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
public inspectAttributes(
|
||||
element: InspectedElement,
|
||||
): InspectedAttributes | null {
|
||||
if (!(element instanceof Node)) return null;
|
||||
return element.attrs;
|
||||
}
|
||||
|
||||
public inspectBoundingBox(element: InspectedElement): InspectedSize {
|
||||
if (!(element instanceof Node)) return {};
|
||||
|
||||
const rect = element.getClientRect({relativeTo: this.view});
|
||||
const scale = element.getAbsoluteScale(this.view);
|
||||
const position = element.getAbsolutePosition(this.view);
|
||||
const offset = element.getOriginOffset();
|
||||
|
||||
return {
|
||||
rect,
|
||||
contentRect: element.getPadd().scale(scale).shrink(rect),
|
||||
marginRect: element.getMargin().scale(scale).expand(rect),
|
||||
position: {
|
||||
x: position.x + offset.x,
|
||||
y: position.y + offset.y,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
193
src/scenes/Scene.ts
Normal file
193
src/scenes/Scene.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import type {Project} from '../Project';
|
||||
import {Meta, Metadata} from '../Meta';
|
||||
import {SavedTimeEvent, TimeEvents} from './TimeEvents';
|
||||
import {SubscribableEvent, SubscribableValueEvent} from '../events';
|
||||
import {Size} from '../types';
|
||||
|
||||
export interface SceneMetadata extends Metadata {
|
||||
timeEvents: SavedTimeEvent[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The constructor used when creating new scenes.
|
||||
*
|
||||
* Each class implementing the {@link Scene} interface should have a matching
|
||||
* constructor.
|
||||
*
|
||||
* @template T The type of the configuration object. This object will be passed
|
||||
* to the constructor from {@link SceneDescription.config}.
|
||||
*/
|
||||
export interface SceneConstructor<T> {
|
||||
new (project: Project, name: string, config: T): Scene;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a scene exposed by a `*.scene.tsx` file.
|
||||
*
|
||||
* @template T The type of the configuration object.
|
||||
*/
|
||||
export interface SceneDescription<T = unknown> {
|
||||
/**
|
||||
* The class used to instantiate the scene.
|
||||
*/
|
||||
klass: SceneConstructor<T>;
|
||||
/**
|
||||
* Name of the scene.
|
||||
*
|
||||
* Should match the first portion of the file name (`[name].scene.ts`).
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Configuration object.
|
||||
*/
|
||||
config: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes cached information about the timing of a scene.
|
||||
*/
|
||||
export interface CachedSceneData {
|
||||
firstFrame: number;
|
||||
lastFrame: number;
|
||||
transitionDuration: number;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The main interface for scenes.
|
||||
*
|
||||
* Any class implementing this interface should have a constructor matching
|
||||
* {@link SceneConstructor}.
|
||||
*
|
||||
* @template T The type of the configuration object.
|
||||
*/
|
||||
export interface Scene<T = unknown> {
|
||||
/**
|
||||
* Name of the scene.
|
||||
*
|
||||
* Will be passed as the second argument to the constructor.
|
||||
*/
|
||||
readonly name: string;
|
||||
/**
|
||||
* Reference to the project.
|
||||
*
|
||||
* Will be passed as the first argument to the constructor.
|
||||
*/
|
||||
readonly project: Project;
|
||||
readonly timeEvents: TimeEvents;
|
||||
readonly meta: Meta<SceneMetadata>;
|
||||
|
||||
/**
|
||||
* The frame at which this scene starts.
|
||||
*/
|
||||
get firstFrame(): number;
|
||||
|
||||
/**
|
||||
* The frame at which this scene ends.
|
||||
*/
|
||||
get lastFrame(): number;
|
||||
|
||||
/**
|
||||
* Triggered when the cached data changes.
|
||||
*
|
||||
* @event CachedSceneData
|
||||
*/
|
||||
get onCacheChanged(): SubscribableValueEvent<CachedSceneData>;
|
||||
|
||||
/**
|
||||
* Triggered when the scene is reloaded.
|
||||
*
|
||||
* @event void
|
||||
*/
|
||||
get onReloaded(): SubscribableEvent<void>;
|
||||
|
||||
/**
|
||||
* Triggered after scene is recalculated.
|
||||
*
|
||||
* @event void
|
||||
*/
|
||||
get onRecalculated(): SubscribableEvent<void>;
|
||||
|
||||
/**
|
||||
* Render the scene onto a canvas.
|
||||
*
|
||||
* @param context
|
||||
* @param canvas
|
||||
*/
|
||||
render(context: CanvasRenderingContext2D, canvas: HTMLCanvasElement): void;
|
||||
|
||||
/**
|
||||
* Reload the scene.
|
||||
*
|
||||
* This method is called whenever something related to this scene has changed:
|
||||
* time events, source code, metadata, etc.
|
||||
*
|
||||
* Should trigger {@link onReloaded}.
|
||||
*
|
||||
* @param config If present, a new configuration object.
|
||||
*/
|
||||
reload(config?: T): void;
|
||||
|
||||
/**
|
||||
* Recalculate the scene.
|
||||
*
|
||||
* The task of this method is to calculate new timings stored in the cache.
|
||||
* When this method is invoked, `this.project.frame` is set to the frame at
|
||||
* which this scene should start ({@link firstFrame}).
|
||||
*
|
||||
* At the end of execution, this method should set `this.project.frame` to the
|
||||
* frame at which this scene ends ({@link lastFrame}).
|
||||
*
|
||||
* Should trigger {@link onRecalculated}.
|
||||
*/
|
||||
recalculate(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Progress this scene one frame forward.
|
||||
*/
|
||||
next(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Reset this scene to its initial state.
|
||||
*
|
||||
* @param previous If present, the previous scene.
|
||||
*/
|
||||
reset(previous?: Scene): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get the size of this scene.
|
||||
*
|
||||
* Usually return `this.project.getSize()`.
|
||||
*/
|
||||
getSize(): Size;
|
||||
|
||||
/**
|
||||
* Is this scene in the {@link SceneState.AfterTransitionIn} state?
|
||||
*/
|
||||
isAfterTransitionIn(): boolean;
|
||||
|
||||
/**
|
||||
* Is this scene in the {@link SceneState.CanTransitionOut} state?
|
||||
*/
|
||||
canTransitionOut(): boolean;
|
||||
|
||||
/**
|
||||
* Is this scene in the {@link SceneState.Finished} state?
|
||||
*/
|
||||
isFinished(): boolean;
|
||||
|
||||
/**
|
||||
* Enter the {@link SceneState.CanTransitionOut} state?
|
||||
*/
|
||||
enterCanTransitionOut(): void;
|
||||
|
||||
/**
|
||||
* Is this scene cached?
|
||||
*
|
||||
* Used only by {@link GeneratorScene}. Seeking through a project that
|
||||
* contains at least one uncached scene will log a warning to the console.
|
||||
*
|
||||
* Should always return `true`.
|
||||
*/
|
||||
isCached(): boolean;
|
||||
}
|
||||
36
src/scenes/SceneState.ts
Normal file
36
src/scenes/SceneState.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Describes the state of a scene.
|
||||
*/
|
||||
export enum SceneState {
|
||||
/**
|
||||
* The scene has just been created/reset.
|
||||
*/
|
||||
Initial,
|
||||
|
||||
/**
|
||||
* The scene has finished transitioning in.
|
||||
*
|
||||
* Informs the Project that the previous scene is no longer necessary and can
|
||||
* be disposed of.
|
||||
*
|
||||
* If a scene doesn't implement the {@link Transitionable} interface, it
|
||||
* should enter this state immediately, instead of {@link Initial}.
|
||||
*/
|
||||
AfterTransitionIn,
|
||||
|
||||
/**
|
||||
* The scene is ready to transition out.
|
||||
*
|
||||
* Informs the project that the next scene can begin.
|
||||
* The {@link Scene.next()} method will still be invoked until the next scene
|
||||
* enters {@link AfterTransitionIn}.
|
||||
*/
|
||||
CanTransitionOut,
|
||||
|
||||
/**
|
||||
* The scene has finished.
|
||||
*
|
||||
* Invoking {@link Scene.next()} won't have any effect.
|
||||
*/
|
||||
Finished,
|
||||
}
|
||||
21
src/scenes/Threadable.ts
Normal file
21
src/scenes/Threadable.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {SubscribableValueEvent} from '../events';
|
||||
import {Thread} from '../threading';
|
||||
|
||||
/**
|
||||
* Scenes can implement this interface to display their thread hierarchy in the
|
||||
* UI.
|
||||
*
|
||||
* This interface is only useful when a scene uses thread generators to run.
|
||||
*/
|
||||
export interface Threadable {
|
||||
/**
|
||||
* Triggered when the main thread changes.
|
||||
*
|
||||
* @event Thread
|
||||
*/
|
||||
get onThreadChanged(): SubscribableValueEvent<Thread>;
|
||||
}
|
||||
|
||||
export function isThreadable(value: any): value is Threadable {
|
||||
return value && typeof value === 'object' && 'onThreadChanged' in value;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {Scene} from './Scene';
|
||||
import {ValueDispatcher} from './events';
|
||||
import type {Scene, SceneMetadata} from './Scene';
|
||||
import {ValueDispatcher} from '../events';
|
||||
|
||||
/**
|
||||
* Represents a time event at runtime.
|
||||
@@ -52,12 +52,12 @@ export class TimeEvents {
|
||||
|
||||
private registeredEvents: Record<string, TimeEvent> = {};
|
||||
private lookup: Record<string, TimeEvent> = {};
|
||||
private previousReference: SavedTimeEvent[];
|
||||
private ignoreSave = false;
|
||||
private previousReference: SavedTimeEvent[] = [];
|
||||
private didEventsChange = false;
|
||||
private preserveTiming = true;
|
||||
|
||||
public constructor(private readonly scene: Scene) {
|
||||
const storageKey = `scene-${scene.project.name()}-${scene.name()}`;
|
||||
const storageKey = `scene-${scene.project.name}-${scene.name}`;
|
||||
const storedEvents = localStorage.getItem(storageKey);
|
||||
if (storedEvents) {
|
||||
console.warn('Migrating localStorage to meta files');
|
||||
@@ -65,21 +65,13 @@ export class TimeEvents {
|
||||
localStorage.removeItem(storageKey);
|
||||
this.load(Object.values<TimeEvent>(JSON.parse(storedEvents)));
|
||||
} else {
|
||||
this.load(scene.meta.getData().timeEvents ?? []);
|
||||
this.previousReference = scene.meta.getData().timeEvents ?? [];
|
||||
this.load(this.previousReference);
|
||||
}
|
||||
|
||||
scene.meta.onDataChanged.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.ignoreSave = true;
|
||||
|
||||
this.load(event.timeEvents ?? []);
|
||||
scene.reload();
|
||||
window.player.reload();
|
||||
}, false);
|
||||
scene.onReloaded.subscribe(this.handleReload);
|
||||
scene.onRecalculated.subscribe(this.handleRecalculated);
|
||||
scene.meta.onDataChanged.subscribe(this.handleMetaChanged, false);
|
||||
}
|
||||
|
||||
public get(name: string) {
|
||||
@@ -107,6 +99,7 @@ export class TimeEvents {
|
||||
};
|
||||
this.registeredEvents[name] = this.lookup[name];
|
||||
this.events.current = Object.values(this.registeredEvents);
|
||||
this.didEventsChange = true;
|
||||
this.scene.reload();
|
||||
}
|
||||
|
||||
@@ -124,6 +117,7 @@ export class TimeEvents {
|
||||
this.scene.project.frame - this.scene.firstFrame,
|
||||
);
|
||||
if (!this.lookup[name]) {
|
||||
this.didEventsChange = true;
|
||||
this.lookup[name] = {
|
||||
name,
|
||||
initialTime,
|
||||
@@ -146,6 +140,7 @@ export class TimeEvents {
|
||||
|
||||
const target = event.initialTime + event.offset;
|
||||
if (!this.preserveTiming && event.targetTime !== target) {
|
||||
this.didEventsChange = true;
|
||||
event.targetTime = target;
|
||||
changed = true;
|
||||
}
|
||||
@@ -166,37 +161,47 @@ export class TimeEvents {
|
||||
/**
|
||||
* Called when the parent scene gets reloaded.
|
||||
*/
|
||||
public onReload() {
|
||||
private handleReload = () => {
|
||||
this.registeredEvents = {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when the parent scene gets cached.
|
||||
* Called when the parent scene gets recalculated.
|
||||
*/
|
||||
public onCache() {
|
||||
private handleRecalculated = () => {
|
||||
this.preserveTiming = true;
|
||||
this.events.current = Object.values(this.registeredEvents);
|
||||
|
||||
if (this.ignoreSave) {
|
||||
this.ignoreSave = false;
|
||||
return;
|
||||
if (
|
||||
this.didEventsChange ||
|
||||
this.previousReference.length !== this.events.current.length
|
||||
) {
|
||||
this.didEventsChange = false;
|
||||
this.previousReference = Object.values(this.registeredEvents).map(
|
||||
event => ({
|
||||
name: event.name,
|
||||
targetTime: event.targetTime,
|
||||
}),
|
||||
);
|
||||
this.scene.meta.setDataSync({
|
||||
timeEvents: this.previousReference,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.save();
|
||||
}
|
||||
|
||||
private save() {
|
||||
this.previousReference = Object.values(this.registeredEvents).map(
|
||||
event => ({
|
||||
name: event.name,
|
||||
targetTime: event.targetTime,
|
||||
}),
|
||||
);
|
||||
|
||||
this.scene.meta.setDataSync({
|
||||
timeEvents: this.previousReference,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Called when the meta of the parent scene changes.
|
||||
*/
|
||||
private handleMetaChanged = (data: SceneMetadata) => {
|
||||
// Ignore the event if `timeEvents` hasn't changed.
|
||||
// This may happen when another part of metadata has changed triggering
|
||||
// this event.
|
||||
if (data.timeEvents === this.previousReference) return;
|
||||
this.previousReference = data.timeEvents;
|
||||
this.load(data.timeEvents ?? []);
|
||||
this.scene.reload();
|
||||
window.player.reload();
|
||||
};
|
||||
|
||||
private load(events: SavedTimeEvent[]) {
|
||||
for (const event of events) {
|
||||
78
src/scenes/Transitionable.ts
Normal file
78
src/scenes/Transitionable.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import {ThreadGenerator} from '../threading';
|
||||
import {Vector2} from '../types';
|
||||
|
||||
/**
|
||||
* Describes the transition context of a scene.
|
||||
*
|
||||
* This context is used by scene transitions to modify the way scenes are
|
||||
* displayed in a renderer-agnostic way.
|
||||
*
|
||||
* It allows transitions to work across different scenes.
|
||||
*/
|
||||
export interface TransitionContext {
|
||||
/**
|
||||
* Set the position of the scene.
|
||||
* @param value
|
||||
*/
|
||||
position(value: Vector2): void;
|
||||
|
||||
/**
|
||||
* Set the scale of the scene.
|
||||
* @param value
|
||||
*/
|
||||
scale(value: Vector2): void;
|
||||
|
||||
/**
|
||||
* Set the rotation of the scene.
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
rotation(value: number): void;
|
||||
|
||||
/**
|
||||
* Set the opacity of the scene.
|
||||
* @param value
|
||||
*/
|
||||
opacity(value: number): void;
|
||||
|
||||
/**
|
||||
* Set whether the scene should be visible.
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
visible(value: boolean): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a function that performs a transition.
|
||||
*/
|
||||
export interface SceneTransition {
|
||||
/**
|
||||
* A function that performs a transition.
|
||||
*
|
||||
* @param next the context of the scene that should appear next.
|
||||
* @param previous If present, the context of the scene that should disappear.
|
||||
*/
|
||||
(next: TransitionContext, previous?: TransitionContext): ThreadGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenes can implement this interface to support transitions.
|
||||
*/
|
||||
export interface Transitionable {
|
||||
/**
|
||||
* Get the transition context for this scene.
|
||||
*/
|
||||
getTransitionContext(): TransitionContext;
|
||||
|
||||
/**
|
||||
* Perform the transition.
|
||||
*
|
||||
* @param transitionRunner A generator function that runs the transition.
|
||||
*/
|
||||
transition(transitionRunner?: SceneTransition): ThreadGenerator;
|
||||
}
|
||||
|
||||
export function isTransitionable(value: any): value is Transitionable {
|
||||
return value && typeof value === 'object' && 'getTransitionContext' in value;
|
||||
}
|
||||
8
src/scenes/index.ts
Normal file
8
src/scenes/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './GeneratorScene';
|
||||
export * from './Inspectable';
|
||||
export * from './KonvaScene';
|
||||
export * from './Scene';
|
||||
export * from './SceneState';
|
||||
export * from './Threadable';
|
||||
export * from './TimeEvents';
|
||||
export * from './Transitionable';
|
||||
@@ -1,6 +0,0 @@
|
||||
import {ThreadGenerator} from '../threading';
|
||||
import {Scene} from '../Scene';
|
||||
|
||||
export interface SceneTransition {
|
||||
(next: Scene, previous?: Scene): ThreadGenerator;
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
export * from './slideTransition';
|
||||
export * from './SceneTransition';
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import type {SceneTransition} from './SceneTransition';
|
||||
import type {SceneTransition} from '../scenes';
|
||||
import {Direction, originPosition} from '../types';
|
||||
import {easeInOutCubic, tween, vector2dTween} from '../tweening';
|
||||
import {useProject} from '../utils';
|
||||
import {useScene} from '../utils';
|
||||
|
||||
export function slideTransition(
|
||||
direction: Direction = Direction.Top,
|
||||
): SceneTransition {
|
||||
return (next, previous) => {
|
||||
const project = useProject();
|
||||
const position = originPosition(
|
||||
direction,
|
||||
project.width(),
|
||||
project.height(),
|
||||
);
|
||||
const size = useScene().getSize();
|
||||
const position = originPosition(direction, size.width, size.height);
|
||||
return tween(0.6, value => {
|
||||
previous?.position(
|
||||
vector2dTween(
|
||||
@@ -21,6 +17,7 @@ export function slideTransition(
|
||||
easeInOutCubic(value),
|
||||
),
|
||||
);
|
||||
|
||||
next.position(
|
||||
vector2dTween(position, {x: 0, y: 0}, easeInOutCubic(value)),
|
||||
);
|
||||
|
||||
@@ -131,11 +131,8 @@ export class Animator<Type, This> {
|
||||
return this;
|
||||
}
|
||||
|
||||
public waitUntil(event: string): this;
|
||||
public waitUntil(time: number): this;
|
||||
public waitUntil(time: number | string): this {
|
||||
// @ts-ignore
|
||||
this.keys.push(() => waitUntil(time));
|
||||
public waitUntil(event: string): this {
|
||||
this.keys.push(() => waitUntil(event));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import mixColor from 'mix-color';
|
||||
import {IRect, Vector2d} from 'konva/lib/types';
|
||||
import {PossibleSpacing, Spacing} from '../types';
|
||||
import type {Rect, Vector2, PossibleSpacing, Spacing} from '../types';
|
||||
|
||||
export interface TweenFunction<T, Rest extends unknown[] = unknown[]> {
|
||||
(from: T, to: T, value: number, ...args: Rest): T;
|
||||
@@ -43,7 +42,7 @@ export function colorTween(from: string, to: string, value: number) {
|
||||
return mixColor(from, to, value);
|
||||
}
|
||||
|
||||
export function vector2dTween(from: Vector2d, to: Vector2d, value: number) {
|
||||
export function vector2dTween(from: Vector2, to: Vector2, value: number) {
|
||||
return {
|
||||
x: map(from.x, to.x, value),
|
||||
y: map(from.y, to.y, value),
|
||||
@@ -64,8 +63,8 @@ export function spacingTween(
|
||||
}
|
||||
|
||||
export function rectArcTween(
|
||||
from: Partial<IRect>,
|
||||
to: Partial<IRect>,
|
||||
from: Partial<Rect>,
|
||||
to: Partial<Rect>,
|
||||
value: number,
|
||||
reverse?: boolean,
|
||||
ratio?: number,
|
||||
@@ -96,10 +95,7 @@ export function rectArcTween(
|
||||
};
|
||||
}
|
||||
|
||||
export function calculateRatio(
|
||||
from: Partial<IRect>,
|
||||
to: Partial<IRect>,
|
||||
): number {
|
||||
export function calculateRatio(from: Partial<Rect>, to: Partial<Rect>): number {
|
||||
let numberOfValues = 0;
|
||||
let ratio = 0;
|
||||
if (from.x) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Vector2d} from 'konva/lib/types';
|
||||
import {Size} from './Size';
|
||||
import type {Vector2} from './Vector';
|
||||
import type {Size} from './Size';
|
||||
|
||||
export enum Center {
|
||||
Vertical = 1,
|
||||
@@ -54,8 +54,8 @@ export function originPosition(
|
||||
origin: Origin | Direction,
|
||||
width = 1,
|
||||
height = 1,
|
||||
): Vector2d {
|
||||
const position: Vector2d = {x: 0, y: 0};
|
||||
): Vector2 {
|
||||
const position: Vector2 = {x: 0, y: 0};
|
||||
|
||||
if (origin === Origin.Middle) {
|
||||
return position;
|
||||
@@ -76,7 +76,7 @@ export function originPosition(
|
||||
return position;
|
||||
}
|
||||
|
||||
export function getOriginOffset(size: Size, origin: Origin): Vector2d {
|
||||
export function getOriginOffset(size: Size, origin: Origin): Vector2 {
|
||||
return originPosition(origin, size.width / 2, size.height / 2);
|
||||
}
|
||||
|
||||
|
||||
6
src/types/Rect.ts
Normal file
6
src/types/Rect.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface Rect {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import {Size} from './Size';
|
||||
import {IRect, Vector2d} from 'konva/lib/types';
|
||||
import type {Size} from './Size';
|
||||
import type {Rect} from './Rect';
|
||||
import type {Vector2} from './Vector';
|
||||
|
||||
interface ISpacing {
|
||||
top: number;
|
||||
@@ -67,7 +68,7 @@ export class Spacing implements ISpacing {
|
||||
return this;
|
||||
}
|
||||
|
||||
public expand<T extends Size | IRect>(value: T): T {
|
||||
public expand<T extends Size | Rect>(value: T): T {
|
||||
const result = {...value};
|
||||
|
||||
result.width += this.x;
|
||||
@@ -80,7 +81,7 @@ export class Spacing implements ISpacing {
|
||||
return result;
|
||||
}
|
||||
|
||||
public shrink<T extends Size | IRect>(value: T): T {
|
||||
public shrink<T extends Size | Rect>(value: T): T {
|
||||
const result = {...value};
|
||||
|
||||
result.width -= this.x;
|
||||
@@ -93,7 +94,7 @@ export class Spacing implements ISpacing {
|
||||
return result;
|
||||
}
|
||||
|
||||
public scale(scale: Vector2d): Spacing {
|
||||
public scale(scale: Vector2): Spacing {
|
||||
return new Spacing([
|
||||
this.top * scale.y,
|
||||
this.right * scale.x,
|
||||
|
||||
10
src/types/Vector.ts
Normal file
10
src/types/Vector.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface Vector2 {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Vector3 {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './Origin';
|
||||
export * from './Rect';
|
||||
export * from './Size';
|
||||
export * from './Spacing';
|
||||
export * from './Vector';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './pop';
|
||||
export * from './slide';
|
||||
export * from './useAnimator';
|
||||
export * from './useProject';
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import {Node} from 'konva/lib/Node';
|
||||
import {useScene} from './useScene';
|
||||
|
||||
export function pop<T extends Node>(node: T): [T, () => void] {
|
||||
const clone: T = node.clone();
|
||||
clone.moveTo(useScene());
|
||||
clone.position(node.absolutePosition());
|
||||
node.hide();
|
||||
|
||||
return [
|
||||
clone,
|
||||
() => {
|
||||
clone.destroy();
|
||||
node.show();
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import {Container} from 'konva/lib/Container';
|
||||
import {Vector2d} from 'konva/lib/types';
|
||||
import type {Container} from 'konva/lib/Container';
|
||||
import type {Vector2} from '../types';
|
||||
|
||||
export function slide(container: Container, offset: Vector2d): void;
|
||||
export function slide(container: Container, offset: Vector2): void;
|
||||
export function slide(container: Container, x: number, y?: number): void;
|
||||
export function slide(
|
||||
container: Container,
|
||||
offset: number | Vector2d,
|
||||
offset: number | Vector2,
|
||||
y = 0,
|
||||
): void {
|
||||
if (typeof offset === 'number') {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Scene} from '../Scene';
|
||||
import type {Scene} from '../scenes';
|
||||
|
||||
let currentScene: Scene = null;
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"src/helpers",
|
||||
"src/media",
|
||||
"src/player",
|
||||
"src/scenes",
|
||||
"src/styles",
|
||||
"src/themes",
|
||||
"src/threading",
|
||||
|
||||
Reference in New Issue
Block a user