feat: renderer ui

This commit is contained in:
aarthificial
2022-03-30 02:44:11 +02:00
parent 5a3ab9ad4d
commit 8a4e5d32b1
6 changed files with 97 additions and 76 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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