From ef51f436c333e9832a42ec2c931e6416bdab72e4 Mon Sep 17 00:00:00 2001 From: Lentil Hoffman Date: Mon, 29 Dec 2025 17:50:09 -0600 Subject: [PATCH] moved Zero Momentum to Utilities toolbar --- Readme.md | 2 +- config.js | 8 ----- display.js | 7 ++--- objects.js | 27 +++++++++------- options.js | 9 +++++- pointer.js | 3 +- simulator.js | 84 ++++++++++++++++++++++++++++--------------------- tool/header.js | 23 ++------------ tool/utility.js | 34 ++++++++++++++++++++ tool/zoom.js | 40 ----------------------- toolbar.js | 26 +++++++++++---- 11 files changed, 135 insertions(+), 128 deletions(-) create mode 100644 tool/utility.js 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); + } + } + } + } }