import { EVENT_PLAY_PAUSE, EVENT_ZOOM, FRAMERATE_SAMPLE_DURATION, } from './config.js'; import {Display} from './display.js'; import {Options} from './options.js'; import {Overlay} from './overlay.js'; import {Panning} from './panning.js'; import {Pointer} from './pointer.js'; import {Select} from './select.js'; import {simOptions} from './sim-options.js'; import {initializeTools} from './sim-tools.js'; import {System} from './system.js'; import {Zoom} from './zoom.js'; export class Sim { info = {}; rawTime = undefined; time = undefined; timeScale = undefined; nextZoom = undefined; playing = true; recentFrames = []; frameRate = 0; system = undefined; display = undefined; overlay = undefined; pointer = undefined; zoom = undefined; panning = undefined; select = undefined; toolbarGroups = {}; toolbars = {}; isCurrentMode = () => undefined; getCurrentMode = () => undefined; setCurrentMode = () => undefined; getOption = () => undefined; setOption = () => undefined; onModeEnter = () => undefined; onModeLeave = () => undefined; constructor(divId) { this.divId = divId; const div = document.getElementById(this.divId); this.div = div; this.options = new Options(this, simOptions); this.display = new Display(this); this.system = new System(this); this.overlay = new Overlay(this); this.pointer = new Pointer(this); this.zoom = new Zoom(this); this.panning = new Panning(this); this.select = new Select(this); initializeTools(this); // Initiate main loop this.rawTime = document.timeline.currentTime; this.time = 0; requestAnimationFrame(t => this.frame(t)); } // Main loop frame(currentTime) { const early = this.markFrame(currentTime); if (early) { // Slow down :) requestAnimationFrame(t => this.frame(t)); return; } this.timeScale = this.getOption('param.timeScale'); const elapsedTime = (currentTime - this.rawTime) * this.timeScale; this.rawTime = currentTime; if (this.playing) { this.time += elapsedTime; } if (this.getOption('debug.currentMode')) { this.info['Mode'] = this.getCurrentMode(); } if (this.getOption('debug.frameRate')) { this.info['Frame Rate'] = this.frameRate.toPrecision(3); } this.zoom.frame(elapsedTime); this.panning.frame(elapsedTime); this.pointer.frame(elapsedTime); // The display.frame() wipes out the canvas, so all // main canvas drawing routines must come after the next line. this.display.frame(elapsedTime); this.select.frame(elapsedTime); // The system.frame() renders objects this.system.frame(elapsedTime); this.overlay.frame(elapsedTime); for (const group in this.toolbarGroups) { this.toolbarGroups[group].frame(elapsedTime); } // Schedule our next iteration // TODO: Consider waiting until the next frame is likely to execute, // in order to aim closer to the target frame rate requestAnimationFrame(t => this.frame(t)); } markFrame(t) { const {recentFrames: rfs} = this; if (!rfs.length) { rfs.push(t); return; } let oldest = rfs[0]; let duration = t - oldest; const count = rfs.length + 1; const frameRate = 1000 * count / duration; const targetFrameRate = this.getOption('display.targetFrameRate'); if (frameRate > targetFrameRate + 1) { return true; } this.frameRate = frameRate; rfs.push(t); while (duration >= FRAMERATE_SAMPLE_DURATION) { rfs.shift(); oldest = rfs[0]; duration = t - oldest; } return false; } toJSON() { return { dateSaved: new Date().toISOString(), system: this.system.toJSON(), panning: this.panning.toJSON(), display: this.display.toJSON(), time: this.time, timeScale: this.timeScale, currentMode: this.getCurrentMode(), }; } fromJSON(state) { this.pause(); this.system.fromJSON(state.system); this.panning.fromJSON(state.panning); this.display.fromJSON(state.display); this.time = state.time; this.timeScale = state.timeScale; this.setCurrentMode(state.currentMode); } // velocity should be in Sim coordinate scale scheduleZoom({x, y}, factor, velocity) { this.zoom.scheduleZoom({x, y}, factor, velocity); } // Transform display coordinates to simulator coordinates using scale and viewOrigin screenToSim(x, y) { return this.display.screenToSim(x, y); } play() { this.playing = true; if (this.panning.paused) { this.panning.paused = false; } const e = new CustomEvent(EVENT_PLAY_PAUSE, {detail: {playing: this.playing}}); this.div.dispatchEvent(e); } pause() { this.playing = false; if (this.panning?.velocity) { this.panning.paused = true; } const e = new CustomEvent(EVENT_PLAY_PAUSE, {detail: {playing: this.playing}}); this.div.dispatchEvent(e); } getScaleDisplay() { const scale = 2 ** Math.abs(this.display.scalePower); const scaleText = this.display.scalePower >= 0 ? `${scale}` : `1/${scale}`; return `${scaleText} (${this.display.scalePower})`; } // cb: () => undefined onZoom(cb) { this.div.addEventListener(EVENT_ZOOM, () => { cb(); }); } }