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 id="loading">Loading...</div>
<input type="button" id="controls-render" name="render" value="Render" />
</form>
<script src="index.js"></script>
</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 {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: '',
};

View File

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

View File

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

View File

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