diff --git a/Readme.md b/Readme.md
index 1737dcf..b6520d3 100644
--- a/Readme.md
+++ b/Readme.md
@@ -13,7 +13,7 @@ Screenshots
TODO
----
-- [ ] Parameter Slider
+- [ ] Parameter Slider (Invisible, mouse/touch drag)
- [ ] Selection Box
- [ ] Object List
- [ ] Object Detail
diff --git a/config.js b/config.js
index 340a08e..dfdaa70 100644
--- a/config.js
+++ b/config.js
@@ -1,11 +1,3 @@
-// DISPLAY
-export const DISPLAY_OBJECTS_INFO = false;
-export const DISPLAY_CURSOR_INFO = true;
-export const DISPLAY_CANVAS_SIZE = false;
-export const DISPLAY_CURRENT_SCALE = false;
-export const DISPLAY_CURRENT_MODE = false;
-export const DISPLAY_PANNING_INFO = true;
-
// VELOCITY
export const VELOCITY_VECTOR_SCALE = 8E0;
export const VELOCITY_VECTOR_COLOR = 'rgba(150, 150, 150, 0.8)'; // optionally set to 'object color'
diff --git a/display.js b/display.js
index eff687f..c97a2fc 100644
--- a/display.js
+++ b/display.js
@@ -1,7 +1,6 @@
import {
ARROWHEAD_LENGTH,
ARROWHEAD_WIDTH,
- DISPLAY_CANVAS_SIZE,
} from './config.js';
export class Display {
@@ -60,12 +59,12 @@ export class Display {
fullscreen() {
this.canvas.width = document.documentElement.clientWidth;
this.canvas.height = document.documentElement.clientHeight;
- if (DISPLAY_CANVAS_SIZE) {
- this.sim.info['Canvas'] = `${this.canvas.width} x ${this.canvas.height}`;
+ if (this.sim.getOption('debug.canvasSize')) {
+ this.sim.info['Canvas'] = [`${this.canvas.width}`, `${this.canvas.height}`];
}
}
- fillCanvas() {
+ fillCanvas() {
const ctx = this.ctx;
ctx.fillStyle = '#000';
ctx.fillRect(this.viewOrigin.x, this.viewOrigin.y, this.width, this.height);
diff --git a/objects.js b/objects.js
index f4fed35..a1237c4 100644
--- a/objects.js
+++ b/objects.js
@@ -1,8 +1,5 @@
import { MassObject } from './object.js';
-import {
- DISPLAY_OBJECTS_INFO,
- ZOOM_TO_FIT_PADDING,
-} from './config.js';
+import { ZOOM_TO_FIT_PADDING } from './config.js';
export class Objects {
objects = [];
@@ -299,7 +296,7 @@ export class Objects {
}
// Display objects info
- if (DISPLAY_OBJECTS_INFO) {
+ if (this.sim.getOption('debug.objectsInfo')) {
this.forEachObject((obj, i) => {
const speed = Math.pow(obj.velocity.x ** 2 + obj.velocity.y ** 2, 1/2);
// Invert y so that the angle is counterclockwise from x-axis
@@ -330,17 +327,25 @@ export class Objects {
count: 0,
});
- if (!count) return;
-
- const centerOfMass = {
+ const centerOfMass = count ? {
x: totalMassLocation.x / totalMass,
y: totalMassLocation.y / totalMass,
- };
-
- return { totalMass, count, totalMassLocation, centerOfMass };
+ } : {x: 0, y: 0};
+
+ // Determine average momentum
+ const netMomentum = this.sim.objects.reduce((acc, obj) => ({
+ x: acc.x + obj.mass * obj.velocity.x,
+ y: acc.y + obj.mass * obj.velocity.y,
+ }), { x: 0, y: 0 });
+
+ return { totalMass, count, totalMassLocation, centerOfMass, netMomentum };
}
computeSystemAngularMomentum(centerOfMass) {
+ if (!centerOfMass) {
+ const sys = this.computeSystemCenter();
+ centerOfMass = sys.centerOfMass;
+ }
return this.reduce((acc, obj) => {
// Angular momentum for each object is m * s / d
// where d is the distance of the object from the global center of mass
diff --git a/options.js b/options.js
index 9de7d78..ac1598f 100644
--- a/options.js
+++ b/options.js
@@ -20,7 +20,14 @@ export const optionsLayout = {
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],
+ canvasSize: ['Canvas Size', 'boolean', false],
+ currentMode: ['Current Mode', 'boolean', false],
+ panningInfo: ['Panning Info', 'boolean', false],
+ },
};
export class Options {
diff --git a/pointer.js b/pointer.js
index 2c77de4..0252b88 100644
--- a/pointer.js
+++ b/pointer.js
@@ -1,5 +1,4 @@
import {
- DISPLAY_CURSOR_INFO,
MODE_MASS_GENERATION,
MODE_OBJECT_SELECT,
MODE_PAN_VIEW,
@@ -25,7 +24,7 @@ export class Pointer {
const el = window;
el.addEventListener('pointermove', e => {
- if (DISPLAY_CURSOR_INFO) {
+ if (this.sim.getOption('debug.cursorInfo')) {
this.sim.info['pointermove'] = [`${e.clientX.toPrecision(6)}, `, `${e.clientY.toPrecision(6)}`];
}
this.handlePointerMove({x: e.clientX, y: e.clientY});
diff --git a/simulator.js b/simulator.js
index 63230b3..56246dc 100644
--- a/simulator.js
+++ b/simulator.js
@@ -1,7 +1,4 @@
import {
- DISPLAY_CURRENT_MODE,
- DISPLAY_CURRENT_SCALE,
- DISPLAY_PANNING_INFO,
EVENT_ZOOM,
SCALE_POWER_MAX,
SCALE_POWER_MIN,
@@ -15,6 +12,7 @@ import { ModeSwitch } from './tool/modes.js';
import { OptionsTool } from './tool/options.js';
import { PlayPause } from './tool/play-pause.js';
import { Zoom } from './tool/zoom.js';
+import { UtilityTool } from './tool/utility.js';
import { Toolbar } from './toolbar.js';
import { ToolbarGroup } from './toolbar-group.js';
@@ -48,35 +46,52 @@ export class Sim {
this.options = new Options(this);
this.display = new Display(this);
this.objects = new Objects(this);
- this.toolbarGroups = {
- left: new ToolbarGroup(this),
- right: new ToolbarGroup(this).topRight(),
- };
this.toolbars = {
- tools: new Toolbar(this, 'Tools', this.toolbarGroups.left),
- modes: new Toolbar(this, 'Modes', this.toolbarGroups.left),
- options: new Toolbar(this, 'Options', this.toolbarGroups.right),
- params: new Toolbar(this, 'Parameters', this.toolbarGroups.right),
- }
+ tools: new Toolbar(this, 'Tools'),
+ modes: new Toolbar(this, 'Modes'),
+ utils: new Toolbar(this, 'Utility'),
+ options: new Toolbar(this, 'Options'),
+ params: new Toolbar(this, 'Parameters'),
+ debug: new Toolbar(this, 'Debug', { expanded: false }),
+ };
+ const { tools, modes, options, params, debug, utils } = this.toolbars;
+ this.toolbarGroups = {
+ left: new ToolbarGroup(this)
+ .addToolbar(tools)
+ .addToolbar(modes)
+ .addToolbar(utils),
+ right: new ToolbarGroup(this).topRight()
+ .addToolbar(options)
+ .addToolbar(params)
+ .addToolbar(debug),
+ };
this.overlay = new Overlay(this);
this.pointer = new Pointer(this);
- {
- // Configure toolbars
- const { tools, modes, options, params } = this.toolbars;
- // Primary Toolbar
- tools.addTool(new Zoom(tools));
- tools.addTool(new PlayPause(tools));
- // Secondary Toolbar: Mode Switches
- modes.addTool(new ModeSwitch(modes));
- // Options Toolbar
- options.addTool(new OptionsTool(options, [
- 'pauseDuring', 'display', 'collision'
- ]));
- // Parameters Toolbar
- params.addTool(new OptionsTool(params, [
- 'param'
- ]));
+ // Configure toolbars
+
+ // Primary
+ tools.addTool(new Zoom(tools));
+ tools.addTool(new PlayPause(tools));
+
+ // Secondary
+ modes.addTool(new ModeSwitch(modes));
+
+ // Utility
+ utils.addTool(new UtilityTool(utils));
+
+ // Options
+ options.addTool(new OptionsTool(options, ['pauseDuring', 'display', 'collision']));
+
+ // Parameters
+ params.addTool(new OptionsTool(params, ['param']));
+
+ // Debug
+ debug.addTool(new OptionsTool(debug, ['debug']));
+
+ for (const id in this.toolbars) {
+ const toolbar = this.toolbars[id];
+ toolbar.applyExpanded();
}
// Initiate main loop
@@ -156,7 +171,7 @@ export class Sim {
this.time += elapsedTime;
}
- if (DISPLAY_CURRENT_MODE) {
+ if (this.getOption('debug.currentMode')) {
this.info['Mode'] = this.getCurrentMode();
}
@@ -165,13 +180,12 @@ export class Sim {
this.nextZoom = undefined;
}
- if (DISPLAY_CURRENT_SCALE) {
- this.info['Scale'] = this.getScaleDisplay();
- }
-
- if (DISPLAY_PANNING_INFO) {
+ if (this.getOption('debug.panningInfo')) {
const {x, y} = this.panning?.velocity ?? {};
- this.info['Panning'] = [`${x?.toPrecision(6)}, `, y?.toPrecision(6)];
+ 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.display.computePanning(elapsedTime);
diff --git a/tool/header.js b/tool/header.js
index 11610a2..bebfc60 100644
--- a/tool/header.js
+++ b/tool/header.js
@@ -2,8 +2,6 @@ import {TOOLBAR_HEADER_CLASSNAME} from '../config.js';
import { Tool } from '../tool.js';
export class Header extends Tool {
- expanded = true;
-
constructor(toolbar, title = 'Tools') {
super(toolbar);
this.title = document.createElement('h1');
@@ -24,27 +22,12 @@ export class Header extends Tool {
}
updateButton() {
- this.toggleButton.innerHTML = this.expanded ? '˄' : '˅';
+ this.toggleButton.innerHTML = this.toolbar.expanded ? '˄' : '˅';
}
toggle() {
- this.expanded = !this.expanded;
+ this.toolbar.expanded = !this.toolbar.expanded;
+ this.toolbar.applyExpanded();
this.updateButton();
- this.apply();
- }
-
- apply() {
- for (const tool of this.toolbar.tools) {
- if (tool === this) continue;
- if (this.expanded) {
- if (!this.toolbar.div.contains(tool.div)) {
- this.toolbar.div.appendChild(tool.div);
- }
- } else {
- if (this.toolbar.div.contains(tool.div)) {
- this.toolbar.div.removeChild(tool.div);
- }
- }
- }
}
}
diff --git a/tool/utility.js b/tool/utility.js
new file mode 100644
index 0000000..0362c1a
--- /dev/null
+++ b/tool/utility.js
@@ -0,0 +1,34 @@
+import {Tool} from '../tool.js';
+import {
+ WIDE_CLASSNAME,
+} from '../config.js';
+
+export class UtilityTool extends Tool {
+
+ constructor(toolbar) {
+ super(toolbar);
+
+ const zeroVelocity = document.createElement('button');
+ this.div.appendChild(zeroVelocity);
+ zeroVelocity.classList.add(WIDE_CLASSNAME);
+ zeroVelocity.innerHTML = 'Zero Momentum';
+
+ zeroVelocity.addEventListener('click', () => {
+ // Determine center of mass and average momentum
+ const { totalMass, netMomentum } = this.sim.objects.computeSystemCenter();
+ const netVelocity = {
+ x: netMomentum.x / totalMass,
+ y: netMomentum.y / totalMass,
+ };
+
+ // Apply offset to all object velocities
+ this.sim.objects.forEachObject(obj => {
+ obj.velocity.x -= netVelocity.x;
+ obj.velocity.y -= netVelocity.y;
+ });
+
+ // Cancel panning
+ this.sim.panning = undefined;
+ });
+ }
+}
diff --git a/tool/zoom.js b/tool/zoom.js
index 03c9fd0..3ad84a8 100644
--- a/tool/zoom.js
+++ b/tool/zoom.js
@@ -22,23 +22,19 @@ export class Zoom extends Tool {
const zoomOut = document.createElement('button');
const zoomIn = document.createElement('button');
const zoomAll = document.createElement('button');
- const zeroVelocity = document.createElement('button');
this.div.appendChild(currentScale);
this.div.appendChild(zoomOut);
this.div.appendChild(zoomIn);
this.div.appendChild(zoomAll);
- this.div.appendChild(zeroVelocity);
zoomAll.classList.add(WIDE_CLASSNAME);
- zeroVelocity.classList.add(WIDE_CLASSNAME);
currentScale.classList.add(WIDE_CLASSNAME);
currentScale.classList.add(TOOL_INFO_CLASSNAME);
zoomOut.innerHTML = 'Zoom
Out';
zoomIn.innerHTML = 'Zoom
In';
zoomAll.innerHTML = 'Zoom to Fit';
- zeroVelocity.innerHTML = 'Zero Momentum';
currentScale.innerHTML = this.displayScaleText;
this.sim.onZoom(() => {
@@ -88,41 +84,5 @@ export class Zoom extends Tool {
};
this.sim.scheduleZoom({x, y}, factor, netVelocity)
});
-
- zeroVelocity.addEventListener('click', () => {
- // TODO: Zero net angular momentum
- // Determine center of mass
- const { totalMass, centerOfMass } =
- this.sim.objects.computeSystemCenter();
-
- // Determine total angular momentum
- const netAngularMomentum = this.sim.objects
- .computeSystemAngularMomentum(centerOfMass);
- console.log('net angular momentum', netAngularMomentum);
- const netAngularVelocity = netAngularMomentum / totalMass;
- console.log('net angular velocity', netAngularVelocity);
-
- // TODO: Camera rotation
-
- // Determine average momentum
- const netMomentum = this.sim.objects.reduce((acc, obj) => ({
- x: acc.x + obj.mass * obj.velocity.x,
- y: acc.y + obj.mass * obj.velocity.y,
- }), { x: 0, y: 0 });
-
- const netVelocity = {
- x: netMomentum.x / totalMass,
- y: netMomentum.y / totalMass,
- };
-
- // Apply offset to all object velocities
- this.sim.objects.forEachObject(obj => {
- obj.velocity.x -= netVelocity.x;
- obj.velocity.y -= netVelocity.y;
- });
-
- // Cancel panning
- this.sim.panning = undefined;
- });
}
}
diff --git a/toolbar.js b/toolbar.js
index 8ffa0b1..eec256e 100644
--- a/toolbar.js
+++ b/toolbar.js
@@ -6,22 +6,21 @@ import {Header} from './tool/header.js';
export class Toolbar {
sim = undefined;
tools = [];
+ expanded = undefined;
+ header = undefined;
- constructor(sim, title, group) {
+ constructor(sim, title, { expanded } = {}) {
this.sim = sim;
+ this.expanded = expanded ?? true;
// Create ourselves a div, as child of sim's div
const div = document.createElement('div');
this.div = div;
- if (group) {
- group.addToolbar(this);
- } else {
- this.sim.div.appendChild(div);
- }
div.classList.add(TOOLBAR_CLASSNAME);
// Create a collapse/expand tool
const header = new Header(this, title);
+ this.header = header;
this.addTool(header);
}
@@ -37,4 +36,19 @@ export class Toolbar {
tool.frame();
}
}
+
+ applyExpanded() {
+ for (const tool of this.tools) {
+ if (tool === this.header) continue;
+ if (this.expanded) {
+ if (!this.div.contains(tool.div)) {
+ this.div.appendChild(tool.div);
+ }
+ } else {
+ if (this.div.contains(tool.div)) {
+ this.div.removeChild(tool.div);
+ }
+ }
+ }
+ }
}