203 lines
5.8 KiB
JavaScript
203 lines
5.8 KiB
JavaScript
import {
|
|
EVENT_ZOOM,
|
|
FRAMERATE_SAMPLE_DURATION,
|
|
SCALE_POWER_MAX,
|
|
SCALE_POWER_MIN,
|
|
} from './config.js';
|
|
import { Display } from './display.js';
|
|
import { Objects } from './objects.js';
|
|
import { Options } from './options.js';
|
|
import { simOptions } from './sim-options.js';
|
|
import { initializeTools } from './sim-tools.js';
|
|
|
|
const simOptions = {
|
|
pauseDuring: {
|
|
creation: ['Pause While Creating', 'boolean', true],
|
|
selection: ['Pause While Selecting', 'boolean', true],
|
|
},
|
|
display: {
|
|
velocity: ['Velocity Vectors', 'boolean', true],
|
|
acceleration: ['Accel. Vectors', 'boolean', true],
|
|
traces: ['Path Traces', 'boolean', true],
|
|
dashedTraces: ['Dashed Traces', 'boolean', false],
|
|
},
|
|
collision: {
|
|
merge: ['Merge Masses<br>on Collision', 'boolean', true, {wide: true}],
|
|
},
|
|
param: {
|
|
gravity: ['Gravity', 'number', 4E4],
|
|
timeScale: ['Time Scale', 'number', 0.2],
|
|
massCreationRate: ['Mass Creation Rate', 'number', 10],
|
|
},
|
|
debug: {
|
|
objectsInfo: ['Objects Info', 'boolean', false],
|
|
cursorInfo: ['Cursor Info', 'boolean', false],
|
|
frameRate: ['Frame Rate', 'boolean', false, {wide: true}],
|
|
currentMode: ['Current Mode', 'boolean', false],
|
|
panningInfo: ['Panning Info', 'boolean', false],
|
|
},
|
|
};
|
|
|
|
export class Sim {
|
|
info = {};
|
|
rawTime = undefined;
|
|
time = undefined;
|
|
nextZoom = undefined;
|
|
playing = true;
|
|
recentFrames = [];
|
|
frameRate = 0;
|
|
|
|
objects = undefined;
|
|
display = undefined;
|
|
overlay = undefined;
|
|
pointer = undefined;
|
|
panning = undefined;
|
|
toolbarGroups = {};
|
|
toolbars = {};
|
|
|
|
isCurrentMode = () => undefined;
|
|
getCurrentMode = () => undefined;
|
|
setCurrentMode = () => undefined;
|
|
getOption = () => undefined;
|
|
onModeEnter = () => undefined;
|
|
onModeLeave = () => undefined;
|
|
|
|
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) / 1000; // ms to s
|
|
this.frameRate = count / duration;
|
|
if (duration >= FRAMERATE_SAMPLE_DURATION) {
|
|
rfs.shift();
|
|
}
|
|
}
|
|
|
|
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.objects = new Objects(this);
|
|
|
|
initializeTools(this);
|
|
|
|
// Initiate main loop
|
|
this.rawTime = document.timeline.currentTime;
|
|
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.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) {
|
|
this.markFrame(currentTime);
|
|
const timeScale = this.getOption('param.timeScale');
|
|
|
|
// elapsedTime in milliseconds
|
|
// rawTime in milliseconds
|
|
const elapsedTime = (currentTime - this.rawTime) / 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);
|
|
}
|
|
|
|
if (this.nextZoom) {
|
|
this.zoom(this.nextZoom);
|
|
this.nextZoom = undefined;
|
|
}
|
|
|
|
if (this.getOption('debug.panningInfo')) {
|
|
const {x, y} = this.panning?.velocity ?? {};
|
|
this.info['Panning Velocity'] = [`${x?.toPrecision(6)}, `, y?.toPrecision(6)];
|
|
const { centerOfMass } = this.objects.computeSystemCenter();
|
|
this.info['Center of Mass'] = [`${centerOfMass.x.toPrecision(6)}, `, centerOfMass.y.toPrecision(6)];
|
|
this.info['Net Angular Momentum'] = this.objects.computeSystemAngularMomentum().toPrecision(6);
|
|
}
|
|
|
|
this.objects.computeFrame(elapsedTime);
|
|
this.overlay.renderInfo();
|
|
// this.display.computePanning(elapsedTime);
|
|
this.display.fillCanvas();
|
|
this.display.drawObjects();
|
|
for (const group in this.toolbarGroups) {
|
|
this.toolbarGroups[group].frame();
|
|
}
|
|
requestAnimationFrame(t => this.loop(t));
|
|
}
|
|
}
|