291 lines
8.7 KiB
JavaScript
291 lines
8.7 KiB
JavaScript
import { MassObject } from './object.js';
|
|
import {
|
|
MASS_CREATION_RATE,
|
|
DISPLAY_OBJECTS_INFO,
|
|
GRAVITATIONAL_CONSTANT,
|
|
ZOOM_TO_FIT_PADDING,
|
|
} from './config.js';
|
|
|
|
export class Objects {
|
|
objects = [];
|
|
creatingObject = undefined;
|
|
selectedObject = undefined;
|
|
paused = false;
|
|
panVelocityPaused = undefined;
|
|
|
|
constructor(sim) {
|
|
this.sim = sim;
|
|
}
|
|
|
|
pause() {
|
|
if (this.sim.playing) {
|
|
this.sim.playing = false;
|
|
this.paused = true;
|
|
if (this.sim.pointer.panning?.velocity) {
|
|
this.sim.pointer.panning.paused = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
resume() {
|
|
if (this.paused) {
|
|
this.sim.playing = true;
|
|
this.paused = false;
|
|
if (this.sim.pointer.panning?.paused) {
|
|
this.sim.pointer.panning.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
|
|
if (this.sim.getOption('pauseDuring.creation')) {
|
|
this.pause();
|
|
}
|
|
|
|
if (this.sim.pointer.panning?.velocity) {
|
|
obj.velocity = {...this.sim.pointer.panning.velocity};
|
|
}
|
|
}
|
|
|
|
doneCreatingObject() {
|
|
if (this.creatingObject !== undefined) {
|
|
this.creatingObject = undefined;
|
|
this.resume();
|
|
}
|
|
}
|
|
|
|
object(i) {
|
|
return this.objects[i];
|
|
}
|
|
|
|
selectObject(i) {
|
|
this.selectedObject = i;
|
|
if (this.sim.getOption('pauseDuring.selection')) {
|
|
this.pause();
|
|
}
|
|
}
|
|
|
|
deselect() {
|
|
this.selectedObject = undefined;
|
|
this.resume();
|
|
}
|
|
|
|
getSelectedOrCreating() {
|
|
let i = this.creatingObject ?? this.selectedObject;
|
|
if (i !== undefined) {
|
|
return this.objects[i];
|
|
}
|
|
}
|
|
|
|
get length() {
|
|
return this.objects.length;
|
|
}
|
|
|
|
get boundingBox() {
|
|
const box = {
|
|
start: {x: undefined, y: undefined},
|
|
end: {x: undefined, y: undefined},
|
|
};
|
|
this.forEachObject(obj => {
|
|
if (box.start.x === undefined) {
|
|
box.start = {...obj.position};
|
|
box.end = {...obj.position};
|
|
} else {
|
|
if (obj.position.x < box.start.x) box.start.x = obj.position.x;
|
|
if (obj.position.x > box.end.x) box.end.x = obj.position.x;
|
|
if (obj.position.y < box.start.y) box.start.y = obj.position.y;
|
|
if (obj.position.y > box.end.y) box.end.y = obj.position.y;
|
|
}
|
|
});
|
|
box.start.x -= ZOOM_TO_FIT_PADDING;
|
|
box.start.y -= ZOOM_TO_FIT_PADDING;
|
|
box.end.x += ZOOM_TO_FIT_PADDING;
|
|
box.end.y += ZOOM_TO_FIT_PADDING;
|
|
return box;
|
|
}
|
|
|
|
objectAtLocation(x, y) {
|
|
let idx = undefined;
|
|
this.forEachObject((obj, i) => {
|
|
// If distance to object is less than object's radius, we are touching the object
|
|
const dist = Math.pow((obj.position.x - x)**2 + (obj.position.y - y)**2, 1/2);
|
|
if (dist <= obj.radius) {
|
|
idx = i;
|
|
return null;
|
|
}
|
|
});
|
|
return idx;
|
|
}
|
|
|
|
handlePointerDown({x, y}) {
|
|
// If pointer is touching an object, select the object
|
|
const touchingObject = this.objectAtLocation(x, y);
|
|
|
|
if (touchingObject !== undefined) {
|
|
this.selectObject(touchingObject);
|
|
} else {
|
|
// Otherwise, create a new object
|
|
this.createObject(x, y);
|
|
}
|
|
}
|
|
|
|
handlePointerUp() {
|
|
this.doneCreatingObject();
|
|
this.deselect();
|
|
}
|
|
|
|
handlePointerMove({x, y, vx, vy}) {
|
|
// If the cursor moves while creating an object, or while an object is selected,
|
|
// update the position and velocity of the object
|
|
const obj = this.getSelectedOrCreating();
|
|
if (this.sim.pointer.panning?.velocity) {
|
|
vx += this.sim.pointer.panning.velocity.x;
|
|
vy += this.sim.pointer.panning.velocity.y;
|
|
}
|
|
if (obj !== undefined) {
|
|
obj.position.x = x;
|
|
obj.position.y = y;
|
|
obj.velocity.x = vx;
|
|
obj.velocity.y = vy;
|
|
}
|
|
}
|
|
|
|
// cb: (obj, idx) => {}
|
|
// TODO: Reducer
|
|
forEachObject(cb, alive = true, startWith = 0) {
|
|
for (let i = startWith; i < this.objects.length; i++) {
|
|
const obj = this.objects[i];
|
|
if (alive === null || alive == obj.alive) {
|
|
const ret = cb(obj, i);
|
|
if (ret === null) break;
|
|
}
|
|
}
|
|
}
|
|
|
|
computeForces() {
|
|
if (this.objects.length < 2) return;
|
|
this.forEachObject(obj => {
|
|
obj.forces = [];
|
|
});
|
|
this.forEachObject((A, i) => {
|
|
this.forEachObject(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);
|
|
const F = GRAVITATIONAL_CONSTANT * A.mass * B.mass / dSquared;
|
|
const Fx = F * dx / d;
|
|
const Fy = F * dy / d;
|
|
A.forces.push({ x: Fx, y: Fy });
|
|
B.forces.push({ x: -Fx, y: -Fy });
|
|
}, true, i + 1);
|
|
});
|
|
// Also compute acceleration
|
|
this.forEachObject(obj => {
|
|
obj.acceleration = obj.getAcceleration();
|
|
});
|
|
}
|
|
|
|
computeFrame(elapsedTime) {
|
|
// If we're creating an object, increment its mass
|
|
// with the mass creation rate accelerating over time
|
|
if (this.creatingObject !== undefined) {
|
|
const obj = this.objects[this.creatingObject];
|
|
const rate = MASS_CREATION_RATE * obj.age;
|
|
obj.mass += rate * elapsedTime;
|
|
}
|
|
|
|
// Calculate forces due to gravity.
|
|
this.computeForces();
|
|
|
|
if (this.sim.playing) {
|
|
// Predict positions (Velocity verlet method)
|
|
this.forEachObject(obj => {
|
|
obj.currentAcceleration = {...obj.acceleration};
|
|
|
|
obj.position.x += elapsedTime *
|
|
(obj.velocity.x + 1/2 * obj.currentAcceleration.x * elapsedTime);
|
|
obj.position.y += elapsedTime *
|
|
(obj.velocity.y + 1/2 * obj.currentAcceleration.y * elapsedTime);
|
|
});
|
|
|
|
// Collisions
|
|
if (this.sim.getOption('collision.merge')) {
|
|
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) {
|
|
// Merge B into A:
|
|
// Set position = center of mass
|
|
// Set velocity = total momentum / total mass
|
|
// Combine forces
|
|
// Add masses
|
|
// Average color
|
|
// If B was selected or being created, select A instead
|
|
// Remove B using splice
|
|
A.position.x = (A.position.x * A.mass + B.position.x * B.mass) / (A.mass + B.mass);
|
|
A.position.y = (A.position.y * A.mass + B.position.y * B.mass) / (A.mass + B.mass);
|
|
A.velocity.x = (A.velocity.x * A.mass + B.velocity.x * B.mass) / (A.mass + B.mass);
|
|
A.velocity.y = (A.velocity.y * A.mass + B.velocity.y * B.mass) / (A.mass + B.mass);
|
|
A.forces.push(...B.forces);
|
|
A.mass += B.mass;
|
|
A.color = {
|
|
r: (A.mass * A.color.r + B.mass * B.color.r) / (A.mass + B.mass),
|
|
g: (A.mass * A.color.g + B.mass * B.color.g) / (A.mass + B.mass),
|
|
b: (A.mass * A.color.b + B.mass * B.color.b) / (A.mass + B.mass),
|
|
};
|
|
if (this.creatingObject === j) this.creatingObject = i;
|
|
if (this.selectedObject === j) this.selectedObject = i;
|
|
B.alive = false;
|
|
B.forces = [];
|
|
}
|
|
}, true, i + 1);
|
|
});
|
|
}
|
|
|
|
// Recompute forces
|
|
this.computeForces();
|
|
|
|
// Predict velocities
|
|
this.forEachObject(obj => {
|
|
const acceleration = {...obj.acceleration};
|
|
obj.acceleration = {
|
|
x: (obj.currentAcceleration.x + acceleration.x) / 2,
|
|
y: (obj.currentAcceleration.y + acceleration.y) / 2,
|
|
};
|
|
obj.velocity.x += obj.acceleration.x * elapsedTime;
|
|
obj.velocity.y += obj.acceleration.y * elapsedTime;
|
|
|
|
// Append to object history
|
|
// TODO: enforce object history length
|
|
// TODO: store object color changes in history
|
|
obj.history.push({position: {...obj.position}});
|
|
});
|
|
}
|
|
|
|
// Display objects info
|
|
if (DISPLAY_OBJECTS_INFO) {
|
|
this.forEachObject((obj, i) => {
|
|
const speed = Math.pow(obj.velocity.x ** 2 + obj.velocity.y ** 2, 1/2);
|
|
// Invert y so that the angle is counterclockwise from x-axis
|
|
const direction = Math.atan2(-obj.velocity.y, obj.velocity.x) * 180 / Math.PI;
|
|
this.sim.info[`Object ${i}`] = [
|
|
`${obj.position.x.toPrecision(6)}, `,
|
|
`${obj.position.y.toPrecision(6)}, `,
|
|
`${obj.mass.toPrecision(6)} kg, `,
|
|
`${speed.toPrecision(2)} m/s, ${direction.toPrecision(2)}°`,
|
|
];
|
|
});
|
|
}
|
|
}
|
|
}
|