gravity/objects.js
2025-12-27 02:14:57 -06:00

229 lines
6.4 KiB
JavaScript

import { MassObject } from './object.js';
import {
MASS_CREATION_RATE,
DISPLAY_OBJECTS_INFO,
MOTION_TIME_SCALE,
GRAVITATIONAL_CONSTANT,
} from './config.js';
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];
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},
};
for (let i = 0; i < this.objects.length; i++) {
const obj = this.objects[i];
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;
}
}
return box;
}
objectAtLocation(x, y) {
for (let i = 0; i < this.objects.length; i++) {
const obj = this.objects[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) {
return i;
}
}
}
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({x, y}) {
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 (obj !== undefined) {
obj.position.x = x;
obj.position.y = y;
obj.velocity.x = vx;
obj.velocity.y = vy;
}
}
computeForces() {
if (this.objects.length < 2) return;
for (let i = 0; i < this.objects.length; i++) {
this.objects[i].forces = [];
}
for (let i = 0; i < this.objects.length; i++) {
const A = this.objects[i];
for (let j = i + 1; j < this.objects.length; j++) {
const B = this.objects[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);
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 });
}
}
// Also compute acceleration
for (let i = 0; i < this.objects.length; i++) {
const obj = this.objects[i];
obj.acceleration = obj.getAcceleration();
}
}
computeFrame(elapsedTime) {
elapsedTime = elapsedTime * MOTION_TIME_SCALE;
// 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)
for (let i = 0; i < this.objects.length; i++) {
const obj = this.objects[i];
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);
}
// Recompute forces
this.computeForces();
// Predict velocities
for (let i = 0; i < this.objects.length; i++) {
const obj = this.objects[i];
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;
}
// TODO: Collisions
}
// Display objects info
if (DISPLAY_OBJECTS_INFO) {
for (let i = 0; i < this.objects.length; i++) {
const obj = this.objects[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}, `,
`${obj.position.y}, `,
`${obj.mass.toPrecision(6)} kg, `,
`${speed.toPrecision(2)} m/s, ${direction.toPrecision(2)}°`,
];
}
}
}
}