Merge branch 'dev'
@ -8,11 +8,11 @@ Uses `npm` for `eslint`.
|
|||||||
Screenshots
|
Screenshots
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export const PATH_TRACES_WIDTH = 1.5;
|
|||||||
export const PATH_TRACES_DASHED_OPACITY = 1.0;
|
export const PATH_TRACES_DASHED_OPACITY = 1.0;
|
||||||
|
|
||||||
// PANNING
|
// PANNING
|
||||||
export const PANNING_ZERO_TOUCH_THRESHOLD = 200;
|
export const PANNING_ZERO_TOUCH_THRESHOLD = 200; // Milliseconds
|
||||||
|
|
||||||
// SIZES
|
// SIZES
|
||||||
export const POINTER_HISTORY_SIZE = 20;
|
export const POINTER_HISTORY_SIZE = 20;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 152 KiB |
11
object.js
@ -16,7 +16,7 @@ import {
|
|||||||
VELOCITY_VECTOR_COLOR,
|
VELOCITY_VECTOR_COLOR,
|
||||||
VELOCITY_VECTOR_WIDTH,
|
VELOCITY_VECTOR_WIDTH,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
import {add, components, copy, direction, div, magnitude, zero} from './vector.js';
|
import {add, components, copy, direction, div, magnitude, mult, square, zero} from './vector.js';
|
||||||
|
|
||||||
export class MassObject {
|
export class MassObject {
|
||||||
sim = undefined;
|
sim = undefined;
|
||||||
@ -31,7 +31,6 @@ export class MassObject {
|
|||||||
forces = []; // [{x, y}]
|
forces = []; // [{x, y}]
|
||||||
history = [];
|
history = [];
|
||||||
alive = true;
|
alive = true;
|
||||||
kineticEnergy = 0;
|
|
||||||
workDoneByPointer = 0;
|
workDoneByPointer = 0;
|
||||||
workDoneByForces = 0;
|
workDoneByForces = 0;
|
||||||
|
|
||||||
@ -92,6 +91,14 @@ export class MassObject {
|
|||||||
return div(netForce, this.mass);
|
return div(netForce, this.mass);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get kineticEnergy() {
|
||||||
|
return this.mass * square(this.velocity) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
get momentum() {
|
||||||
|
return mult(this.mass, this.velocity);
|
||||||
|
}
|
||||||
|
|
||||||
drawPath(display) {
|
drawPath(display) {
|
||||||
const {ctx} = display;
|
const {ctx} = display;
|
||||||
const {color: {r, g, b}} = this;
|
const {color: {r, g, b}} = this;
|
||||||
|
|||||||
@ -69,8 +69,6 @@ export class Panning {
|
|||||||
// Additional scaling factor
|
// Additional scaling factor
|
||||||
velocity = mult(velocity, this.sim.getOption('display.panningSpeed'));
|
velocity = mult(velocity, this.sim.getOption('display.panningSpeed'));
|
||||||
|
|
||||||
// TODO: Make it easier to slow down the camera
|
|
||||||
|
|
||||||
// Add pointer velocity to current panning velocity
|
// Add pointer velocity to current panning velocity
|
||||||
this.velocity = add(this.velocity, velocity);
|
this.velocity = add(this.velocity, velocity);
|
||||||
}
|
}
|
||||||
@ -82,10 +80,6 @@ export class Panning {
|
|||||||
this.velocity = zero;
|
this.velocity = zero;
|
||||||
}
|
}
|
||||||
this.touchStart = undefined;
|
this.touchStart = undefined;
|
||||||
|
|
||||||
if (this.sim.getOption('compensate.fastPanning')) {
|
|
||||||
this.velocity = zero;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -53,7 +53,12 @@ export class Pointer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePointerDown({x: clientX, y: clientY}) {
|
handlePointerDown({x: clientX, y: clientY, touches}) {
|
||||||
|
// TODO: Check if pointer was already down;
|
||||||
|
// equivalently, e.touches.length > 1
|
||||||
|
|
||||||
|
// Global pinch to zoom? Or only in certain modes?
|
||||||
|
|
||||||
this.updatePointer({x: clientX, y: clientY});
|
this.updatePointer({x: clientX, y: clientY});
|
||||||
|
|
||||||
switch (this.sim.getCurrentMode()) {
|
switch (this.sim.getCurrentMode()) {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
BIN
screenshots/gravity-simulator-5.png
Normal file
|
After Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
@ -10,18 +10,19 @@ export const simOptions = {
|
|||||||
traces: ['Path Traces', 'boolean', true],
|
traces: ['Path Traces', 'boolean', true],
|
||||||
dashedTraces: ['Dashed', 'boolean', false, {tall: true, showIf: 'display.traces'}],
|
dashedTraces: ['Dashed', 'boolean', false, {tall: true, showIf: 'display.traces'}],
|
||||||
velocityScale: ['Velocity<br>Vec Scale', 'number', 80, {showIf: 'display.velocity'}],
|
velocityScale: ['Velocity<br>Vec Scale', 'number', 80, {showIf: 'display.velocity'}],
|
||||||
accelerationScale: ['Accel<br>Vec Scale', 'number', 800, {showIf: 'display.acceleration'}],
|
accelerationScale: ['Accel<br>Vec Scale', 'number', 80, {showIf: 'display.acceleration'}],
|
||||||
zoomVectors: ['Zoom Vectors', 'boolean', true],
|
zoomVectors: ['Zoom Vectors', 'boolean', true],
|
||||||
panningSpeed: ['Pan<br>Speed', 'number', 0.1],
|
panningSpeed: ['Pan<br>Speed', 'number', 0.1],
|
||||||
},
|
},
|
||||||
compensate: {
|
compensate: {
|
||||||
timeScale: ['Time Scale Compensator', 'boolean', false, {wide: true}],
|
timeScale: ['Time Scale Compensator', 'boolean', false, {wide: true}],
|
||||||
fastPanning: ['Fast Panning', 'boolean', false],
|
|
||||||
},
|
},
|
||||||
param: {
|
param: {
|
||||||
gravity: ['Gravity', 'number', 1],
|
gravity: ['Gravity', 'number', 1],
|
||||||
timeScale: ['Time Scale', 'number', 0.1],
|
timeScale: ['Time Scale', 'number', 0.1],
|
||||||
massCreationRate: ['Mass Creation Rate', 'number', 1],
|
massCreationRate: ['Mass Creation Rate', 'number', 1],
|
||||||
|
elasticity: ['Elasticity', 'number', 0.7, {min: 0, max: 1}],
|
||||||
|
mergeThreshold: ['Merge Threshold', 'number', 0.1, {min: 0, max: 1}],
|
||||||
},
|
},
|
||||||
debug: {
|
debug: {
|
||||||
objectsInfo: ['Objects Info', 'boolean', false],
|
objectsInfo: ['Objects Info', 'boolean', false],
|
||||||
|
|||||||
@ -51,6 +51,8 @@ export class Sim {
|
|||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
if (url.host === 'gravity.dev.laddhoffman.com') {
|
if (url.host === 'gravity.dev.laddhoffman.com') {
|
||||||
document.title += ' [Dev]';
|
document.title += ' [Dev]';
|
||||||
|
} else if (url.host === 'gravity.local') {
|
||||||
|
document.title += ' [Local]';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.options = new Options(this, simOptions);
|
this.options = new Options(this, simOptions);
|
||||||
@ -109,6 +111,7 @@ export class Sim {
|
|||||||
requestAnimationFrame(t => this.frame(t));
|
requestAnimationFrame(t => this.frame(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record frame timestamp information
|
||||||
markFrame(t) {
|
markFrame(t) {
|
||||||
const {recentFrames: rfs} = this;
|
const {recentFrames: rfs} = this;
|
||||||
if (!rfs.length) {
|
if (!rfs.length) {
|
||||||
@ -133,6 +136,7 @@ export class Sim {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toJSON is used to export the state of the simulation
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
dateSaved: new Date().toISOString(),
|
dateSaved: new Date().toISOString(),
|
||||||
@ -145,6 +149,7 @@ export class Sim {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fromJSON is used to import the state of the simulation
|
||||||
fromJSON(state) {
|
fromJSON(state) {
|
||||||
this.pause();
|
this.pause();
|
||||||
this.system.fromJSON(state.system);
|
this.system.fromJSON(state.system);
|
||||||
@ -165,6 +170,7 @@ export class Sim {
|
|||||||
return this.display.screenToSim(x, y);
|
return this.display.screenToSim(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start the simulation if it's not already running
|
||||||
play() {
|
play() {
|
||||||
this.playing = true;
|
this.playing = true;
|
||||||
|
|
||||||
@ -176,6 +182,7 @@ export class Sim {
|
|||||||
this.div.dispatchEvent(e);
|
this.div.dispatchEvent(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pause the simulation if it's currently running
|
||||||
pause() {
|
pause() {
|
||||||
this.playing = false;
|
this.playing = false;
|
||||||
|
|
||||||
@ -187,6 +194,7 @@ export class Sim {
|
|||||||
this.div.dispatchEvent(e);
|
this.div.dispatchEvent(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate the display content representing the current scale of the simulation
|
||||||
getScaleDisplay() {
|
getScaleDisplay() {
|
||||||
const scale = 2 ** Math.abs(this.display.scalePower);
|
const scale = 2 ** Math.abs(this.display.scalePower);
|
||||||
const scaleText = this.display.scalePower >= 0 ? `${scale}` : `1/${scale}`;
|
const scaleText = this.display.scalePower >= 0 ? `${scale}` : `1/${scale}`;
|
||||||
|
|||||||
201
system.js
@ -72,6 +72,15 @@ export class System {
|
|||||||
// TODO: Calculate work done by pointer here?
|
// TODO: Calculate work done by pointer here?
|
||||||
// Either interpolate the acceleration and use m*a, or
|
// Either interpolate the acceleration and use m*a, or
|
||||||
// measure the change in the object's kinetic energy
|
// measure the change in the object's kinetic energy
|
||||||
|
// Or both!
|
||||||
|
// In the frame loop, for the selected object we already calculate
|
||||||
|
// the work done by the pointer in preventing the acceleration the object
|
||||||
|
// would have experienced.
|
||||||
|
// But that doesn't capture the work done by the pointer moving the object.
|
||||||
|
// If the sim is paused, the dot(force, displacement) method loses accuracy.
|
||||||
|
// If the sim is paused though, then channge in kinetic energy will be easy to measure.
|
||||||
|
// If the sim is not paused, we can use dot(force, displacement) method.
|
||||||
|
|
||||||
obj.position = add(start, delta);
|
obj.position = add(start, delta);
|
||||||
obj.velocity = zero;
|
obj.velocity = zero;
|
||||||
}
|
}
|
||||||
@ -111,54 +120,6 @@ export class System {
|
|||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Collisions
|
|
||||||
this.forEachObject((A, i) => {
|
|
||||||
this.forEachObject((B, j) => {
|
|
||||||
const dx = (B.position.x - A.position.x);
|
|
||||||
const dy = (B.position.y - A.position.y);
|
|
||||||
const dSquared = dx ** 2 + dy ** 2;
|
|
||||||
const d = Math.sqrt(dSquared);
|
|
||||||
if (d < A.radius + B.radius) {
|
|
||||||
let S, T;
|
|
||||||
// Merge the older into the newer, in order to provide mass creation rate continuity
|
|
||||||
if (A.age > B.age) {
|
|
||||||
// A merges into B; B survives
|
|
||||||
S = B;
|
|
||||||
T = A;
|
|
||||||
// If A was selected or being created, select S instead
|
|
||||||
if (this.creatingObject === i) this.creatingObject = j;
|
|
||||||
if (this.selectedObject === i) this.selectedObject = j;
|
|
||||||
} else {
|
|
||||||
// B merges into A; A survives
|
|
||||||
S = A;
|
|
||||||
T = B;
|
|
||||||
// If B was selected or being created, select S instead
|
|
||||||
if (this.creatingObject === j) this.creatingObject = i;
|
|
||||||
if (this.selectedObject === j) this.selectedObject = i;
|
|
||||||
}
|
|
||||||
// Merge T into S:
|
|
||||||
// Set position = center of mass
|
|
||||||
// Set velocity = total momentum / total mass
|
|
||||||
// Combine forces
|
|
||||||
// Add masses
|
|
||||||
// Average color
|
|
||||||
S.position = weightedAvg([[S.position, S.mass], [T.position, T.mass]]);
|
|
||||||
S.velocity = weightedAvg([[S.velocity, S.mass], [T.velocity, T.mass]]);
|
|
||||||
S.forces.push(...T.forces);
|
|
||||||
S.mass += T.mass;
|
|
||||||
S.color = {
|
|
||||||
r: (S.mass * S.color.r + T.mass * T.color.r) / (S.mass + T.mass),
|
|
||||||
g: (S.mass * S.color.g + T.mass * T.color.g) / (S.mass + T.mass),
|
|
||||||
b: (S.mass * S.color.b + T.mass * T.color.b) / (S.mass + T.mass),
|
|
||||||
};
|
|
||||||
T.alive = false;
|
|
||||||
T.forces = [];
|
|
||||||
const e = new CustomEvent(EVENT_OBJECT_MERGE, {detail: {surviving: S, merged: T}});
|
|
||||||
this.sim.div.dispatchEvent(e);
|
|
||||||
}
|
|
||||||
}, {alive: true, startWith: i + 1});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Recompute forces
|
// Recompute forces
|
||||||
this.computeForces();
|
this.computeForces();
|
||||||
|
|
||||||
@ -189,8 +150,41 @@ export class System {
|
|||||||
obj.history.shift();
|
obj.history.shift();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Collisions
|
||||||
|
this.forEachObject((A, i) => {
|
||||||
|
this.forEachObject((B, j) => {
|
||||||
|
if (this.objectsOverlap(A, B)) {
|
||||||
|
// this.mergeObjects(i, j);
|
||||||
|
this.bounceObjects(i, j);
|
||||||
|
}
|
||||||
|
}, {alive: true, startWith: i + 1});
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Display total energy
|
||||||
|
const totalKE = this.reduce((total, obj) => {
|
||||||
|
const energy = obj.mass * square(obj.velocity) / 2;
|
||||||
|
return total + energy;
|
||||||
|
}, 0, {alive: true});
|
||||||
|
|
||||||
|
const G = this.sim.getOption('param.gravity');
|
||||||
|
|
||||||
|
const totalGPE = this.reduce((total, A, i) => {
|
||||||
|
return total + this.reduce((objTotal, B) => {
|
||||||
|
const energy = -G * A.mass * B.mass / magnitude(sub(A.position, B.position));
|
||||||
|
return objTotal + energy;
|
||||||
|
}, 0, {alive: true, startWith: i + 1});
|
||||||
|
}, 0, {alive: true});
|
||||||
|
|
||||||
|
const netMomentum = this.reduce((net, obj) => add(net, obj.momentum), zero);
|
||||||
|
|
||||||
|
this.sim.info['Total E'] = (totalKE + totalGPE).toExponential(2);
|
||||||
|
this.sim.info['Total K'] = totalKE.toExponential(2);
|
||||||
|
this.sim.info['Total U<sub>g</sub>'] = totalGPE.toExponential(2);
|
||||||
|
this.sim.info['Net Momentum'] = magnitude(netMomentum).toExponential(2);
|
||||||
|
|
||||||
// Display objects info
|
// Display objects info
|
||||||
// First clear info from previous frame
|
// First clear info from previous frame
|
||||||
this.forEachObject((_obj, i) => {
|
this.forEachObject((_obj, i) => {
|
||||||
@ -395,6 +389,107 @@ export class System {
|
|||||||
return objects;
|
return objects;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
objectsOverlap(A, B) {
|
||||||
|
const dx = (B.position.x - A.position.x);
|
||||||
|
const dy = (B.position.y - A.position.y);
|
||||||
|
const dSquared = dx ** 2 + dy ** 2;
|
||||||
|
const d = Math.sqrt(dSquared);
|
||||||
|
return d < A.radius + B.radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
bounceObjects(i, j) {
|
||||||
|
const A = this.object(i);
|
||||||
|
const B = this.object(j);
|
||||||
|
const totalMass = A.mass + B.mass;
|
||||||
|
|
||||||
|
// TODO: Handle scenario where an object overlaps more than one other object
|
||||||
|
|
||||||
|
const autoMerge = true;
|
||||||
|
|
||||||
|
const Z = this.sim.getOption('param.elasticity'); // Elasticity: 0 = inelastic, 1 = elastic
|
||||||
|
const r = sub(A.position, B.position);
|
||||||
|
const normal = div(r, magnitude(r));
|
||||||
|
const tangent = {x: -normal.y, y: normal.x};
|
||||||
|
const vAt = dot(A.velocity, tangent);
|
||||||
|
|
||||||
|
// Require that objects are moving toward each other!
|
||||||
|
// Normal is directed toward A, so they are moving toward each other if vBn > vAn
|
||||||
|
const vAn = dot(A.velocity, normal);
|
||||||
|
const vBn = dot(B.velocity, normal);
|
||||||
|
if (vAn >= vBn) {
|
||||||
|
// The objects are already moving away from each other
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Are these objects sticking together?
|
||||||
|
// One way to determine this is,
|
||||||
|
// does either object have enough KE to escape their gravitational potential?
|
||||||
|
if (autoMerge) {
|
||||||
|
// const vSquared = Math.abs(vAn - vBn) ** 2;
|
||||||
|
const vSquared = square(sub(A.velocity, B.velocity));
|
||||||
|
const G = this.sim.getOption('param.gravity');
|
||||||
|
const mergeThreshold = this.sim.getOption('param.mergeThreshold');
|
||||||
|
if (vSquared < mergeThreshold * G * totalMass / magnitude(r)) {
|
||||||
|
// Neither object looks like it can escape!
|
||||||
|
this.mergeObjects(i, j);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the collision
|
||||||
|
const Zscaled = Math.sqrt(Z);
|
||||||
|
const vAnNew = (vAn * (A.mass - Zscaled * B.mass) + vBn * (1 + Zscaled) * B.mass) / totalMass;
|
||||||
|
|
||||||
|
const vA = add(mult(tangent, vAt), mult(normal, vAnNew));
|
||||||
|
const vB = div(add(mult(A.velocity, A.mass), mult(B.velocity, B.mass), mult(vA, -A.mass)), B.mass);
|
||||||
|
|
||||||
|
// const Ki = A.kineticEnergy + B.kineticEnergy;
|
||||||
|
A.velocity = vA;
|
||||||
|
B.velocity = vB;
|
||||||
|
// const Kf = A.kineticEnergy + B.kineticEnergy;
|
||||||
|
// console.log('Collision: Zscaled', Zscaled, 'Energy before', Ki, 'Energy after', Kf, 'Fraction change', (Kf - Ki) / Ki);
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeObjects(i, j) {
|
||||||
|
const A = this.object(i);
|
||||||
|
const B = this.object(j);
|
||||||
|
let S, T;
|
||||||
|
// Merge the older into the newer, in order to provide mass creation rate continuity
|
||||||
|
if (A.age > B.age) {
|
||||||
|
// A merges into B; B survives
|
||||||
|
S = B;
|
||||||
|
T = A;
|
||||||
|
// If A was selected or being created, select S instead
|
||||||
|
if (this.creatingObject === i) this.creatingObject = j;
|
||||||
|
if (this.selectedObject === i) this.selectedObject = j;
|
||||||
|
} else {
|
||||||
|
// B merges into A; A survives
|
||||||
|
S = A;
|
||||||
|
T = B;
|
||||||
|
// If B was selected or being created, select S instead
|
||||||
|
if (this.creatingObject === j) this.creatingObject = i;
|
||||||
|
if (this.selectedObject === j) this.selectedObject = i;
|
||||||
|
}
|
||||||
|
// Merge T into S:
|
||||||
|
// Set position = center of mass
|
||||||
|
// Set velocity = total momentum / total mass
|
||||||
|
// Combine forces
|
||||||
|
// Add masses
|
||||||
|
// Average color
|
||||||
|
S.position = weightedAvg([[S.position, S.mass], [T.position, T.mass]]);
|
||||||
|
S.velocity = weightedAvg([[S.velocity, S.mass], [T.velocity, T.mass]]);
|
||||||
|
S.mass += T.mass;
|
||||||
|
S.color = {
|
||||||
|
r: (S.mass * S.color.r + T.mass * T.color.r) / (S.mass + T.mass),
|
||||||
|
g: (S.mass * S.color.g + T.mass * T.color.g) / (S.mass + T.mass),
|
||||||
|
b: (S.mass * S.color.b + T.mass * T.color.b) / (S.mass + T.mass),
|
||||||
|
};
|
||||||
|
T.alive = false;
|
||||||
|
T.forces = [];
|
||||||
|
const e = new CustomEvent(EVENT_OBJECT_MERGE, {detail: {surviving: S, merged: T}});
|
||||||
|
this.sim.div.dispatchEvent(e);
|
||||||
|
}
|
||||||
|
|
||||||
computeForces() {
|
computeForces() {
|
||||||
const gravity = this.sim.getOption('param.gravity');
|
const gravity = this.sim.getOption('param.gravity');
|
||||||
if (this.objects.length < 2) return;
|
if (this.objects.length < 2) return;
|
||||||
@ -406,6 +501,11 @@ export class System {
|
|||||||
const r = sub(B.position, A.position);
|
const r = sub(B.position, A.position);
|
||||||
const dSquared = square(r);
|
const dSquared = square(r);
|
||||||
const d = Math.sqrt(dSquared);
|
const d = Math.sqrt(dSquared);
|
||||||
|
// If the objects are overlapping, don't apply gravity;
|
||||||
|
// This should give them a chance to fully rebound, and avoid accidental capture.
|
||||||
|
// if (d <= A.radius + B.radius) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
const F = gravity * A.mass * B.mass / dSquared;
|
const F = gravity * A.mass * B.mass / dSquared;
|
||||||
const Fx = F * r.x / d;
|
const Fx = F * r.x / d;
|
||||||
const Fy = F * r.y / d;
|
const Fy = F * r.y / d;
|
||||||
@ -423,9 +523,6 @@ export class System {
|
|||||||
this.forEachObject(obj => {
|
this.forEachObject(obj => {
|
||||||
// Acceleration
|
// Acceleration
|
||||||
obj.acceleration = obj.getAcceleration();
|
obj.acceleration = obj.getAcceleration();
|
||||||
|
|
||||||
// Kinetic Energy
|
|
||||||
obj.kineticEnergy = obj.mass * square(obj.velocity) / 2;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -452,7 +549,7 @@ export class System {
|
|||||||
const netMomentum = this.reduce((acc, obj) => {
|
const netMomentum = this.reduce((acc, obj) => {
|
||||||
if (objects.length && !objects.includes(obj)) return acc;
|
if (objects.length && !objects.includes(obj)) return acc;
|
||||||
return add(acc, mult(obj.velocity, obj.mass));
|
return add(acc, mult(obj.velocity, obj.mass));
|
||||||
}, zero);
|
}, zero, {alive: true});
|
||||||
|
|
||||||
return {totalMass, count, totalMassLocation, centerOfMass, netMomentum};
|
return {totalMass, count, totalMassLocation, centerOfMass, netMomentum};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import {hide, show} from '../helper.js';
|
import {hide, show} from '../helper.js';
|
||||||
import {Tool} from '../tool.js';
|
import {Tool} from '../tool.js';
|
||||||
|
import {magnitude} from '../vector.js';
|
||||||
|
|
||||||
export class ObjectsTool extends Tool {
|
export class ObjectsTool extends Tool {
|
||||||
objects = [];
|
objects = [];
|
||||||
@ -63,9 +64,12 @@ export class ObjectsTool extends Tool {
|
|||||||
// })));
|
// })));
|
||||||
objectEl.innerHTML =
|
objectEl.innerHTML =
|
||||||
`<span style="background-color: rgb(${r},${g},${b});"> </span>` +
|
`<span style="background-color: rgb(${r},${g},${b});"> </span>` +
|
||||||
`E<sub>k</sub>: ${obj.kineticEnergy.toFixed(0)}` +
|
`<div style="display:inline-block; vertical-align:middle; padding: 2px;">` +
|
||||||
`<br> W<sub>p</sub>: ${obj.workDoneByPointer.toFixed(0)}` +
|
`E<sub>k</sub>: ${obj.kineticEnergy.toFixed(0)}<br>` +
|
||||||
`<br> W<sub>f</sub>: ${obj.workDoneByForces.toFixed(0)}`;
|
`|p|: ${magnitude(obj.momentum).toFixed(0)}<br>` +
|
||||||
|
// `<br> W<sub>p</sub>: ${obj.workDoneByPointer.toFixed(0)}` +
|
||||||
|
// `<br> W<sub>f</sub>: ${obj.workDoneByForces.toFixed(0)}`;
|
||||||
|
'</div>';
|
||||||
|
|
||||||
// `${obj.mass.toPrecision(3)} ` +
|
// `${obj.mass.toPrecision(3)} ` +
|
||||||
// `${distance.toPrecision(3)}`;
|
// `${distance.toPrecision(3)}`;
|
||||||
|
|||||||
@ -126,6 +126,13 @@ export class OptionsTool extends Tool {
|
|||||||
|
|
||||||
input.addEventListener('change', () => {
|
input.addEventListener('change', () => {
|
||||||
this.sim.setOption(path, input.value);
|
this.sim.setOption(path, input.value);
|
||||||
|
// Enforce min/max if provided
|
||||||
|
if (item.max !== undefined && this.sim.getOption(path) > item.max) {
|
||||||
|
this.sim.setOption(path, item.max);
|
||||||
|
}
|
||||||
|
if (item.min !== undefined && this.sim.getOption(path) < item.min) {
|
||||||
|
this.sim.setOption(path, item.min);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.sim.onOptionSet(path, ({value}) => {
|
this.sim.onOptionSet(path, ({value}) => {
|
||||||
|
|||||||
12
vector.js
@ -1,7 +1,7 @@
|
|||||||
export const copy = (v) => ({x: v.x, y: v.y});
|
export const copy = (v) => ({x: v.x, y: v.y});
|
||||||
export const dot = (a, b) => a.x * b.x + a.y * b.y;
|
export const dot = (a, b) => a.x * b.x + a.y * b.y;
|
||||||
export const cross = (a, b) => a.x * b.y - a.y * b.x;
|
export const cross = (a, b) => a.x * b.y - a.y * b.x;
|
||||||
export const add = (a, b) => ({x: a.x + b.x, y: a.y + b.y});
|
const _add = (a, b) => ({x: a.x + b.x, y: a.y + b.y});
|
||||||
export const sub = (a, b) => ({x: a.x - b.x, y: a.y - b.y});
|
export const sub = (a, b) => ({x: a.x - b.x, y: a.y - b.y});
|
||||||
export const square = ({x, y}) => x ** 2 + y ** 2;
|
export const square = ({x, y}) => x ** 2 + y ** 2;
|
||||||
export const magnitude = ({x, y}) => Math.sqrt(square({x, y}));
|
export const magnitude = ({x, y}) => Math.sqrt(square({x, y}));
|
||||||
@ -20,7 +20,7 @@ export const weightedAvg = (items) => {
|
|||||||
let res = zero;
|
let res = zero;
|
||||||
let W = 0;
|
let W = 0;
|
||||||
for (const [v, w] of items) {
|
for (const [v, w] of items) {
|
||||||
res = add(res, mult(v, w));
|
res = _add(res, mult(v, w));
|
||||||
W += w;
|
W += w;
|
||||||
}
|
}
|
||||||
return div(res, W);
|
return div(res, W);
|
||||||
@ -33,3 +33,11 @@ export const components = (mag, dir) => mult(mag, {
|
|||||||
|
|
||||||
export const direction = ({x, y}) => Math.atan2(y, x);
|
export const direction = ({x, y}) => Math.atan2(y, x);
|
||||||
export const degrees = (rad) => rad * 180 / Math.PI;
|
export const degrees = (rad) => rad * 180 / Math.PI;
|
||||||
|
|
||||||
|
export const add = (...vectors) => {
|
||||||
|
let res = zero;
|
||||||
|
for (const v of vectors) {
|
||||||
|
res = _add(res, v);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|||||||
27
vector.test.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import {add, weightedAvg} from "./vector.js";
|
||||||
|
|
||||||
|
const assert = (expected, actual) => {
|
||||||
|
if (expected !== actual) {
|
||||||
|
console.error(`fail: ${expected} !== ${actual}`);
|
||||||
|
throw new Error('fail');
|
||||||
|
}
|
||||||
|
console.log(`${expected} === ${actual}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// add
|
||||||
|
{
|
||||||
|
const v1 = {x: 1, y: 1};
|
||||||
|
const v = add(v1, v1, v1);
|
||||||
|
assert('{"x":3,"y":3}', JSON.stringify(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
// weightedAvg
|
||||||
|
|
||||||
|
{
|
||||||
|
const v1 = {x: 1, y: 1};
|
||||||
|
const v2 = {x: -1, y: -1};
|
||||||
|
const v3 = {x: 2, y: 2};
|
||||||
|
assert('{"x":0,"y":0}', JSON.stringify(weightedAvg([[v1, 1], [v2, 1]])));
|
||||||
|
assert('{"x":1,"y":1}', JSON.stringify(weightedAvg([[v1, 1], [v1, 1]])));
|
||||||
|
assert('{"x":1.25,"y":1.25}', JSON.stringify(weightedAvg([[v1, 3], [v3, 1]])));
|
||||||
|
}
|
||||||