import { ACCELERATION_VECTOR_ARROWHEAD, ACCELERATION_VECTOR_COLOR, ACCELERATION_VECTOR_SCALE, 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_SCALE, VELOCITY_VECTOR_WIDTH, } from './config.js'; export class MassObject { 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; constructor(x, y) { 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.created = document.timeline.currentTime; } get age() { return document.timeline.currentTime - this.created; } 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.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) * this.scale; const arrowLength = Math.log(distance) * OFFSCREEN_OBJECT_LINE_SCALE / this.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 speed = Math.sqrt(vx ** 2 + vy ** 2); const endVx = x + VELOCITY_VECTOR_SCALE * vx / speed * Math.log(speed); const endVy = y + VELOCITY_VECTOR_SCALE * vy / speed * Math.log(speed); const style = VELOCITY_VECTOR_COLOR === 'object color' ? `rgb(${r}, ${g}, ${b})` : VELOCITY_VECTOR_COLOR; sim.display.drawArrow(x, y, endVx, endVy, { style, width: VELOCITY_VECTOR_WIDTH, arrowhead: VELOCITY_VECTOR_ARROWHEAD, fill: false, ifShort: 'head' }); } // Draw arrow for acceleration if (sim.getOption('display.acceleration')) { const accelerationMagnitude = Math.sqrt(acceleration.x ** 2 + acceleration.y ** 2); const endAx = x + ACCELERATION_VECTOR_SCALE * acceleration.x / accelerationMagnitude * Math.log(accelerationMagnitude); const endAy = y + ACCELERATION_VECTOR_SCALE * acceleration.y / accelerationMagnitude * Math.log(accelerationMagnitude); const style = ACCELERATION_VECTOR_COLOR === 'object color' ? `rgb(${r}, ${g}, ${b})` : ACCELERATION_VECTOR_COLOR; sim.display.drawArrow(x, y, endAx, endAy, { style, width: ACCELERATION_VECTOR_WIDTH, arrowhead: ACCELERATION_VECTOR_ARROWHEAD, fill: false, ifShort: 'tail' }); } } }