import { DISPLAY_CURRENT_MODE, DISPLAY_CURRENT_SCALE, EVENT_ZOOM, SCALE_POWER_MAX, SCALE_POWER_MIN, } from './config.js'; import {Display} from './display.js'; import {Objects} from './objects.js'; import {Overlay} from './overlay.js'; import {Pointer} from './pointer.js'; import {ModeSwitch} from './tool/modes.js'; import {Options} from './tool/options.js'; import {PlayPause} from './tool/play-pause.js'; import {Zoom} from './tool/zoom.js'; import {Toolbar} from './toolbar.js'; export class Sim { info = {}; rawTime = undefined; time = undefined; nextZoom = undefined; playing = true; display = undefined; overlay = undefined; pointer = undefined; objects = undefined; toolbar = undefined; toolbar2 = undefined; isCurrentMode = () => undefined; getCurrentMode = () => undefined; setCurrentMode = () => undefined; getOption = () => undefined; onModeEnter = () => undefined; onModeLeave = () => undefined; constructor(divId) { this.divId = divId; const div = document.getElementById(this.divId); this.div = div; this.display = new Display(this); this.objects = new Objects(this); this.toolbar = new Toolbar(this, 'Tools'); this.toolbar2 = new Toolbar(this, 'Options'); this.overlay = new Overlay(this); // Set up toolbar this.toolbar.addTool(new Zoom(this.toolbar)); this.toolbar.addTool(new PlayPause(this.toolbar)); this.toolbar.addTool(new ModeSwitch(this.toolbar)); // Set up second toolbar this.toolbar2.topRight(); this.toolbar2.addTool(new Options(this.toolbar)); this.pointer = new Pointer(this); // Initiate main loop this.rawTime = document.timeline.currentTime / 1000; this.time = 0; requestAnimationFrame(t => this.loop(t)); } // It's better not to change the scale in the middle of possible frame calculations, // so use this to schedule it and let the event loop pick it up. // velocity should be in Sim coordinate scale scheduleZoom({x, y}, factor, velocity) { this.nextZoom = {x, y, factor, velocity}; } // x, y should be in Sim coordinates // velocity should be in Sim coordinate scale zoom({x, y, factor, velocity}) { // x, y are the mouse coordinates, which should be the center of the new view frame // the new view origin should be x, y minus half the new view width and height // compute new scale this.display.scalePower += factor; // TODO: Lossy rescaling to expand zoom range if (this.display.scalePower > SCALE_POWER_MAX) this.display.scalePower = SCALE_POWER_MAX; if (this.display.scalePower < SCALE_POWER_MIN) this.display.scalePower = SCALE_POWER_MIN; // compute coordinates of new view frame this.display.viewOrigin.x = x - this.display.width / 2; this.display.viewOrigin.y = y - this.display.height / 2; this.pointer.clearPointerHistory(); if (this.playing && velocity) { this.pointer.panning = { velocity: { x: -velocity.x, y: -velocity.y, } }; } const e = new CustomEvent(EVENT_ZOOM); this.div.dispatchEvent(e); } // Transform display coordinates to simulator coordinates using scale and viewOrigin screenToSim(x, y) { return this.display.screenToSim(x, y); } play() { this.playing = true; } pause() { this.playing = false; } getScaleDisplay() { const scale = 2 ** Math.abs(this.display.scalePower); return this.display.scalePower >= 0 ? `${scale}` : `1/${scale}`; } // cb: () => undefined onZoom(cb) { this.div.addEventListener(EVENT_ZOOM, () => { cb(); }); } // Main loop loop(currentTime) { currentTime /= 1000; const timeScale = this.getOption('param.timeScale'); const elapsedTime = (currentTime - this.rawTime) * timeScale; this.rawTime = currentTime; this.time += elapsedTime; if (DISPLAY_CURRENT_MODE) { this.info['Mode'] = this.getCurrentMode(); } if (this.nextZoom) { this.zoom(this.nextZoom); this.nextZoom = undefined; } if (DISPLAY_CURRENT_SCALE) { this.info['Scale'] = this.getScaleDisplay(); } this.pointer.computeFrame(elapsedTime); this.objects.computeFrame(elapsedTime); this.overlay.renderInfo(); this.display.fillCanvas(); this.display.drawObjects(); this.toolbar.frame(); requestAnimationFrame(t => this.loop(t)); } }