zero total momentum; options tool

This commit is contained in:
Ladd 2025-12-27 02:14:57 -06:00
parent b41d6dd488
commit 3233052f63
8 changed files with 163 additions and 20 deletions

View File

@ -1,3 +1,4 @@
// DISPLAY
export const DISPLAY_OBJECTS_INFO = false; export const DISPLAY_OBJECTS_INFO = false;
export const DISPLAY_CURSOR_INFO = false; export const DISPLAY_CURSOR_INFO = false;
export const DISPLAY_CANVAS_SIZE = false; export const DISPLAY_CANVAS_SIZE = false;
@ -6,23 +7,23 @@ export const DISPLAY_CURRENT_MODE = false;
export const DISPLAY_VELOCITY_VECTORS = true; export const DISPLAY_VELOCITY_VECTORS = true;
export const DISPLAY_ACCELERATION_VECTORS = true; export const DISPLAY_ACCELERATION_VECTORS = true;
export const MASS_CREATION_RATE = 1E1; // VELOCITY
export const POINTER_HISTORY_SIZE = 15;
export const VELOCITY_VECTOR_SCALE = 5E0; export const VELOCITY_VECTOR_SCALE = 5E0;
export const VELOCITY_VECTOR_COLOR = 'rgb(150, 150, 150)'; // optionally set to 'object color' export const VELOCITY_VECTOR_COLOR = 'rgb(150, 150, 150)'; // optionally set to 'object color'
export const VELOCITY_VECTOR_WIDTH = 1.5; export const VELOCITY_VECTOR_WIDTH = 1.5;
export const VELOCITY_VECTOR_ARROWHEAD = true; export const VELOCITY_VECTOR_ARROWHEAD = true;
// ACCELERATION
export const ACCELERATION_VECTOR_SCALE = 5E0; export const ACCELERATION_VECTOR_SCALE = 5E0;
export const ACCELERATION_VECTOR_COLOR = 'rgb(0, 255, 0)'; // optionally set to 'object color' export const ACCELERATION_VECTOR_COLOR = 'rgb(0, 128, 0)'; // optionally set to 'object color'
// export const ACCELERATION_VECTOR_COLOR = 'object color';
export const ACCELERATION_VECTOR_WIDTH = 1.5; export const ACCELERATION_VECTOR_WIDTH = 1.5;
export const ACCELERATION_VECTOR_ARROWHEAD = true; export const ACCELERATION_VECTOR_ARROWHEAD = true;
// SCALING FACTORS
export const MASS_CREATION_RATE = 1E1;
export const POINTER_HISTORY_SIZE = 15;
export const MOTION_TIME_SCALE = 1E-4; export const MOTION_TIME_SCALE = 1E-4;
export const PAN_VELOCITY_SCALE_FACTOR = 1E-3; export const PAN_VELOCITY_SCALE_FACTOR = 1E-3;
export const ARROWHEAD_LENGTH = 7; export const ARROWHEAD_LENGTH = 7;
export const ARROWHEAD_WIDTH = 5; export const ARROWHEAD_WIDTH = 5;
export const OFFSCREEN_OBJECT_LINE_SCALE = 7; export const OFFSCREEN_OBJECT_LINE_SCALE = 7;
@ -34,7 +35,13 @@ export const SCALE_POWER_MAX = 8;
export const SCALE_POWER_MIN = -8; export const SCALE_POWER_MIN = -8;
export const GRAVITATIONAL_CONSTANT = 1E5; export const GRAVITATIONAL_CONSTANT = 1E5;
// CSS CLASS NAMES
export const DRAGGABLE_ELEMENT_CLASSNAME = 'lhg-draggable-element'; export const DRAGGABLE_ELEMENT_CLASSNAME = 'lhg-draggable-element';
// MODES
export const MODE_MASS_GENERATION = 'mass-gen'; export const MODE_MASS_GENERATION = 'mass-gen';
export const MODE_PAN_VIEW = 'pan-view'; export const MODE_PAN_VIEW = 'pan-view';
// OPTIONS
export const PAUSE_DURING_CREATION = true;
export const PAUSE_DURING_SELECTION = true;

View File

@ -8,7 +8,7 @@ body {
background-color: #000; background-color: #000;
color: #8f8; color: #8f8;
font-family: monospace; font-family: monospace;
font-size: 16pt; font-size: 14pt;
overflow: hidden; overflow: hidden;
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;

View File

@ -8,7 +8,6 @@ export class MassObject {
color = {r: undefined, g: undefined, b: undefined}; color = {r: undefined, g: undefined, b: undefined};
created = undefined; created = undefined;
forces = []; // [{x, y}] forces = []; // [{x, y}]
active = false;
constructor(x, y, index) { constructor(x, y, index) {
this.index = index; this.index = index;

View File

@ -10,24 +10,44 @@ export class Objects {
objects = []; objects = [];
creatingObject = undefined; creatingObject = undefined;
selectedObject = undefined; selectedObject = undefined;
paused = false;
constructor(sim) { constructor(sim) {
this.sim = sim; this.sim = sim;
} }
pause() {
if (this.sim.playing) {
this.sim.playing = false;
this.paused = true;
}
}
resume() {
if (this.paused) {
this.sim.playing = true;
this.paused = false;
}
}
// Create an object with mass that grows as pointer is held down // Create an object with mass that grows as pointer is held down
createObject(x, y) { createObject(x, y) {
const idx = this.objects.length; const idx = this.objects.length;
const obj = new MassObject(x, y, idx); const obj = new MassObject(x, y, idx);
this.creatingObject = idx; this.creatingObject = idx;
this.objects.push(obj); this.objects.push(obj);
// Pause the simulation during mass creation; this avoids some complex local dynamics
// TODO: Make this optional (toolbar item to enable)
if (this.sim.getOption('pauseDuring.creation')) {
this.pause();
}
} }
doneCreatingObject() { doneCreatingObject() {
if (this.creatingObject !== undefined) { if (this.creatingObject !== undefined) {
const obj = this.objects[this.creatingObject]; const obj = this.objects[this.creatingObject];
obj.active = true;
this.creatingObject = undefined; this.creatingObject = undefined;
this.resume();
} }
} }
@ -37,10 +57,14 @@ export class Objects {
selectObject(i) { selectObject(i) {
this.selectedObject = i; this.selectedObject = i;
if (this.sim.getOption('pauseDuring.selection')) {
this.pause();
}
} }
deselect() { deselect() {
this.selectedObject = undefined; this.selectedObject = undefined;
this.resume();
} }
getSelectedOrCreating() { getSelectedOrCreating() {
@ -123,7 +147,6 @@ export class Objects {
const A = this.objects[i]; const A = this.objects[i];
for (let j = i + 1; j < this.objects.length; j++) { for (let j = i + 1; j < this.objects.length; j++) {
const B = this.objects[j]; const B = this.objects[j];
if (!A.active || !B.active) continue;
const dx = (B.position.x - A.position.x); const dx = (B.position.x - A.position.x);
const dy = (B.position.y - A.position.y); const dy = (B.position.y - A.position.y);
const dSquared = dx ** 2 + dy ** 2; const dSquared = dx ** 2 + dy ** 2;
@ -135,6 +158,12 @@ export class Objects {
B.forces.push({ x: -Fx, y: -Fy }); B.forces.push({ x: -Fx, y: -Fy });
} }
} }
// Also compute acceleration
for (let i = 0; i < this.objects.length; i++) {
const obj = this.objects[i];
obj.acceleration = obj.getAcceleration();
}
} }
computeFrame(elapsedTime) { computeFrame(elapsedTime) {
@ -151,10 +180,11 @@ export class Objects {
this.computeForces(); this.computeForces();
if (this.sim.playing) { if (this.sim.playing) {
// Generate predicted positions (Velocity verlet method) // Predict positions (Velocity verlet method)
for (let i = 0; i < this.objects.length; i++) { for (let i = 0; i < this.objects.length; i++) {
const obj = this.objects[i]; const obj = this.objects[i];
obj.currentAcceleration = obj.getAcceleration(); obj.currentAcceleration = {...obj.acceleration};
obj.position.x += elapsedTime * obj.position.x += elapsedTime *
(obj.velocity.x + 1/2 * obj.currentAcceleration.x * elapsedTime); (obj.velocity.x + 1/2 * obj.currentAcceleration.x * elapsedTime);
obj.position.y += elapsedTime * obj.position.y += elapsedTime *
@ -164,10 +194,10 @@ export class Objects {
// Recompute forces // Recompute forces
this.computeForces(); this.computeForces();
// Generated predicted velocities // Predict velocities
for (let i = 0; i < this.objects.length; i++) { for (let i = 0; i < this.objects.length; i++) {
const obj = this.objects[i]; const obj = this.objects[i];
const acceleration = obj.getAcceleration(); const acceleration = {...obj.acceleration};
obj.acceleration = { obj.acceleration = {
x: (obj.currentAcceleration.x + acceleration.x) / 2, x: (obj.currentAcceleration.x + acceleration.x) / 2,
y: (obj.currentAcceleration.y + acceleration.y) / 2, y: (obj.currentAcceleration.y + acceleration.y) / 2,
@ -175,11 +205,8 @@ export class Objects {
obj.velocity.x += obj.acceleration.x * elapsedTime; obj.velocity.x += obj.acceleration.x * elapsedTime;
obj.velocity.y += obj.acceleration.y * elapsedTime; obj.velocity.y += obj.acceleration.y * elapsedTime;
} }
} else { // TODO: Collisions
for (let i = 0; i < this.objects.length; i++) {
const obj = this.objects[i];
obj.acceleration = obj.getAcceleration();
}
} }
// Display objects info // Display objects info

View File

@ -6,6 +6,7 @@ import { Toolbar } from './toolbar.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 { ModeSwitch } from './tool/modes.js'; import { ModeSwitch } from './tool/modes.js';
import { Options } from './tool/options.js';
import { import {
SCALE_POWER_MAX, SCALE_POWER_MAX,
SCALE_POWER_MIN, SCALE_POWER_MIN,
@ -25,6 +26,7 @@ export class Sim {
objects = undefined; objects = undefined;
isCurrentMode = () => false; isCurrentMode = () => false;
getOption = () => undefined;
init(divId) { init(divId) {
this.divId = divId; this.divId = divId;
@ -41,6 +43,7 @@ export class Sim {
this.toolbar.addTool(new Zoom(this.toolbar)); this.toolbar.addTool(new Zoom(this.toolbar));
this.toolbar.addTool(new PlayPause(this.toolbar)); this.toolbar.addTool(new PlayPause(this.toolbar));
this.toolbar.addTool(new ModeSwitch(this.toolbar)); this.toolbar.addTool(new ModeSwitch(this.toolbar));
this.toolbar.addTool(new Options(this.toolbar));
// Initiate main loop // Initiate main loop
this.time = document.timeline.currentTime; this.time = document.timeline.currentTime;

75
tool/options.js Normal file
View File

@ -0,0 +1,75 @@
// Options picker
import { Tool } from '../tool.js';
import {
MODE_PAN_VIEW,
PAUSE_DURING_CREATION,
PAUSE_DURING_SELECTION,
} from '../config.js';
export class Options extends Tool {
options = [{
type: 'group', name: 'pauseDuring', title: 'Pause During',
items: [
{ type: 'boolean', name: 'creation', title: 'Creation', default: PAUSE_DURING_CREATION },
{ type: 'boolean', name: 'selection', title: 'Selection', default: PAUSE_DURING_SELECTION },
]
}];
values = {};
visitItem(item, path) {
path = [path, item.name].filter(x => !!x).join('.');
switch (item.type) {
case 'group': {
const group = document.createElement('div');
const heading = document.createElement('h3');
heading.innerHTML = item.title;
group.appendChild(heading);
for (const next of item.items) {
const child = this.visitItem(next, path);
group.appendChild(child);
}
return group;
}
case 'boolean': {
const button = document.createElement('button');
button.innerHTML = `<h4>${item.title}</h4>`;
this.setOption(path, item.default);
button.style.opacity = this.values[path] ? '100%' : '50%';
button.addEventListener('click', (e) => {
this.setOption(path, !this.getOption(path));
button.style.opacity = this.values[path] ? '100%' : '50%';
});
return button;
break;
}
default:
throw new Error('unknown option type');
}
}
constructor(toolbar) {
super(toolbar);
const heading = document.createElement('h2');
heading.innerHTML = 'Options';
this.div.appendChild(heading);
for (const item of this.options) {
const child = this.visitItem(item);
this.div.appendChild(child);
}
// Global methods to get/set current option values
this.sim.getOption = (path) => this.getOption(path);
this.sim.setOption = (path, value) => this.setOption(path, value);
}
getOption(path) {
const val = this.values[path];
return val;
}
setOption(path, value) {
this.values[path] = value;
// TODO Update button opacity in case this is called from elsewhere
}
}

View File

@ -10,8 +10,8 @@ export class PlayPause extends Tool {
const pauseButton = document.createElement('button'); const pauseButton = document.createElement('button');
const playButton = document.createElement('button'); const playButton = document.createElement('button');
this.div.appendChild(pauseButton);
this.div.appendChild(playButton); this.div.appendChild(playButton);
this.div.appendChild(pauseButton);
pauseButton.innerHTML = this.pauseHTML; pauseButton.innerHTML = this.pauseHTML;
playButton.innerHTML = this.playHTML; playButton.innerHTML = this.playHTML;

View File

@ -11,17 +11,22 @@ 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 zeroNetMomentum = document.createElement('button');
this.div.appendChild(zoomOut); this.div.appendChild(zoomOut);
this.div.appendChild(zoomIn); this.div.appendChild(zoomIn);
this.div.appendChild(document.createElement('br')); this.div.appendChild(document.createElement('br'));
this.div.appendChild(zoomAll); this.div.appendChild(zoomAll);
this.div.appendChild(document.createElement('br'));
this.div.appendChild(zeroNetMomentum);
zoomAll.classList.add('wide'); zoomAll.classList.add('wide');
zeroNetMomentum.classList.add('wide');
zoomOut.innerHTML = '<h2>Zoom<br>Out</h2>'; zoomOut.innerHTML = '<h2>Zoom<br>Out</h2>';
zoomIn.innerHTML = '<h2>Zoom<br>In</h2>'; zoomIn.innerHTML = '<h2>Zoom<br>In</h2>';
zoomAll.innerHTML = '<h2>Zoom to Fit</h2>'; zoomAll.innerHTML = '<h2>Zoom to Fit</h2>';
zeroNetMomentum.innerHTML = '<h2>Zero Net Momentum</h2>';
zoomOut.addEventListener('click', (e) => { zoomOut.addEventListener('click', (e) => {
// Aim at center of view // Aim at center of view
@ -55,5 +60,32 @@ export class Zoom extends Tool {
} }
} }
}); });
zeroNetMomentum.addEventListener('click', (e) => {
const { objects } = this.sim.objects;
// Find total momentum
let totalMomentum = objects.reduce((total, obj) => {
const px = obj.mass * obj.velocity.x;
const py = obj.mass * obj.velocity.y;
return {
x: total.x + px,
y: total.y + py,
};
}, {x: 0, y: 0});
// Find average momentum
let averageMomentum = {
x: totalMomentum.x / objects.length,
y: totalMomentum.y / objects.length,
};
// Subtract average from each
for (let i = 0; i < objects.length; i++) {
const obj = objects[i];
obj.velocity.x -= averageMomentum.x / obj.mass;
obj.velocity.y -= averageMomentum.y / obj.mass;
}
});
} }
} }