mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-04-22 03:00:03 -04:00
feat: renderer ui
This commit is contained in:
@@ -108,6 +108,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div id="loading">Loading...</div>
|
<div id="loading">Loading...</div>
|
||||||
|
<input type="button" id="controls-render" name="render" value="Render" />
|
||||||
</form>
|
</form>
|
||||||
<script src="index.js"></script>
|
<script src="index.js"></script>
|
||||||
</body>
|
</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 {cancel, TimeTween, waitFor} from '../animations';
|
||||||
import {AnimatedGetSet, getset, KonvaNode, threadable} from '../decorators';
|
import {AnimatedGetSet, getset, KonvaNode, threadable} from '../decorators';
|
||||||
import {GeneratorHelper} from '../helpers';
|
import {GeneratorHelper} from '../helpers';
|
||||||
import {ImageData} from 'canvas';
|
|
||||||
|
|
||||||
export interface SpriteData {
|
export interface SpriteData {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
url: string;
|
|
||||||
data: number[];
|
data: number[];
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
@@ -46,7 +44,6 @@ export class Sprite extends LayoutShape {
|
|||||||
private frame: SpriteData = {
|
private frame: SpriteData = {
|
||||||
height: 0,
|
height: 0,
|
||||||
width: 0,
|
width: 0,
|
||||||
url: '',
|
|
||||||
data: [],
|
data: [],
|
||||||
fileName: '',
|
fileName: '',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,23 +8,16 @@ export class Controls {
|
|||||||
private loadingIndicator: HTMLElement;
|
private loadingIndicator: HTMLElement;
|
||||||
private stepRequested: boolean = false;
|
private stepRequested: boolean = false;
|
||||||
private resetRequested: boolean = false;
|
private resetRequested: boolean = false;
|
||||||
|
private rendering: boolean = false;
|
||||||
private lastUpdate: number = 0;
|
private lastUpdate: number = 0;
|
||||||
private updateTimes: number[] = [];
|
private updateTimes: number[] = [];
|
||||||
private overallTime: number = 0;
|
private overallTime: number = 0;
|
||||||
|
private directory: FileSystemDirectoryHandle;
|
||||||
|
|
||||||
public set loading(value: boolean) {
|
public set loading(value: boolean) {
|
||||||
this.loadingIndicator.hidden = !value;
|
this.loadingIndicator.hidden = !value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isPlaying(): boolean {
|
|
||||||
if (this.stepRequested) {
|
|
||||||
this.stepRequested = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.play.checked;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get isLooping(): boolean {
|
public get isLooping(): boolean {
|
||||||
return this.loop.checked;
|
return this.loop.checked;
|
||||||
}
|
}
|
||||||
@@ -33,19 +26,14 @@ export class Controls {
|
|||||||
return parseInt(this.from.value);
|
return parseInt(this.from.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get shouldReset(): boolean {
|
|
||||||
if (this.resetRequested) {
|
|
||||||
this.resetRequested = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get playbackSpeed(): number {
|
public get playbackSpeed(): number {
|
||||||
return parseFloat(this.speed.value);
|
return parseFloat(this.speed.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get isRendering(): boolean {
|
||||||
|
return this.rendering;
|
||||||
|
}
|
||||||
|
|
||||||
public constructor(private form: HTMLFormElement) {
|
public constructor(private form: HTMLFormElement) {
|
||||||
this.play = form.play;
|
this.play = form.play;
|
||||||
this.loop = form.loop;
|
this.loop = form.loop;
|
||||||
@@ -57,22 +45,43 @@ export class Controls {
|
|||||||
|
|
||||||
form.next.addEventListener('click', this.handleNext);
|
form.next.addEventListener('click', this.handleNext);
|
||||||
form.refresh.addEventListener('click', this.handleReset);
|
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.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 => {
|
document.addEventListener('keydown', event => {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case ' ':
|
case ' ':
|
||||||
this.toggle();
|
event.preventDefault();
|
||||||
|
this.togglePlayback();
|
||||||
break;
|
break;
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
|
event.preventDefault();
|
||||||
this.handleNext();
|
this.handleNext();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public consumeState() {
|
||||||
|
const state = {
|
||||||
|
isPlaying: this.play.checked || this.stepRequested,
|
||||||
|
shouldReset: this.resetRequested,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.stepRequested = false;
|
||||||
|
this.resetRequested = false;
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
public onReset() {
|
public onReset() {
|
||||||
this.overallTime = 0;
|
this.overallTime = 0;
|
||||||
this.updateTimes = [];
|
this.updateTimes = [];
|
||||||
@@ -94,16 +103,40 @@ export class Controls {
|
|||||||
this.lastUpdate = performance.now();
|
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 = () => {
|
private handleReset = () => {
|
||||||
this.resetRequested = true;
|
this.resetRequested = true;
|
||||||
}
|
};
|
||||||
|
|
||||||
private handleNext = () => {
|
private handleNext = () => {
|
||||||
this.stepRequested = true;
|
this.stepRequested = true;
|
||||||
}
|
};
|
||||||
|
|
||||||
private toggle = (value?: boolean) => {
|
private togglePlayback = (value?: boolean) => {
|
||||||
this.play.checked = value ?? !this.play.checked;
|
this.play.checked = value ?? !this.play.checked;
|
||||||
localStorage.setItem('play', this.play.checked ? 'true' : 'false');
|
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 {Project} from '../Project';
|
||||||
import {Controls} from './Controls';
|
import {Controls} from './Controls';
|
||||||
import {ThreadsMonitor} from "./ThreadsMonitor";
|
import {ThreadsMonitor} from './ThreadsMonitor';
|
||||||
|
|
||||||
const MINIMUM_ANIMATION_DURATION = 1000;
|
const MINIMUM_ANIMATION_DURATION = 1000;
|
||||||
const MAX_AUDIO_DESYNC = 1 / 50;
|
const MAX_AUDIO_DESYNC = 1 / 50;
|
||||||
@@ -34,26 +34,45 @@ export class Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async reset() {
|
private async reset() {
|
||||||
this.startTime = performance.now();
|
|
||||||
this.project.start();
|
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.project.draw();
|
||||||
this.controls.onReset();
|
this.controls.onReset();
|
||||||
this.controls.onFrame(this.project.frame);
|
this.controls.onFrame(this.project.frame);
|
||||||
this.finished = false;
|
this.startTime = performance.now();
|
||||||
if (this.audio) {
|
if (this.audio) {
|
||||||
this.audio.currentTime = 0;
|
this.audio.currentTime = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async run() {
|
private async run() {
|
||||||
if (this.controls.shouldReset) {
|
const {isPlaying, shouldReset} = this.controls.consumeState();
|
||||||
|
|
||||||
|
if (shouldReset) {
|
||||||
await this.reset();
|
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) {
|
if (this.controls.playbackSpeed !== 1 && this.audio) {
|
||||||
this.audio.currentTime = this.project.time;
|
this.audio.currentTime = this.project.time;
|
||||||
} else if (this.controls.isPlaying) {
|
} else if (isPlaying) {
|
||||||
if (this.audio?.paused) {
|
if (this.audio?.paused) {
|
||||||
await this.audio?.play();
|
await this.audio?.play();
|
||||||
}
|
}
|
||||||
@@ -64,9 +83,10 @@ export class Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this.controls.isPlaying ||
|
!isPlaying ||
|
||||||
(this.controls.playbackSpeed === 1 &&
|
(this.controls.playbackSpeed === 1 &&
|
||||||
this.audio?.currentTime < this.project.time)
|
this.audio &&
|
||||||
|
this.audio.currentTime < this.project.time)
|
||||||
) {
|
) {
|
||||||
this.request();
|
this.request();
|
||||||
return;
|
return;
|
||||||
@@ -77,7 +97,10 @@ export class Player {
|
|||||||
// Prevent animation from restarting too quickly.
|
// Prevent animation from restarting too quickly.
|
||||||
const animationDuration = performance.now() - this.startTime;
|
const animationDuration = performance.now() - this.startTime;
|
||||||
if (animationDuration < MINIMUM_ANIMATION_DURATION) {
|
if (animationDuration < MINIMUM_ANIMATION_DURATION) {
|
||||||
setTimeout(this.run, MINIMUM_ANIMATION_DURATION - animationDuration);
|
setTimeout(
|
||||||
|
() => this.run(),
|
||||||
|
MINIMUM_ANIMATION_DURATION - animationDuration,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.reset();
|
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 {
|
return {
|
||||||
fileName,
|
fileName,
|
||||||
data: Array.from(imageData.data),
|
data: Array.from(imageData.data),
|
||||||
url: `data:image/png;base64,${data}`,
|
|
||||||
width: dimensions.width,
|
width: dimensions.width,
|
||||||
height: dimensions.height,
|
height: dimensions.height,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user