gravity/object.js
2026-01-03 13:10:38 -06:00

246 lines
7.4 KiB
JavaScript

import {
ACCELERATION_VECTOR_ARROWHEAD,
ACCELERATION_VECTOR_COLOR,
ACCELERATION_VECTOR_WIDTH,
OFFSCREEN_OBJECT_ARROWHEAD_LENGTH,
OFFSCREEN_OBJECT_LINE_SCALE,
OFFSCREEN_OBJECT_LINE_WIDTH,
PATH_TRACES_COLOR,
PATH_TRACES_DASHED_OPACITY,
PATH_TRACES_OPACITY,
PATH_TRACES_WIDTH,
VELOCITY_VECTOR_ARROWHEAD,
VELOCITY_VECTOR_COLOR,
VELOCITY_VECTOR_WIDTH,
} from './config.js';
import {add, components, direction, div, magnitude} from './vector.js';
export class MassObject {
sim = undefined;
id = undefined;
mass = 0;
density = 1;
position = {x: undefined, y: undefined};
velocity = {x: 0, y: 0};
acceleration = {x: 0, y: 0};
color = {r: undefined, g: undefined, b: undefined};
created = undefined;
forces = []; // [{x, y}]
history = [];
alive = true;
currentPosition = undefined;
currentAcceleration = undefined;
constructor(sim, x, y) {
this.sim = sim;
this.id = crypto.randomUUID();
this.position.x = x;
this.position.y = y;
this.color.r = Math.random() * 256;
this.color.g = Math.random() * 256;
this.color.b = Math.random() * 256;
this.timeCreated = this.sim.time;
this.rawTimeCreated = this.sim.rawTime;
}
get age() {
return this.sim.time - this.timeCreated;
}
get rawAge() {
return this.sim.rawTime - this.rawTimeCreated;
}
get radius() {
// radius should be proportional to cube root of mass
return Math.pow(this.mass / this.density, 1 / 3);
}
getAcceleration() {
let ax = 0;
let ay = 0;
for (let {x, y} of this.forces) {
ax += x;
ay += y;
}
return {
x: ax / this.mass,
y: ay / this.mass,
};
}
drawObject(sim) {
const {
color: {r, g, b},
position: {x, y},
velocity: {x: vx, y: vy},
acceleration,
radius,
} = this;
const {
display: {
ctx,
height: H,
width: W,
viewOrigin: {x: ox, y: oy},
}
} = sim;
const cx = ox + W / 2;
const cy = oy + H / 2;
// Draw path traces
if (sim.getOption('display.traces') && this.history?.length) {
const dashedTraces = sim.getOption('display.dashedTraces');
const opacity = dashedTraces ? PATH_TRACES_DASHED_OPACITY : PATH_TRACES_OPACITY;
ctx.strokeStyle = PATH_TRACES_COLOR === 'object color' ?
`rgba(${r}, ${g}, ${b}, ${opacity})` : PATH_TRACES_COLOR;
ctx.lineWidth = PATH_TRACES_WIDTH / this.sim.display.scale;
ctx.beginPath();
let dash = false;
for (let i = 0; i < this.history.length; i++) {
// if (i % 2 > 0) continue;
const {position: {x, y}} = this.history[i];
if (dashedTraces) {
if (dash) {
ctx.lineTo(x, y);
} else {
ctx.moveTo(x, y);
}
dash = !dash;
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
}
if (!this.alive) return;
// If the object is outside the display area, draw an arrow at the edge of the display
if (Math.abs(x - cx) - radius >= W / 2 ||
Math.abs(y - cy) - radius >= H / 2) {
// Find where a line from center of display to object intersects display edge
let px, py;
if (y <= cy) {
// Line intersects y = 0:
const y0px = cx + (H / 2) / (cy - y) * (x - cx);
if (Math.abs(y0px - cx) <= W / 2) {
px = y0px;
py = oy;
}
} else {
// Line intersects y = H
const yHpx = cx + (H / 2) / (y - cy) * (x - cx);
if (Math.abs(yHpx - cx) <= W / 2) {
px = yHpx;
py = oy + H;
}
}
if (px === undefined) {
if (x <= cx) {
// Line intersects x = 0:
px = ox;
py = cy + (W / 2) / (cx - x) * (y - cy);
} else {
// Line intersects x = W:
px = ox + W;
py = cy + (W / 2) / (x - cx) * (y - cy);
}
}
const arrowDirection = Math.atan2(py - cy, px - cx);
// Length of arrow based on distance (logarithmic scale)
const distance = Math.sqrt((x - px) ** 2, (y - py) ** 2);
const arrowLength = Math.log(distance + 1) * OFFSCREEN_OBJECT_LINE_SCALE / this.sim.display.scale;
const startAx = px - arrowLength * Math.cos(arrowDirection);
const startAy = py - arrowLength * Math.sin(arrowDirection);
sim.display.drawArrow(startAx, startAy, px, py, {
style: `rgb(${r}, ${g}, ${b})`,
width: OFFSCREEN_OBJECT_LINE_WIDTH,
arrowheadLength: OFFSCREEN_OBJECT_ARROWHEAD_LENGTH,
fill: false,
ifShort: 'head',
});
return;
}
// Draw filled circle for the object
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.fill();
// Draw arrow for the velocity
if (sim.getOption('display.velocity')) {
const vecScale = this.sim.getOption('display.velocityScale');
const selected = this.sim.system.getSelectedOrCreating();
const isSelected = selected?.id === this.id;
let velocity = {x: vx, y: vy};
if (isSelected) {
// If this object is being dragged by the user,
// show the pointer velocity instead of object velocity
let pointerV = this.sim.pointer.latestVelocity;
if (this.sim.getOption('compensate.timeScale')) {
pointerV = div(pointerV, this.sim.timeScale);
}
velocity = add(velocity, pointerV);
}
const speed = magnitude(velocity);
const arrowDirection = direction(velocity);
// Prevent negative numbers by adding 1
// TODO: Make logarithmic vector length scale optional
// Make arrow lengths appear consistent regardless of display scale
let arrowLength = Math.log10(speed + 1) * vecScale;
if (!this.sim.getOption('display.zoomVectors')) {
arrowLength /= this.sim.display.scale;
}
// const offset = mult({x: Math.cos(
const offset = components(radius, arrowDirection)
const start = add({x, y}, offset);
const end = add(start, components(arrowLength, arrowDirection));
const style = VELOCITY_VECTOR_COLOR === 'object color' ?
`rgb(${r}, ${g}, ${b})` : VELOCITY_VECTOR_COLOR;
sim.display.drawArrow(start.x, start.y, end.x, end.y, {
style,
width: VELOCITY_VECTOR_WIDTH,
arrowhead: VELOCITY_VECTOR_ARROWHEAD,
fill: false,
ifShort: 'head'
});
}
// Draw arrow for acceleration
if (sim.getOption('display.acceleration')) {
const vecScale = this.sim.getOption('display.accelerationScale');
const accel = magnitude(acceleration);
const arrowDirection = direction(acceleration);
// Prevent negative numbers by adding 1
// TODO: Make logarithmic vector length scale optional
// Make arrow lengths appear consistent regardless of display scale
let arrowLength = Math.log10(accel + 1) * vecScale;
if (!this.sim.getOption('display.zoomVectors')) {
arrowLength /= this.sim.display.scale;
}
const offset = components(radius, arrowDirection)
const start = add({x, y}, offset);
const end = add(start, components(arrowLength, arrowDirection));
const style = ACCELERATION_VECTOR_COLOR === 'object color' ?
`rgb(${r}, ${g}, ${b})` : ACCELERATION_VECTOR_COLOR;
sim.display.drawArrow(start.x, start.y, end.x, end.y, {
style,
width: ACCELERATION_VECTOR_WIDTH,
arrowhead: ACCELERATION_VECTOR_ARROWHEAD,
fill: false,
ifShort: 'tail'
});
}
}
}