diff --git a/Readme.md b/Readme.md
index b6520d3..1ff4a50 100644
--- a/Readme.md
+++ b/Readme.md
@@ -8,7 +8,8 @@ Uses `npm` for `eslint`.
Screenshots
-----------
-
+
+
TODO
----
@@ -30,10 +31,10 @@ TODO
- [ ] Undo "Clear Traces" Action
- [ ] Undo "Reset
- [ ] Time Control: Reverse Time
-- [ ] Save to LocalStorage
+- [x] Save to LocalStorage
- [ ] Lossy Rescaling To Widen Zoom (Handling overflow/underflow)
- [ ] Track farthest reaches, min/max in each dimension (x, y)
- - [ ] Enabling Zoom to Fit Traces
- [x] Compute Net Angular Momentum
- [ ] Display Net Angular Momentum
-
+- [ ] Calculate Work as FxD as measure of energy flux
+ - [ ] Option to automatically slow time when energy flux is greater
diff --git a/config.js b/config.js
index dfdaa70..b44f303 100644
--- a/config.js
+++ b/config.js
@@ -18,6 +18,7 @@ export const PATH_TRACES_DASHED_OPACITY = 1.0;
// SIZES
export const POINTER_HISTORY_SIZE = 20;
+export const FRAMERATE_SAMPLE_DURATION = 2.0; // Seconds
export const POINTER_DOWN_HISTORY_SIZE = 5;
export const ARROWHEAD_LENGTH = 7;
export const ARROWHEAD_WIDTH = 5;
@@ -36,6 +37,7 @@ export const TOOL_INFO_CLASSNAME = 'lhg-tool-info';
export const TOOLBAR_CLASSNAME = 'lhg-toolbar';
export const TOOLBAR_HEADER_CLASSNAME = 'lhg-toolbar-header';
export const WIDE_CLASSNAME = 'lhg-wide';
+export const TALL_CLASSNAME = 'lhg-tall';
export const OVERLAY_INFO_BOX_CLASSNAME = 'lhg-overlay-info-box';
// EVENT NAMES
@@ -48,3 +50,6 @@ export const EVENT_OPTION_SET = 'lhg-option-set';
export const MODE_MASS_GENERATION = 'mass-gen';
export const MODE_PAN_VIEW = 'pan-view';
export const MODE_OBJECT_SELECT = 'select';
+
+// LOCAL STORAGE PREFIXES/SUFFIXES
+export const TOOLBAR_EXPANDED_SUFFIX = 'lhg-toolbar-expanded';
diff --git a/display.js b/display.js
index c97a2fc..e193d37 100644
--- a/display.js
+++ b/display.js
@@ -59,9 +59,6 @@ export class Display {
fullscreen() {
this.canvas.width = document.documentElement.clientWidth;
this.canvas.height = document.documentElement.clientHeight;
- if (this.sim.getOption('debug.canvasSize')) {
- this.sim.info['Canvas'] = [`${this.canvas.width}`, `${this.canvas.height}`];
- }
}
fillCanvas() {
@@ -160,8 +157,9 @@ export class Display {
} else if (this.sim.panning && !this.sim.panning.paused) {
// Apply update to viewOrigin based on panning
const { velocity } = this.sim.panning;
- this.viewOrigin.x -= velocity.x * elapsedTime;
- this.viewOrigin.y -= velocity.y * elapsedTime;
+ // TODO: something with time scale? Panning is too fast.
+ this.viewOrigin.x -= velocity.x * elapsedTime / 1000; // millisecond conversion?
+ this.viewOrigin.y -= velocity.y * elapsedTime / 1000;
}
}
diff --git a/gravity-simulator-3.png b/gravity-simulator-3.png
new file mode 100644
index 0000000..2d104ad
Binary files /dev/null and b/gravity-simulator-3.png differ
diff --git a/objects.js b/objects.js
index a1237c4..56233eb 100644
--- a/objects.js
+++ b/objects.js
@@ -200,13 +200,20 @@ export class Objects {
});
}
+ // elapsedTime is given in milliseconds
computeFrame(elapsedTime) {
+ // convert elapsed time to seconds
+ elapsedTime /= 1000;
// If we're creating an object, increment its mass
// with the mass creation rate accelerating over time
- const massCreationRate = this.sim.getOption('param.massCreationRate');
+
+ // Scaling this parameter because of millisecond conversion
+ const massCreationRate = this.sim.getOption('param.massCreationRate') / 1000;
+
if (this.creatingObject !== undefined) {
const obj = this.objects[this.creatingObject];
const rate = massCreationRate * obj.age;
+ console.log('obj.age', obj.age, 'mass creation rate', rate, 'elapsedTime', elapsedTime);
// TODO: After objects merge during creation, mass creation rate can accelerate
obj.mass += rate * elapsedTime;
}
@@ -215,6 +222,8 @@ export class Objects {
this.computeForces();
if (this.sim.playing) {
+ // TODO: If creating/selected object, clamp its position to the cursor
+
// Predict positions (Velocity verlet method)
this.forEachObject(obj => {
obj.currentAcceleration = {...obj.acceleration};
@@ -306,8 +315,9 @@ export class Objects {
`${obj.position.y.toPrecision(6)}, `,
`${obj.mass.toPrecision(6)} kg, `,
`${speed.toPrecision(2)} m/s, ${direction.toPrecision(2)}°`,
+ `Alive: ${obj.alive}`,
];
- });
+ }, { alive: null });
}
}
diff --git a/options.js b/options.js
index ac1598f..5381ad1 100644
--- a/options.js
+++ b/options.js
@@ -2,54 +2,51 @@ import {
EVENT_OPTION_SET,
} from './config.js';
-export const optionsLayout = {
- 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
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],
- canvasSize: ['Canvas Size', 'boolean', false],
- currentMode: ['Current Mode', 'boolean', false],
- panningInfo: ['Panning Info', 'boolean', false],
- },
-};
-
export class Options {
sim = undefined;
+ options = undefined;
values = {};
- constructor(sim) {
+ getStorageKey(path) {
+ return `${path}:options`;
+ }
+
+ constructor(sim, options) {
this.sim = sim;
+ this.options = options;
// Global methods to get/set current option values
this.sim.getOption = (path) => this.getOption(path);
this.sim.setOption = (path, value) => this.setOption(path, value);
this.sim.onOptionSet = (path, cb) => this.onOptionSet(path, cb);
+
+ // Initialize values from localStorage
+ for (const groupName of Object.keys(options)) {
+ for (const [name, [, , defaultValue]] of Object.entries(this.options[groupName])) {
+ const path = [groupName, name].join('.');
+ let value = this.getOption(path)
+ if (value === undefined) {
+ value = defaultValue;
+ this.setOption(path, value);
+ }
+ }
+ }
}
getOption(path) {
- const val = this.values[path];
- return val;
+ let value = this.values[path];
+ if (value === undefined) {
+ value = localStorage.getItem(this.getStorageKey(path));
+ if (value === 'false') value = false;
+ else if (value === 'true') value = true;
+ this.values[path] = value;
+ }
+ return value;
}
setOption(path, value) {
this.values[path] = value;
+ window.localStorage.setItem(this.getStorageKey(path), value);
const e = new CustomEvent(EVENT_OPTION_SET, {detail: {path, value}});
this.sim.div.dispatchEvent(e);
}
@@ -63,8 +60,8 @@ export class Options {
});
}
- static getSection(layout, sectionName) {
- const section = layout[sectionName];
+ getSection(sectionName) {
+ const section = this.options[sectionName];
const group = {
type: 'group',
name: sectionName,
diff --git a/pointer.js b/pointer.js
index 0252b88..d9c10bc 100644
--- a/pointer.js
+++ b/pointer.js
@@ -66,7 +66,7 @@ export class Pointer {
points = Math.min(points, POINTER_HISTORY_SIZE, this.pointerHistory.length);
const start = this.pointerHistory[this.pointerHistory.length - points];
const end = this.pointerHistory[this.pointerHistory.length - 1];
- const dt = (end.t - start.t) / 1000;
+ const dt = (end.t - start.t);
return {
x: (end.x - start.x) / dt,
y: (end.y - start.y) / dt,
@@ -145,14 +145,13 @@ export class Pointer {
// TODO: If e.touches.length > 1, user may be engaging pinch to zoom
handlePointerMove({x: clientX, y: clientY}) {
this.updatePointer({x: clientX, y: clientY});
- const v = this.latestPointerVelocity;
- // Convert pointer velocity to simulation scale
- v.x /= this.sim.display.scale;
- v.y /= this.sim.display.scale;
if (this.sim.isCurrentMode(MODE_MASS_GENERATION)) {
+ // Convert pointer velocity to simulation scale
+ const vx = this.latestPointerVelocity.x / this.sim.display.scale;
+ const vy = this.latestPointerVelocity.y / this.sim.display.scale;
const {x, y} = this.sim.screenToSim(clientX, clientY);
- this.sim.objects.handlePointerMove({x, y, vx: v.x, vy: v.y});
+ this.sim.objects.handlePointerMove({x, y, vx, vy});
} else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) {
if (this.panTouchStart) {
@@ -160,7 +159,7 @@ export class Pointer {
this.panTouchLatest = {
x: clientX,
y: clientY,
- t: document.timeline.currentTime,
+ t: this.sim.rawTime,
};
}
}
diff --git a/simulator.js b/simulator.js
index a243b0a..111cae2 100644
--- a/simulator.js
+++ b/simulator.js
@@ -1,5 +1,6 @@
import {
EVENT_ZOOM,
+ FRAMERATE_SAMPLE_DURATION,
SCALE_POWER_MAX,
SCALE_POWER_MIN,
} from './config.js';
@@ -16,12 +17,42 @@ import { UtilityTool } from './tool/utility.js';
import { Toolbar } from './toolbar.js';
import { ToolbarGroup } from './toolbar-group.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
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;
@@ -38,12 +69,26 @@ export class Sim {
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);
+ this.options = new Options(this, simOptions);
this.display = new Display(this);
this.objects = new Objects(this);
this.toolbars = {
@@ -95,7 +140,7 @@ export class Sim {
}
// Initiate main loop
- this.rawTime = document.timeline.currentTime / 1000;
+ this.rawTime = document.timeline.currentTime;
this.time = 0;
requestAnimationFrame(t => this.loop(t));
}
@@ -163,10 +208,14 @@ export class Sim {
// Main loop
loop(currentTime) {
- currentTime /= 1000;
+ this.markFrame(currentTime);
const timeScale = this.getOption('param.timeScale');
- const elapsedTime = (currentTime - this.rawTime) * timeScale;
+
+ // elapsedTime in milliseconds
+ // rawTime in milliseconds
+ const elapsedTime = (currentTime - this.rawTime) / timeScale;
this.rawTime = currentTime;
+
if (this.playing) {
this.time += elapsedTime;
}
@@ -175,6 +224,10 @@ export class Sim {
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;
@@ -188,15 +241,14 @@ export class Sim {
this.info['Net Angular Momentum'] = this.objects.computeSystemAngularMomentum().toPrecision(6);
}
- this.display.computePanning(elapsedTime);
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));
}
}
diff --git a/style.css b/style.css
index bcd5887..8b713cc 100644
--- a/style.css
+++ b/style.css
@@ -33,8 +33,8 @@ div.lhg-toolbar {
width: fit-content;
margin: 0.5em;
border-radius: 0.5em;
- border-width: 1px;
- border-color: #282;
+ border-width: 2px;
+ border-color: #151;
border-style: solid;
}
@@ -44,7 +44,7 @@ div.lhg-tool {
left: 0;
width: 12em;
/* padding: 0.5em; */
- margin: 0.5em;
+ /* margin: 0.5em; */
text-align: middle;
}
@@ -107,6 +107,11 @@ div.lhg-tool .lhg-wide {
width: 12em;
}
+div.lhg-tool .lhg-tall {
+ padding-top: 1em;
+ padding-bottom: 1em;
+}
+
div.lhg-overlay-info-box {
position: absolute;
top: 0px;
diff --git a/tool.js b/tool.js
index 90ebf34..eaccac5 100644
--- a/tool.js
+++ b/tool.js
@@ -1,18 +1,18 @@
// Idea here is, tool can declare its parameters;
-// can call back to toolbar for whatever...
-// through toolbar can access sim
+// can call back to container for whatever...
+// through container can access sim
import {
TOOL_CLASSNAME,
} from './config.js';
export class Tool {
- toolbar = undefined;
+ container = undefined;
sim = undefined;
- constructor(toolbar) {
- this.toolbar = toolbar;
- this.sim = this.toolbar.sim;
+ constructor(container) {
+ this.container = container;
+ this.sim = this.container.sim;
const div = document.createElement('div');
this.div = div;
div.classList.add(TOOL_CLASSNAME)
diff --git a/tool/header.js b/tool/header.js
index bebfc60..1dd2433 100644
--- a/tool/header.js
+++ b/tool/header.js
@@ -2,8 +2,9 @@ import {TOOLBAR_HEADER_CLASSNAME} from '../config.js';
import { Tool } from '../tool.js';
export class Header extends Tool {
- constructor(toolbar, title = 'Tools') {
- super(toolbar);
+
+ constructor(container, title = 'Tools') {
+ super(container);
this.title = document.createElement('h1');
this.title.innerHTML = title;
@@ -22,12 +23,12 @@ export class Header extends Tool {
}
updateButton() {
- this.toggleButton.innerHTML = this.toolbar.expanded ? '˄' : '˅';
+ this.toggleButton.innerHTML = this.container.expanded ? '˅' : '˄';
}
toggle() {
- this.toolbar.expanded = !this.toolbar.expanded;
- this.toolbar.applyExpanded();
+ this.container.expanded = !this.container.expanded;
+ this.container.applyExpanded();
this.updateButton();
}
}
diff --git a/tool/modes.js b/tool/modes.js
index fe37178..cd009dc 100644
--- a/tool/modes.js
+++ b/tool/modes.js
@@ -18,8 +18,8 @@ export class ModeSwitch extends Tool {
];
buttons = [];
- constructor(toolbar) {
- super(toolbar);
+ constructor(container) {
+ super(container);
const modesDiv = document.createElement('div');
const heading = document.createElement('h2');
diff --git a/tool/options.js b/tool/options.js
index 5dd0163..206b7a8 100644
--- a/tool/options.js
+++ b/tool/options.js
@@ -4,7 +4,6 @@ import {
WIDE_CLASSNAME,
} from '../config.js';
import { Tool } from '../tool.js';
-import { Options, optionsLayout } from '../options.js';
export class OptionsTool extends Tool {
visitItem(item, path) {
@@ -29,14 +28,20 @@ export class OptionsTool extends Tool {
if (item.wide === true) {
button.classList.add(WIDE_CLASSNAME);
}
- this.sim.setOption(path, item.default);
- button.style.opacity = this.sim.getOption(path) ? '100%' : '50%';
- button.addEventListener('click', () => {
- this.sim.setOption(path, !this.sim.getOption(path));
- button.style.opacity = this.sim.getOption(path) ? '100%' : '50%';
- });
+ const value = this.sim.getOption(path);
+ if (value === undefined) {
+ this.sim.setOption(path, item.default);
+ }
+ button.style.opacity = value ? '100%' : '50%';
this.sim.onOptionSet(path, value => {
+ console.log('option set cb', path, value);
button.style.opacity = value ? '100%' : '50%';
+ console.log('button opacity', button.style.opacity);
+ });
+ button.addEventListener('click', () => {
+ const value = this.sim.options.getOption(path, true);
+ console.log('click, option value', value);
+ this.sim.setOption(path, !value);
});
return button;
}
@@ -77,12 +82,12 @@ export class OptionsTool extends Tool {
}
}
- constructor(toolbar, sections) {
- super(toolbar);
+ constructor(container, sections) {
+ super(container);
for (const sectionName of sections) {
- const item = Options.getSection(optionsLayout, sectionName);
- const child = this.visitItem(item);
+ const option = this.sim.options.getSection(sectionName);
+ const child = this.visitItem(option);
this.div.appendChild(child);
}
}
diff --git a/tool/play-pause.js b/tool/play-pause.js
index 491969d..6d661bb 100644
--- a/tool/play-pause.js
+++ b/tool/play-pause.js
@@ -1,68 +1,36 @@
-import {TOOL_INFO_CLASSNAME, WIDE_CLASSNAME} from '../config.js';
+import {
+ TALL_CLASSNAME
+} from '../config.js';
import {Tool} from '../tool.js';
export class PlayPause extends Tool {
playHTML = 'Play';
pauseHTML = 'Pause';
- currentTimeEl = undefined;
pauseButton = undefined;
playButton = undefined;
- get timeText() {
- let time = this.sim.time;
- // Time in seconds
- const s = time % 60;
- time = (time - s) / 60;
- const m = time % 60;
- time = (time - m) / 60;
- const h = time % 24;
- time = (time - h) / 24;
- const d = time;
- time -= m * 60;
-
- const ms = (s - Math.floor(s)) * 1000;
- return [
- d || undefined,
- h.toString().padStart(2, '0'),
- m.toString().padStart(2, '0'),
- [
- s.toFixed(0).padStart(2, '0'),
- ms.toFixed(0).padStart(3, '0'),
- ].join('.')
- ].filter(x => x !== undefined).join(':');
- }
-
- frame() {
- if (this.currentTimeEl) {
- this.currentTimeEl.innerHTML = this.timeText;
- }
- }
-
updateButtons() {
this.pauseButton.style.opacity = this.sim.playing ? '100%' : '50%';
this.playButton.style.opacity = this.sim.playing ? '50%' : '100%';
}
- constructor(toolbar) {
- super(toolbar);
+ constructor(container) {
+ super(container);
- const currentTime = document.createElement('button');
const pauseButton = document.createElement('button');
const playButton = document.createElement('button');
+
this.pauseButton = pauseButton;
this.playButton = playButton;
- this.currentTimeEl = currentTime;
- this.div.appendChild(currentTime);
this.div.appendChild(pauseButton);
this.div.appendChild(playButton);
- currentTime.classList.add(TOOL_INFO_CLASSNAME);
- currentTime.classList.add(WIDE_CLASSNAME);
-
pauseButton.innerHTML = this.pauseHTML;
playButton.innerHTML = this.playHTML;
- currentTime.innerHTML = this.timeText;
+
+ playButton.classList.add(TALL_CLASSNAME);
+ pauseButton.classList.add(TALL_CLASSNAME);
this.updateButtons();
diff --git a/tool/utility.js b/tool/utility.js
index 268da4c..2e92b2c 100644
--- a/tool/utility.js
+++ b/tool/utility.js
@@ -1,23 +1,83 @@
import {Tool} from '../tool.js';
import {
+ TOOL_INFO_CLASSNAME,
WIDE_CLASSNAME,
} from '../config.js';
export class UtilityTool extends Tool {
- constructor(toolbar) {
- super(toolbar);
+ currentTimeEl = undefined;
+
+ get displayScaleText() {
+ return `Scale: ${this.sim.getScaleDisplay()}`;
+ }
+
+ get timeText() {
+ let time = this.sim.time;
+ // Time in milliseconds
+ const ms = Math.floor(time % 1000);
+ time = (time - ms) / 1000;
+ const s = Math.floor(time % 60);
+ time = (time - s) / 60;
+ const m = Math.floor(time % 60);
+ time = (time - m) / 60;
+ const h = Math.floor(time % 24);
+ time = (time - h) / 24;
+ const d = Math.floor(time);
+ return [
+ d || undefined,
+ h.toString().padStart(2, '0'),
+ m.toString().padStart(2, '0'),
+ [
+ s.toString().padStart(2, '0'),
+ ms.toString().padStart(3, '0'),
+ ].join('.')
+ ].filter(x => x !== undefined).join(':');
+ }
+
+ frame() {
+ if (this.currentTimeEl) {
+ this.currentTimeEl.innerHTML = this.timeText;
+ }
+ }
+
+ constructor(container) {
+ super(container);
const zeroVelocity = document.createElement('button');
const clearTraces = document.createElement('button');
+ const zoomAll = document.createElement('button');
+ const currentScale = document.createElement('button')
+ const currentTime = document.createElement('button');
+ const clearDebug = document.createElement('button');
- this.div.appendChild(clearTraces);
+ this.currentTimeEl = currentTime;
+
+ this.div.appendChild(currentTime);
+ this.div.appendChild(currentScale);
+ this.div.appendChild(zoomAll);
this.div.appendChild(zeroVelocity);
+ this.div.appendChild(clearTraces);
+ this.div.appendChild(clearDebug);
zeroVelocity.classList.add(WIDE_CLASSNAME);
clearTraces.classList.add(WIDE_CLASSNAME);
+ zoomAll.classList.add(WIDE_CLASSNAME);
+ currentScale.classList.add(WIDE_CLASSNAME);
+ currentScale.classList.add(TOOL_INFO_CLASSNAME);
+ currentTime.classList.add(TOOL_INFO_CLASSNAME);
+ currentTime.classList.add(WIDE_CLASSNAME);
+ clearDebug.classList.add(WIDE_CLASSNAME);
zeroVelocity.innerHTML = 'Zero Momentum';
clearTraces.innerHTML = 'Clear Traces';
+ zoomAll.innerHTML = 'Zoom to Fit';
+ currentScale.innerHTML = this.displayScaleText;
+ currentTime.innerHTML = this.timeText;
+ clearDebug.innerHTML = 'Clear Debug';
+
+ this.sim.onZoom(() => {
+ currentScale.innerHTML = this.displayScaleText;
+ });
zeroVelocity.addEventListener('click', () => {
// Determine center of mass and average momentum
@@ -43,5 +103,39 @@ export class UtilityTool extends Tool {
obj.history = [];
}, {alive: null});
});
+
+ zoomAll.addEventListener('click', () => {
+ // Determine bounding box
+ const box = this.sim.objects.boundingBox;
+ const x = (box.start.x + box.end.x) / 2;
+ const y = (box.start.y + box.end.y) / 2;
+ const widthRatio = Math.abs(box.start.x - box.end.x) / this.sim.display.width;
+ const heightRatio = Math.abs(box.start.y - box.end.y) / this.sim.display.height;
+ const biggerRatio = Math.max(widthRatio, heightRatio);
+ const base2factor = Math.log(1/biggerRatio) / Math.log(2) - 0.5;
+ const factor = Math.floor(base2factor);
+ // Determine average momentum and set panning velocity to match
+ const netMomentum = {x: 0, y: 0};
+ let totalMass = 0;
+ let count = 0;
+ this.sim.objects.forEachObject(obj => {
+ count++;
+ netMomentum.x += obj.mass * obj.velocity.x;
+ netMomentum.y += obj.mass * obj.velocity.y;
+ totalMass += obj.mass;
+ });
+ if (!count) {
+ return;
+ }
+ const netVelocity = {
+ x: netMomentum.x / totalMass,
+ y: netMomentum.y / totalMass,
+ };
+ this.sim.scheduleZoom({x, y}, factor, netVelocity)
+ });
+
+ clearDebug.addEventListener('click', () => {
+ this.sim.info = {};
+ });
}
}
diff --git a/tool/zoom.js b/tool/zoom.js
index 3ad84a8..ff738f2 100644
--- a/tool/zoom.js
+++ b/tool/zoom.js
@@ -1,45 +1,21 @@
import { Tool } from '../tool.js';
import {
- TOOL_INFO_CLASSNAME,
- WIDE_CLASSNAME,
ZOOM_IN_FACTOR,
ZOOM_OUT_FACTOR,
} from '../config.js';
export class Zoom extends Tool {
- get displayScale() {
- return this.sim.getScaleDisplay();
- }
+ constructor(container) {
+ super(container);
- get displayScaleText() {
- return `Scale: ${this.displayScale}`;
- }
-
- constructor(toolbar) {
- super(toolbar);
-
- const currentScale = document.createElement('button')
const zoomOut = document.createElement('button');
const zoomIn = document.createElement('button');
- const zoomAll = document.createElement('button');
- this.div.appendChild(currentScale);
this.div.appendChild(zoomOut);
this.div.appendChild(zoomIn);
- this.div.appendChild(zoomAll);
-
- zoomAll.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';
- currentScale.innerHTML = this.displayScaleText;
-
- this.sim.onZoom(() => {
- currentScale.innerHTML = this.displayScaleText;
- });
zoomOut.addEventListener('click', () => {
// Aim at center of view
@@ -54,35 +30,5 @@ export class Zoom extends Tool {
const y = this.sim.display.height * this.sim.display.scale / 2;
this.sim.scheduleZoom(this.sim.screenToSim(x, y), ZOOM_IN_FACTOR);
});
-
- zoomAll.addEventListener('click', () => {
- // Determine bounding box
- const box = this.sim.objects.boundingBox;
- const x = (box.start.x + box.end.x) / 2;
- const y = (box.start.y + box.end.y) / 2;
- const widthRatio = Math.abs(box.start.x - box.end.x) / this.sim.display.width;
- const heightRatio = Math.abs(box.start.y - box.end.y) / this.sim.display.height;
- const biggerRatio = Math.max(widthRatio, heightRatio);
- const base2factor = Math.log(1/biggerRatio) / Math.log(2) - 1;
- const factor = Math.floor(base2factor);
- // Determine average momentum and set panning velocity to match
- const netMomentum = {x: 0, y: 0};
- let totalMass = 0;
- let count = 0;
- this.sim.objects.forEachObject(obj => {
- count++;
- netMomentum.x += obj.mass * obj.velocity.x;
- netMomentum.y += obj.mass * obj.velocity.y;
- totalMass += obj.mass;
- });
- if (!count) {
- return;
- }
- const netVelocity = {
- x: netMomentum.x / totalMass,
- y: netMomentum.y / totalMass,
- };
- this.sim.scheduleZoom({x, y}, factor, netVelocity)
- });
}
}
diff --git a/toolbar.js b/toolbar.js
index eec256e..4281035 100644
--- a/toolbar.js
+++ b/toolbar.js
@@ -1,5 +1,6 @@
import {
TOOLBAR_CLASSNAME,
+ TOOLBAR_EXPANDED_SUFFIX,
} from './config.js';
import {Header} from './tool/header.js';
@@ -8,10 +9,17 @@ export class Toolbar {
tools = [];
expanded = undefined;
header = undefined;
+ title = undefined;
- constructor(sim, title, { expanded } = {}) {
+ // TODO: Index on something more durable than title
+ getExpandedKey() {
+ return [this.title, TOOLBAR_EXPANDED_SUFFIX].join(':');
+ }
+
+ constructor(sim, title) {
this.sim = sim;
- this.expanded = expanded ?? true;
+ this.title = title;
+ this.expanded = window.localStorage.getItem(this.getExpandedKey()) === 'true';
// Create ourselves a div, as child of sim's div
const div = document.createElement('div');
@@ -22,6 +30,9 @@ export class Toolbar {
const header = new Header(this, title);
this.header = header;
this.addTool(header);
+
+ header.updateButton();
+ this.applyExpanded();
}
// tool: instance of Tool
@@ -38,8 +49,10 @@ export class Toolbar {
}
applyExpanded() {
+ window.localStorage.setItem(this.getExpandedKey(), this.expanded);
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);