moved Zero Momentum to Utilities toolbar

This commit is contained in:
Lentil Hoffman 2025-12-29 17:50:09 -06:00
parent d869bc5a6f
commit ef51f436c3
Signed by: lentil
GPG Key ID: 0F5B99F3F4D0C087
11 changed files with 135 additions and 128 deletions

View File

@ -13,7 +13,7 @@ Screenshots
TODO TODO
---- ----
- [ ] Parameter Slider - [ ] Parameter Slider (Invisible, mouse/touch drag)
- [ ] Selection Box - [ ] Selection Box
- [ ] Object List - [ ] Object List
- [ ] Object Detail - [ ] Object Detail

View File

@ -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 // VELOCITY
export const VELOCITY_VECTOR_SCALE = 8E0; export const VELOCITY_VECTOR_SCALE = 8E0;
export const VELOCITY_VECTOR_COLOR = 'rgba(150, 150, 150, 0.8)'; // optionally set to 'object color' export const VELOCITY_VECTOR_COLOR = 'rgba(150, 150, 150, 0.8)'; // optionally set to 'object color'

View File

@ -1,7 +1,6 @@
import { import {
ARROWHEAD_LENGTH, ARROWHEAD_LENGTH,
ARROWHEAD_WIDTH, ARROWHEAD_WIDTH,
DISPLAY_CANVAS_SIZE,
} from './config.js'; } from './config.js';
export class Display { export class Display {
@ -60,8 +59,8 @@ export class Display {
fullscreen() { fullscreen() {
this.canvas.width = document.documentElement.clientWidth; this.canvas.width = document.documentElement.clientWidth;
this.canvas.height = document.documentElement.clientHeight; this.canvas.height = document.documentElement.clientHeight;
if (DISPLAY_CANVAS_SIZE) { if (this.sim.getOption('debug.canvasSize')) {
this.sim.info['Canvas'] = `${this.canvas.width} x ${this.canvas.height}`; this.sim.info['Canvas'] = [`${this.canvas.width}`, `${this.canvas.height}`];
} }
} }

View File

@ -1,8 +1,5 @@
import { MassObject } from './object.js'; import { MassObject } from './object.js';
import { import { ZOOM_TO_FIT_PADDING } from './config.js';
DISPLAY_OBJECTS_INFO,
ZOOM_TO_FIT_PADDING,
} from './config.js';
export class Objects { export class Objects {
objects = []; objects = [];
@ -299,7 +296,7 @@ export class Objects {
} }
// Display objects info // Display objects info
if (DISPLAY_OBJECTS_INFO) { if (this.sim.getOption('debug.objectsInfo')) {
this.forEachObject((obj, i) => { this.forEachObject((obj, i) => {
const speed = Math.pow(obj.velocity.x ** 2 + obj.velocity.y ** 2, 1/2); 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 // Invert y so that the angle is counterclockwise from x-axis
@ -330,17 +327,25 @@ export class Objects {
count: 0, count: 0,
}); });
if (!count) return; const centerOfMass = count ? {
const centerOfMass = {
x: totalMassLocation.x / totalMass, x: totalMassLocation.x / totalMass,
y: totalMassLocation.y / totalMass, y: totalMassLocation.y / totalMass,
}; } : {x: 0, y: 0};
return { totalMass, count, totalMassLocation, centerOfMass }; // 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) { computeSystemAngularMomentum(centerOfMass) {
if (!centerOfMass) {
const sys = this.computeSystemCenter();
centerOfMass = sys.centerOfMass;
}
return this.reduce((acc, obj) => { return this.reduce((acc, obj) => {
// Angular momentum for each object is m * s / d // Angular momentum for each object is m * s / d
// where d is the distance of the object from the global center of mass // where d is the distance of the object from the global center of mass

View File

@ -20,7 +20,14 @@ export const optionsLayout = {
gravity: ['Gravity', 'number', 4E4], gravity: ['Gravity', 'number', 4E4],
timeScale: ['Time Scale', 'number', 0.2], timeScale: ['Time Scale', 'number', 0.2],
massCreationRate: ['Mass Creation Rate', 'number', 10], 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 { export class Options {

View File

@ -1,5 +1,4 @@
import { import {
DISPLAY_CURSOR_INFO,
MODE_MASS_GENERATION, MODE_MASS_GENERATION,
MODE_OBJECT_SELECT, MODE_OBJECT_SELECT,
MODE_PAN_VIEW, MODE_PAN_VIEW,
@ -25,7 +24,7 @@ export class Pointer {
const el = window; const el = window;
el.addEventListener('pointermove', e => { 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.sim.info['pointermove'] = [`${e.clientX.toPrecision(6)}, `, `${e.clientY.toPrecision(6)}`];
} }
this.handlePointerMove({x: e.clientX, y: e.clientY}); this.handlePointerMove({x: e.clientX, y: e.clientY});

View File

@ -1,7 +1,4 @@
import { import {
DISPLAY_CURRENT_MODE,
DISPLAY_CURRENT_SCALE,
DISPLAY_PANNING_INFO,
EVENT_ZOOM, EVENT_ZOOM,
SCALE_POWER_MAX, SCALE_POWER_MAX,
SCALE_POWER_MIN, SCALE_POWER_MIN,
@ -15,6 +12,7 @@ import { ModeSwitch } from './tool/modes.js';
import { OptionsTool } from './tool/options.js'; import { OptionsTool } from './tool/options.js';
import { PlayPause } from './tool/play-pause.js'; import { PlayPause } from './tool/play-pause.js';
import { Zoom } from './tool/zoom.js'; import { Zoom } from './tool/zoom.js';
import { UtilityTool } from './tool/utility.js';
import { Toolbar } from './toolbar.js'; import { Toolbar } from './toolbar.js';
import { ToolbarGroup } from './toolbar-group.js'; import { ToolbarGroup } from './toolbar-group.js';
@ -48,35 +46,52 @@ export class Sim {
this.options = new Options(this); this.options = new Options(this);
this.display = new Display(this); this.display = new Display(this);
this.objects = new Objects(this); this.objects = new Objects(this);
this.toolbarGroups = {
left: new ToolbarGroup(this),
right: new ToolbarGroup(this).topRight(),
};
this.toolbars = { this.toolbars = {
tools: new Toolbar(this, 'Tools', this.toolbarGroups.left), tools: new Toolbar(this, 'Tools'),
modes: new Toolbar(this, 'Modes', this.toolbarGroups.left), modes: new Toolbar(this, 'Modes'),
options: new Toolbar(this, 'Options', this.toolbarGroups.right), utils: new Toolbar(this, 'Utility'),
params: new Toolbar(this, 'Parameters', this.toolbarGroups.right), 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.overlay = new Overlay(this);
this.pointer = new Pointer(this); this.pointer = new Pointer(this);
{ // Configure toolbars
// Configure toolbars
const { tools, modes, options, params } = this.toolbars; // Primary
// Primary Toolbar tools.addTool(new Zoom(tools));
tools.addTool(new Zoom(tools)); tools.addTool(new PlayPause(tools));
tools.addTool(new PlayPause(tools));
// Secondary Toolbar: Mode Switches // Secondary
modes.addTool(new ModeSwitch(modes)); modes.addTool(new ModeSwitch(modes));
// Options Toolbar
options.addTool(new OptionsTool(options, [ // Utility
'pauseDuring', 'display', 'collision' utils.addTool(new UtilityTool(utils));
]));
// Parameters Toolbar // Options
params.addTool(new OptionsTool(params, [ options.addTool(new OptionsTool(options, ['pauseDuring', 'display', 'collision']));
'param'
])); // 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 // Initiate main loop
@ -156,7 +171,7 @@ export class Sim {
this.time += elapsedTime; this.time += elapsedTime;
} }
if (DISPLAY_CURRENT_MODE) { if (this.getOption('debug.currentMode')) {
this.info['Mode'] = this.getCurrentMode(); this.info['Mode'] = this.getCurrentMode();
} }
@ -165,13 +180,12 @@ export class Sim {
this.nextZoom = undefined; this.nextZoom = undefined;
} }
if (DISPLAY_CURRENT_SCALE) { if (this.getOption('debug.panningInfo')) {
this.info['Scale'] = this.getScaleDisplay();
}
if (DISPLAY_PANNING_INFO) {
const {x, y} = this.panning?.velocity ?? {}; 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); this.display.computePanning(elapsedTime);

View File

@ -2,8 +2,6 @@ import {TOOLBAR_HEADER_CLASSNAME} from '../config.js';
import { Tool } from '../tool.js'; import { Tool } from '../tool.js';
export class Header extends Tool { export class Header extends Tool {
expanded = true;
constructor(toolbar, title = 'Tools') { constructor(toolbar, title = 'Tools') {
super(toolbar); super(toolbar);
this.title = document.createElement('h1'); this.title = document.createElement('h1');
@ -24,27 +22,12 @@ export class Header extends Tool {
} }
updateButton() { updateButton() {
this.toggleButton.innerHTML = this.expanded ? '˄' : '˅'; this.toggleButton.innerHTML = this.toolbar.expanded ? '˄' : '˅';
} }
toggle() { toggle() {
this.expanded = !this.expanded; this.toolbar.expanded = !this.toolbar.expanded;
this.toolbar.applyExpanded();
this.updateButton(); 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);
}
}
}
} }
} }

34
tool/utility.js Normal file
View File

@ -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;
});
}
}

View File

@ -22,23 +22,19 @@ export class Zoom extends Tool {
const zoomOut = document.createElement('button'); const zoomOut = document.createElement('button');
const zoomIn = document.createElement('button'); const zoomIn = document.createElement('button');
const zoomAll = document.createElement('button'); const zoomAll = document.createElement('button');
const zeroVelocity = document.createElement('button');
this.div.appendChild(currentScale); this.div.appendChild(currentScale);
this.div.appendChild(zoomOut); this.div.appendChild(zoomOut);
this.div.appendChild(zoomIn); this.div.appendChild(zoomIn);
this.div.appendChild(zoomAll); this.div.appendChild(zoomAll);
this.div.appendChild(zeroVelocity);
zoomAll.classList.add(WIDE_CLASSNAME); zoomAll.classList.add(WIDE_CLASSNAME);
zeroVelocity.classList.add(WIDE_CLASSNAME);
currentScale.classList.add(WIDE_CLASSNAME); currentScale.classList.add(WIDE_CLASSNAME);
currentScale.classList.add(TOOL_INFO_CLASSNAME); currentScale.classList.add(TOOL_INFO_CLASSNAME);
zoomOut.innerHTML = 'Zoom<br>Out'; zoomOut.innerHTML = 'Zoom<br>Out';
zoomIn.innerHTML = 'Zoom<br>In'; zoomIn.innerHTML = 'Zoom<br>In';
zoomAll.innerHTML = 'Zoom to Fit'; zoomAll.innerHTML = 'Zoom to Fit';
zeroVelocity.innerHTML = 'Zero Momentum';
currentScale.innerHTML = this.displayScaleText; currentScale.innerHTML = this.displayScaleText;
this.sim.onZoom(() => { this.sim.onZoom(() => {
@ -88,41 +84,5 @@ export class Zoom extends Tool {
}; };
this.sim.scheduleZoom({x, y}, factor, netVelocity) 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;
});
} }
} }

View File

@ -6,22 +6,21 @@ import {Header} from './tool/header.js';
export class Toolbar { export class Toolbar {
sim = undefined; sim = undefined;
tools = []; tools = [];
expanded = undefined;
header = undefined;
constructor(sim, title, group) { constructor(sim, title, { expanded } = {}) {
this.sim = sim; this.sim = sim;
this.expanded = expanded ?? true;
// Create ourselves a div, as child of sim's div // Create ourselves a div, as child of sim's div
const div = document.createElement('div'); const div = document.createElement('div');
this.div = div; this.div = div;
if (group) {
group.addToolbar(this);
} else {
this.sim.div.appendChild(div);
}
div.classList.add(TOOLBAR_CLASSNAME); div.classList.add(TOOLBAR_CLASSNAME);
// Create a collapse/expand tool // Create a collapse/expand tool
const header = new Header(this, title); const header = new Header(this, title);
this.header = header;
this.addTool(header); this.addTool(header);
} }
@ -37,4 +36,19 @@ export class Toolbar {
tool.frame(); 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);
}
}
}
}
} }