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 ----------- -![Simulation of a small body orbiting a pair of larger ones](./gravity-simulator-2.png "Gravity Simulator Screenshot 2") +![A small body orbiting a pair of larger ones](./gravity-simulator-2.png "Gravity Simulator Screenshot 2") +![A body orbiting a pair, all three of similar size](./gravity-simulator-3.png "Gravity Simulator Screenshot 3") 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);