zero total momentum; options tool
This commit is contained in:
parent
b41d6dd488
commit
3233052f63
19
config.js
19
config.js
@ -1,3 +1,4 @@
|
||||
// DISPLAY
|
||||
export const DISPLAY_OBJECTS_INFO = false;
|
||||
export const DISPLAY_CURSOR_INFO = 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_ACCELERATION_VECTORS = true;
|
||||
|
||||
export const MASS_CREATION_RATE = 1E1;
|
||||
export const POINTER_HISTORY_SIZE = 15;
|
||||
|
||||
// VELOCITY
|
||||
export const VELOCITY_VECTOR_SCALE = 5E0;
|
||||
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_ARROWHEAD = true;
|
||||
|
||||
// ACCELERATION
|
||||
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 = 'object color';
|
||||
export const ACCELERATION_VECTOR_COLOR = 'rgb(0, 128, 0)'; // optionally set to 'object color'
|
||||
export const ACCELERATION_VECTOR_WIDTH = 1.5;
|
||||
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 PAN_VELOCITY_SCALE_FACTOR = 1E-3;
|
||||
|
||||
export const ARROWHEAD_LENGTH = 7;
|
||||
export const ARROWHEAD_WIDTH = 5;
|
||||
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 GRAVITATIONAL_CONSTANT = 1E5;
|
||||
|
||||
// CSS CLASS NAMES
|
||||
export const DRAGGABLE_ELEMENT_CLASSNAME = 'lhg-draggable-element';
|
||||
|
||||
// MODES
|
||||
export const MODE_MASS_GENERATION = 'mass-gen';
|
||||
export const MODE_PAN_VIEW = 'pan-view';
|
||||
|
||||
// OPTIONS
|
||||
export const PAUSE_DURING_CREATION = true;
|
||||
export const PAUSE_DURING_SELECTION = true;
|
||||
|
||||
@ -8,7 +8,7 @@ body {
|
||||
background-color: #000;
|
||||
color: #8f8;
|
||||
font-family: monospace;
|
||||
font-size: 16pt;
|
||||
font-size: 14pt;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
@ -8,7 +8,6 @@ export class MassObject {
|
||||
color = {r: undefined, g: undefined, b: undefined};
|
||||
created = undefined;
|
||||
forces = []; // [{x, y}]
|
||||
active = false;
|
||||
|
||||
constructor(x, y, index) {
|
||||
this.index = index;
|
||||
|
||||
49
objects.js
49
objects.js
@ -10,24 +10,44 @@ export class Objects {
|
||||
objects = [];
|
||||
creatingObject = undefined;
|
||||
selectedObject = undefined;
|
||||
paused = false;
|
||||
|
||||
constructor(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
|
||||
createObject(x, y) {
|
||||
const idx = this.objects.length;
|
||||
const obj = new MassObject(x, y, idx);
|
||||
this.creatingObject = idx;
|
||||
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() {
|
||||
if (this.creatingObject !== undefined) {
|
||||
const obj = this.objects[this.creatingObject];
|
||||
obj.active = true;
|
||||
this.creatingObject = undefined;
|
||||
this.resume();
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,10 +57,14 @@ export class Objects {
|
||||
|
||||
selectObject(i) {
|
||||
this.selectedObject = i;
|
||||
if (this.sim.getOption('pauseDuring.selection')) {
|
||||
this.pause();
|
||||
}
|
||||
}
|
||||
|
||||
deselect() {
|
||||
this.selectedObject = undefined;
|
||||
this.resume();
|
||||
}
|
||||
|
||||
getSelectedOrCreating() {
|
||||
@ -123,7 +147,6 @@ export class Objects {
|
||||
const A = this.objects[i];
|
||||
for (let j = i + 1; j < this.objects.length; j++) {
|
||||
const B = this.objects[j];
|
||||
if (!A.active || !B.active) continue;
|
||||
const dx = (B.position.x - A.position.x);
|
||||
const dy = (B.position.y - A.position.y);
|
||||
const dSquared = dx ** 2 + dy ** 2;
|
||||
@ -135,6 +158,12 @@ export class Objects {
|
||||
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) {
|
||||
@ -151,10 +180,11 @@ export class Objects {
|
||||
this.computeForces();
|
||||
|
||||
if (this.sim.playing) {
|
||||
// Generate predicted positions (Velocity verlet method)
|
||||
// Predict positions (Velocity verlet method)
|
||||
for (let i = 0; i < this.objects.length; i++) {
|
||||
const obj = this.objects[i];
|
||||
obj.currentAcceleration = obj.getAcceleration();
|
||||
obj.currentAcceleration = {...obj.acceleration};
|
||||
|
||||
obj.position.x += elapsedTime *
|
||||
(obj.velocity.x + 1/2 * obj.currentAcceleration.x * elapsedTime);
|
||||
obj.position.y += elapsedTime *
|
||||
@ -164,10 +194,10 @@ export class Objects {
|
||||
// Recompute forces
|
||||
this.computeForces();
|
||||
|
||||
// Generated predicted velocities
|
||||
// Predict velocities
|
||||
for (let i = 0; i < this.objects.length; i++) {
|
||||
const obj = this.objects[i];
|
||||
const acceleration = obj.getAcceleration();
|
||||
const acceleration = {...obj.acceleration};
|
||||
obj.acceleration = {
|
||||
x: (obj.currentAcceleration.x + acceleration.x) / 2,
|
||||
y: (obj.currentAcceleration.y + acceleration.y) / 2,
|
||||
@ -175,11 +205,8 @@ export class Objects {
|
||||
obj.velocity.x += obj.acceleration.x * elapsedTime;
|
||||
obj.velocity.y += obj.acceleration.y * elapsedTime;
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < this.objects.length; i++) {
|
||||
const obj = this.objects[i];
|
||||
obj.acceleration = obj.getAcceleration();
|
||||
}
|
||||
// TODO: Collisions
|
||||
|
||||
}
|
||||
|
||||
// Display objects info
|
||||
|
||||
@ -6,6 +6,7 @@ import { Toolbar } from './toolbar.js';
|
||||
import { PlayPause } from './tool/play-pause.js';
|
||||
import { Zoom } from './tool/zoom.js';
|
||||
import { ModeSwitch } from './tool/modes.js';
|
||||
import { Options } from './tool/options.js';
|
||||
import {
|
||||
SCALE_POWER_MAX,
|
||||
SCALE_POWER_MIN,
|
||||
@ -25,6 +26,7 @@ export class Sim {
|
||||
objects = undefined;
|
||||
|
||||
isCurrentMode = () => false;
|
||||
getOption = () => undefined;
|
||||
|
||||
init(divId) {
|
||||
this.divId = divId;
|
||||
@ -41,6 +43,7 @@ export class Sim {
|
||||
this.toolbar.addTool(new Zoom(this.toolbar));
|
||||
this.toolbar.addTool(new PlayPause(this.toolbar));
|
||||
this.toolbar.addTool(new ModeSwitch(this.toolbar));
|
||||
this.toolbar.addTool(new Options(this.toolbar));
|
||||
|
||||
// Initiate main loop
|
||||
this.time = document.timeline.currentTime;
|
||||
|
||||
75
tool/options.js
Normal file
75
tool/options.js
Normal 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
|
||||
}
|
||||
}
|
||||
@ -10,8 +10,8 @@ export class PlayPause extends Tool {
|
||||
const pauseButton = document.createElement('button');
|
||||
const playButton = document.createElement('button');
|
||||
|
||||
this.div.appendChild(pauseButton);
|
||||
this.div.appendChild(playButton);
|
||||
this.div.appendChild(pauseButton);
|
||||
|
||||
pauseButton.innerHTML = this.pauseHTML;
|
||||
playButton.innerHTML = this.playHTML;
|
||||
|
||||
32
tool/zoom.js
32
tool/zoom.js
@ -11,17 +11,22 @@ export class Zoom extends Tool {
|
||||
const zoomOut = document.createElement('button');
|
||||
const zoomIn = document.createElement('button');
|
||||
const zoomAll = document.createElement('button');
|
||||
const zeroNetMomentum = document.createElement('button');
|
||||
|
||||
this.div.appendChild(zoomOut);
|
||||
this.div.appendChild(zoomIn);
|
||||
this.div.appendChild(document.createElement('br'));
|
||||
this.div.appendChild(zoomAll);
|
||||
this.div.appendChild(document.createElement('br'));
|
||||
this.div.appendChild(zeroNetMomentum);
|
||||
|
||||
zoomAll.classList.add('wide');
|
||||
zeroNetMomentum.classList.add('wide');
|
||||
|
||||
zoomOut.innerHTML = '<h2>Zoom<br>Out</h2>';
|
||||
zoomIn.innerHTML = '<h2>Zoom<br>In</h2>';
|
||||
zoomAll.innerHTML = '<h2>Zoom to Fit</h2>';
|
||||
zeroNetMomentum.innerHTML = '<h2>Zero Net Momentum</h2>';
|
||||
|
||||
zoomOut.addEventListener('click', (e) => {
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user