gravity/simulator.js
2025-12-31 18:41:50 -06:00

134 lines
3.2 KiB
JavaScript

import {
EVENT_ZOOM,
FRAMERATE_SAMPLE_DURATION,
} from './config.js';
import { Display } from './display.js';
import { System } from './system.js';
import { Options } from './options.js';
import { Zoom } from './zoom.js';
import { simOptions } from './sim-options.js';
import { initializeTools } from './sim-tools.js';
export class Sim {
info = {};
rawTime = undefined;
time = undefined;
timeScale = undefined;
nextZoom = undefined;
playing = true;
recentFrames = [];
frameRate = 0;
panning = undefined;
system = undefined;
display = undefined;
overlay = undefined;
pointer = undefined;
zoom = 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.zoom = new Zoom(this);
initializeTools(this);
// Initiate main loop
this.rawTime = document.timeline.currentTime;
this.time = 0;
requestAnimationFrame(t => this.loop(t));
}
markFrame(t) {
const { recentFrames: rfs } = this;
rfs.push(t);
if (rfs.length < 2) return;
const oldest = rfs[0];
const newest = rfs[rfs.length - 1];
const count = rfs.length;
const duration = (newest - oldest);
this.frameRate = 1000 * count / duration; // Converting from ms to s
if (duration >= FRAMERATE_SAMPLE_DURATION) {
rfs.shift();
}
}
// 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;
}
pause() {
this.playing = false;
}
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();
});
}
// Main loop
loop(currentTime) {
this.markFrame(currentTime);
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();
this.display.frame(elapsedTime);
this.system.frame(elapsedTime);
this.overlay.frame();
for (const group in this.toolbarGroups) {
this.toolbarGroups[group].frame();
}
requestAnimationFrame(t => this.loop(t));
}
}