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_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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
49
objects.js
49
objects.js
@ -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
|
||||||
|
|||||||
@ -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
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 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;
|
||||||
|
|||||||
32
tool/zoom.js
32
tool/zoom.js
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user