Compare commits
No commits in common. "190eca6691c2e169b37e5955546c3514db51930f" and "71897117fa09c86d10a77a23544520494ea8b508" have entirely different histories.
190eca6691
...
71897117fa
@ -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; // Milliseconds
|
export const PANNING_ZERO_TOUCH_THRESHOLD = 200;
|
||||||
|
|
||||||
// SIZES
|
// SIZES
|
||||||
export const POINTER_HISTORY_SIZE = 20;
|
export const POINTER_HISTORY_SIZE = 20;
|
||||||
|
|||||||
|
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
gravity-simulator-5.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 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, mult, square, zero} from './vector.js';
|
import {add, components, copy, direction, div, magnitude, zero} from './vector.js';
|
||||||
|
|
||||||
export class MassObject {
|
export class MassObject {
|
||||||
sim = undefined;
|
sim = undefined;
|
||||||
@ -31,6 +31,7 @@ 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;
|
||||||
|
|
||||||
@ -91,14 +92,6 @@ 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,6 +69,8 @@ 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);
|
||||||
}
|
}
|
||||||
@ -80,6 +82,10 @@ 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,12 +53,7 @@ export class Pointer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePointerDown({x: clientX, y: clientY, touches}) {
|
handlePointerDown({x: clientX, y: clientY}) {
|
||||||
// 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: 266 KiB |
@ -10,19 +10,18 @@ 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', 80, {showIf: 'display.acceleration'}],
|
accelerationScale: ['Accel<br>Vec Scale', 'number', 800, {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,8 +51,6 @@ 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);
|
||||||
@ -111,7 +109,6 @@ 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) {
|
||||||
@ -136,7 +133,6 @@ 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(),
|
||||||
@ -149,7 +145,6 @@ 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);
|
||||||
@ -170,7 +165,6 @@ 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;
|
||||||
|
|
||||||
@ -182,7 +176,6 @@ 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;
|
||||||
|
|
||||||
@ -194,7 +187,6 @@ 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,15 +72,6 @@ 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;
|
||||||
}
|
}
|
||||||
@ -120,6 +111,54 @@ 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();
|
||||||
|
|
||||||
@ -150,40 +189,7 @@ 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
|
||||||
@ -389,107 +395,6 @@ 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;
|
||||||
@ -501,11 +406,6 @@ 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;
|
||||||
@ -523,6 +423,9 @@ 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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -549,7 +452,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, {alive: true});
|
}, zero);
|
||||||
|
|
||||||
return {totalMass, count, totalMassLocation, centerOfMass, netMomentum};
|
return {totalMass, count, totalMassLocation, centerOfMass, netMomentum};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
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 = [];
|
||||||
@ -64,12 +63,9 @@ 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>` +
|
||||||
`<div style="display:inline-block; vertical-align:middle; padding: 2px;">` +
|
`E<sub>k</sub>: ${obj.kineticEnergy.toFixed(0)}` +
|
||||||
`E<sub>k</sub>: ${obj.kineticEnergy.toFixed(0)}<br>` +
|
`<br> W<sub>p</sub>: ${obj.workDoneByPointer.toFixed(0)}` +
|
||||||
`|p|: ${magnitude(obj.momentum).toFixed(0)}<br>` +
|
`<br> W<sub>f</sub>: ${obj.workDoneByForces.toFixed(0)}`;
|
||||||
// `<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,13 +126,6 @@ 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;
|
||||||
const _add = (a, b) => ({x: a.x + b.x, y: a.y + b.y});
|
export 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,11 +33,3 @@ 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;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
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]])));
|
|
||||||
}
|
|
||||||