import Render2D, { Render2DConstructor } from './Render2D' export type KeydownCallbackMap = { [key: string]: { callback: (e: KeyboardEvent) => void, options: { preventDefault?: boolean } } } export interface Animate2DConstructor extends Render2DConstructor { frameRenderCallback: (tick: number, renderer: Render2D) => void keydownCallbackMap?: KeydownCallbackMap } export default class Animate2D extends Render2D { private frameRenderCallback: (tick: number, renderer: Render2D) => void private runState: 'play'|'pause' = 'play' public tick: number = 0 private defaultKeydownCallbackMap: KeydownCallbackMap = { // space bar pauses animation 'Space': { callback: () => { if (this.runState === 'play') { // emit a pause event const event = new CustomEvent('pause') window.dispatchEvent(event) } else { // emit a play event const event = new CustomEvent('play') window.dispatchEvent(event) } }, options: {preventDefault: true} }, // return key steps animation forward 'Enter': { callback: () => { // emit a step event const event = new CustomEvent('step') window.dispatchEvent(event) }, options: {preventDefault: true} } } constructor(opts: Animate2DConstructor) { const { frameRenderCallback, autoPlay = true, frameRate = 60, title = 'Canvas', id = 'canvas_0', } = opts super({ ...opts, frameRate, autoPlay, title, id }) this.frameRenderCallback = frameRenderCallback this.frameRate = frameRate window.addEventListener('render', () => { this.frameRenderCallback(this.tick, this) }) window.addEventListener('stop', () => { this.pause() }) window.addEventListener('pause', () => { this.pause() }) window.addEventListener('play', () => { this.play() this.step() }) window.addEventListener('step', () => { if (this.runState === 'play') { return } this.step() }) window.addEventListener('fps', (e: Event) => { const customEvent = e as CustomEvent this.frameRate = customEvent.detail }) // create a keydown event handler window.addEventListener('keydown', (e: KeyboardEvent) => { const mergedMaps = {...this.defaultKeydownCallbackMap, ...opts.keydownCallbackMap} const codeAction = mergedMaps?.[e.code] if (codeAction) { const {callback, options} = codeAction callback(e) if (options.preventDefault) { e.preventDefault() } } }) if (autoPlay) { this.runState = 'play' this.play() } else { this.runState = 'pause' this.pause() } this.step() } public step() { this.frameRenderCallback(this.tick, this) this.tick++ if (this.runState === 'play') { setTimeout( () => requestAnimationFrame(() => this.step()), 1000 / this.frameRate ) } } public play() { this.runState = 'play' this.step() } public pause() { this.runState = 'pause' } public toggle() { if (this.runState === 'play') { this.pause() } else { this.play() } } public stepForward() { this.pause() this.step() } }