mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-11 06:48:12 -05:00
feat: renderer ui
This commit is contained in:
@@ -108,6 +108,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div id="loading">Loading...</div>
|
||||
<input type="button" id="controls-render" name="render" value="Render" />
|
||||
</form>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import type {Project} from './Project';
|
||||
|
||||
export const Renderer = (factory: () => Project) => {
|
||||
document.addEventListener('click', () =>
|
||||
render(factory()).catch(console.error),
|
||||
);
|
||||
};
|
||||
|
||||
async function render(project: Project) {
|
||||
let totalSize = 0;
|
||||
const startTime = Date.now();
|
||||
|
||||
project.start();
|
||||
const directory = await window.showDirectoryPicker();
|
||||
|
||||
while (!(await project.next())) {
|
||||
project.draw();
|
||||
const name = project.frame.toString().padStart(6, '0');
|
||||
const content = await new Promise<Blob>(resolve => project.toCanvas().toBlob(resolve, 'image/png'));
|
||||
const size = (content.size) / 1024;
|
||||
totalSize += size;
|
||||
|
||||
const file = await directory.getFileHandle(`frame-${name}.png`, {
|
||||
create: true,
|
||||
});
|
||||
const stream = await file.createWritable();
|
||||
await stream.write(content);
|
||||
await stream.close();
|
||||
|
||||
console.log(
|
||||
`Frame: ${name}, Size: ${Math.round(size)} kB, Total: ${Math.round(
|
||||
totalSize,
|
||||
)} kB, Elapsed: ${Math.round((Date.now() - startTime) / 1000)}`,
|
||||
);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,9 @@ import {LayoutShape, LayoutShapeConfig} from './LayoutShape';
|
||||
import {cancel, TimeTween, waitFor} from '../animations';
|
||||
import {AnimatedGetSet, getset, KonvaNode, threadable} from '../decorators';
|
||||
import {GeneratorHelper} from '../helpers';
|
||||
import {ImageData} from 'canvas';
|
||||
|
||||
export interface SpriteData {
|
||||
fileName: string;
|
||||
url: string;
|
||||
data: number[];
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -46,7 +44,6 @@ export class Sprite extends LayoutShape {
|
||||
private frame: SpriteData = {
|
||||
height: 0,
|
||||
width: 0,
|
||||
url: '',
|
||||
data: [],
|
||||
fileName: '',
|
||||
};
|
||||
|
||||
@@ -8,23 +8,16 @@ export class Controls {
|
||||
private loadingIndicator: HTMLElement;
|
||||
private stepRequested: boolean = false;
|
||||
private resetRequested: boolean = false;
|
||||
private rendering: boolean = false;
|
||||
private lastUpdate: number = 0;
|
||||
private updateTimes: number[] = [];
|
||||
private overallTime: number = 0;
|
||||
private directory: FileSystemDirectoryHandle;
|
||||
|
||||
public set loading(value: boolean) {
|
||||
this.loadingIndicator.hidden = !value;
|
||||
}
|
||||
|
||||
public get isPlaying(): boolean {
|
||||
if (this.stepRequested) {
|
||||
this.stepRequested = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.play.checked;
|
||||
}
|
||||
|
||||
public get isLooping(): boolean {
|
||||
return this.loop.checked;
|
||||
}
|
||||
@@ -33,19 +26,14 @@ export class Controls {
|
||||
return parseInt(this.from.value);
|
||||
}
|
||||
|
||||
public get shouldReset(): boolean {
|
||||
if (this.resetRequested) {
|
||||
this.resetRequested = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public get playbackSpeed(): number {
|
||||
return parseFloat(this.speed.value);
|
||||
}
|
||||
|
||||
public get isRendering(): boolean {
|
||||
return this.rendering;
|
||||
}
|
||||
|
||||
public constructor(private form: HTMLFormElement) {
|
||||
this.play = form.play;
|
||||
this.loop = form.loop;
|
||||
@@ -57,22 +45,43 @@ export class Controls {
|
||||
|
||||
form.next.addEventListener('click', this.handleNext);
|
||||
form.refresh.addEventListener('click', this.handleReset);
|
||||
form.render.addEventListener('click', () => this.toggleRendering());
|
||||
this.current.addEventListener(
|
||||
'click',
|
||||
() => (this.from.value = this.current.value),
|
||||
);
|
||||
|
||||
this.play.checked = localStorage.getItem('play') === 'true';
|
||||
this.play.addEventListener('change', () => this.toggle(this.play.checked));
|
||||
this.play.addEventListener('change', () =>
|
||||
this.togglePlayback(this.play.checked),
|
||||
);
|
||||
|
||||
document.addEventListener('keydown', event => {
|
||||
switch (event.key) {
|
||||
case ' ':
|
||||
this.toggle();
|
||||
event.preventDefault();
|
||||
this.togglePlayback();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
event.preventDefault();
|
||||
this.handleNext();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public consumeState() {
|
||||
const state = {
|
||||
isPlaying: this.play.checked || this.stepRequested,
|
||||
shouldReset: this.resetRequested,
|
||||
};
|
||||
|
||||
this.stepRequested = false;
|
||||
this.resetRequested = false;
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
public onReset() {
|
||||
this.overallTime = 0;
|
||||
this.updateTimes = [];
|
||||
@@ -94,16 +103,40 @@ export class Controls {
|
||||
this.lastUpdate = performance.now();
|
||||
}
|
||||
|
||||
public async onRender(frame: number, content: Blob) {
|
||||
const name = frame.toString().padStart(6, '0');
|
||||
const size = content.size / 1024;
|
||||
|
||||
try {
|
||||
this.directory ??= await window.showDirectoryPicker();
|
||||
const file = await this.directory.getFileHandle(`frame-${name}.png`, {
|
||||
create: true,
|
||||
});
|
||||
const stream = await file.createWritable();
|
||||
await stream.write(content);
|
||||
await stream.close();
|
||||
console.log(`Frame: ${name}, Size: ${Math.round(size)} kB`);
|
||||
this.onFrame(frame);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
await this.toggleRendering(false);
|
||||
}
|
||||
}
|
||||
|
||||
private handleReset = () => {
|
||||
this.resetRequested = true;
|
||||
}
|
||||
};
|
||||
|
||||
private handleNext = () => {
|
||||
this.stepRequested = true;
|
||||
}
|
||||
};
|
||||
|
||||
private toggle = (value?: boolean) => {
|
||||
private togglePlayback = (value?: boolean) => {
|
||||
this.play.checked = value ?? !this.play.checked;
|
||||
localStorage.setItem('play', this.play.checked ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public toggleRendering = async (value?: boolean) => {
|
||||
this.rendering = value ?? !this.rendering;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Project} from '../Project';
|
||||
import {Controls} from './Controls';
|
||||
import {ThreadsMonitor} from "./ThreadsMonitor";
|
||||
import {ThreadsMonitor} from './ThreadsMonitor';
|
||||
|
||||
const MINIMUM_ANIMATION_DURATION = 1000;
|
||||
const MAX_AUDIO_DESYNC = 1 / 50;
|
||||
@@ -34,26 +34,45 @@ export class Player {
|
||||
}
|
||||
|
||||
private async reset() {
|
||||
this.startTime = performance.now();
|
||||
this.project.start();
|
||||
await this.project.next();
|
||||
this.finished = await this.project.next();
|
||||
while (this.project.frame < this.controls.startFrom && !this.finished) {
|
||||
this.finished = await this.project.next();
|
||||
}
|
||||
this.project.draw();
|
||||
this.controls.onReset();
|
||||
this.controls.onFrame(this.project.frame);
|
||||
this.finished = false;
|
||||
this.startTime = performance.now();
|
||||
if (this.audio) {
|
||||
this.audio.currentTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private async run() {
|
||||
if (this.controls.shouldReset) {
|
||||
const {isPlaying, shouldReset} = this.controls.consumeState();
|
||||
|
||||
if (shouldReset) {
|
||||
await this.reset();
|
||||
}
|
||||
|
||||
if (this.controls.isRendering) {
|
||||
if (!this.audio?.paused) {
|
||||
this.audio?.pause();
|
||||
}
|
||||
this.finished = await this.project.next();
|
||||
this.project.draw();
|
||||
await this.controls.onRender(this.project.frame, await this.getContent());
|
||||
if (this.finished) {
|
||||
await this.controls.toggleRendering(false);
|
||||
}
|
||||
|
||||
this.request();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.controls.playbackSpeed !== 1 && this.audio) {
|
||||
this.audio.currentTime = this.project.time;
|
||||
} else if (this.controls.isPlaying) {
|
||||
} else if (isPlaying) {
|
||||
if (this.audio?.paused) {
|
||||
await this.audio?.play();
|
||||
}
|
||||
@@ -64,9 +83,10 @@ export class Player {
|
||||
}
|
||||
|
||||
if (
|
||||
!this.controls.isPlaying ||
|
||||
!isPlaying ||
|
||||
(this.controls.playbackSpeed === 1 &&
|
||||
this.audio?.currentTime < this.project.time)
|
||||
this.audio &&
|
||||
this.audio.currentTime < this.project.time)
|
||||
) {
|
||||
this.request();
|
||||
return;
|
||||
@@ -77,7 +97,10 @@ export class Player {
|
||||
// Prevent animation from restarting too quickly.
|
||||
const animationDuration = performance.now() - this.startTime;
|
||||
if (animationDuration < MINIMUM_ANIMATION_DURATION) {
|
||||
setTimeout(this.run, MINIMUM_ANIMATION_DURATION - animationDuration);
|
||||
setTimeout(
|
||||
() => this.run(),
|
||||
MINIMUM_ANIMATION_DURATION - animationDuration,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await this.reset();
|
||||
@@ -120,4 +143,10 @@ export class Player {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getContent(): Promise<Blob> {
|
||||
return new Promise<Blob>(resolve =>
|
||||
this.project.toCanvas().toBlob(resolve, 'image/png'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ module.exports = async function (fileName) {
|
||||
return {
|
||||
fileName,
|
||||
data: Array.from(imageData.data),
|
||||
url: `data:image/png;base64,${data}`,
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user