diff --git a/config.js b/config.js index f27ed1e..63eeebe 100644 --- a/config.js +++ b/config.js @@ -35,6 +35,7 @@ export const ARROWHEAD_WIDTH = 5; export const OFFSCREEN_OBJECT_LINE_SCALE = 7; export const OFFSCREEN_OBJECT_LINE_WIDTH = 2; export const OFFSCREEN_OBJECT_ARROWHEAD_LENGTH = 15; +export const ZOOM_TO_FIT_PADDING = 50; export const ZOOM_IN_FACTOR = 1; export const ZOOM_OUT_FACTOR = -1; export const SCALE_POWER_MAX = 8; diff --git a/objects.js b/objects.js index 7f4b369..bd8f8bc 100644 --- a/objects.js +++ b/objects.js @@ -4,6 +4,7 @@ import { DISPLAY_OBJECTS_INFO, MOTION_TIME_SCALE, GRAVITATIONAL_CONSTANT, + ZOOM_TO_FIT_PADDING, } from './config.js'; export class Objects { @@ -94,6 +95,10 @@ export class Objects { 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; } @@ -175,7 +180,6 @@ export class Objects { } 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) { @@ -199,39 +203,41 @@ export class Objects { }); // 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) { - // 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.color.r + B.color.r) / 2, - g: (A.color.g + B.color.g) / 2, - b: (A.color.b + B.color.b) / 2, - }; - if (this.creatingObject === j) this.creatingObject = i; - if (this.selectedObject === j) this.selectedObject = i; - B.alive = false; - B.forces = []; - } - }, true, i + 1); - }); + 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.color.r + B.color.r) / 2, + g: (A.color.g + B.color.g) / 2, + b: (A.color.b + B.color.b) / 2, + }; + 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(); @@ -255,13 +261,13 @@ export class Objects { // Display objects info if (DISPLAY_OBJECTS_INFO) { - this.forEachObject(obj => { + 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}, `, - `${obj.position.y}, `, + `${obj.position.x.toPrecision(6)}, `, + `${obj.position.y.toPrecision(6)}, `, `${obj.mass.toPrecision(6)} kg, `, `${speed.toPrecision(2)} m/s, ${direction.toPrecision(2)}°`, ]; diff --git a/overlay.js b/overlay.js index cbb46b9..52ca549 100644 --- a/overlay.js +++ b/overlay.js @@ -10,6 +10,7 @@ export class Overlay { this.sim.div.appendChild(infoBox); this.infoBox = infoBox; infoBox.style.position = 'relative'; + infoBox.style.display = 'inline-block'; infoBox.style.top = 0; infoBox.style.left = 0; infoBox.width = 'fit-content'; diff --git a/pointer.js b/pointer.js index 77bbcb9..2f0c55d 100644 --- a/pointer.js +++ b/pointer.js @@ -6,7 +6,6 @@ import { DRAGGABLE_ELEMENT_CLASSNAME, MODE_MASS_GENERATION, MODE_PAN_VIEW, - PAN_VELOCITY_SCALE_FACTOR, } from './config.js'; function dispatchEvent(target, eventType, data) { @@ -127,15 +126,13 @@ export class Pointer { handlePointerDown({x: clientX, y: clientY}) { this.updatePointer({x: clientX, y: clientY}); if (this.sim.isCurrentMode(MODE_MASS_GENERATION)) { + this.panning = undefined; const {x, y} = this.sim.screenToSim(clientX, clientY) this.sim.objects.handlePointerDown({x, y}); } else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) { this.panning = { gathering: true, - viewOriginStart: this.sim.display.viewOrigin, - pointerStart: {x: clientX, y: clientY}, - pointerCurrent: {x: clientX, y: clientY}, velocity: {x: 0, y: 0}, }; } @@ -170,8 +167,9 @@ export class Pointer { } else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) { if (this.panning?.gathering) { this.updatePointer({ x: clientX, y: clientY }); - this.panning.pointerCurrent = {x: clientX, y: clientY}; - this.panning.velocity = this.getPointerVelocity(); + const velocity = this.getPointerVelocity(); + this.panning.velocity.x = velocity.x; + this.panning.velocity.y = velocity.y; } } } @@ -185,15 +183,12 @@ export class Pointer { // Apply update to viewOrigin based on panning if (this.panning) { - const {pointerStart, pointerCurrent, viewOriginStart, velocity} = this.sim.pointer.panning; + const {velocity} = this.panning; // Convert pointer velocity to sim internal scale const vx = velocity.x / this.sim.display.scale; const vy = velocity.y / this.sim.display.scale; - this.panning.viewOriginStart = this.sim.display.viewOrigin; - this.panning.pointerStart = pointerCurrent; - // const speed = Math.sqrt(velocity.x ** 2 + velocity.y ** 2); - this.sim.display.viewOrigin.x -= vx * elapsedTime * PAN_VELOCITY_SCALE_FACTOR; - this.sim.display.viewOrigin.y -= vy * elapsedTime * PAN_VELOCITY_SCALE_FACTOR; + this.sim.display.viewOrigin.x -= vx * elapsedTime; + this.sim.display.viewOrigin.y -= vy * elapsedTime; } } } diff --git a/simulator.js b/simulator.js index 799fc05..f53cac9 100644 --- a/simulator.js +++ b/simulator.js @@ -12,6 +12,9 @@ import { SCALE_POWER_MIN, DISPLAY_CURRENT_SCALE, DISPLAY_CURRENT_MODE, + PAN_VELOCITY_SCALE_FACTOR, + MOTION_TIME_SCALE, + MODE_PAN_VIEW, } from './config.js'; export class Sim { @@ -53,12 +56,14 @@ export class Sim { // It's better not to change the scale in the middle of possible frame calculations, // so use this to schedule it and let the event loop pick it up. - scheduleZoom({x, y}, factor) { - this.nextZoom = {x, y, factor}; + // velocity should be in Sim coordinate scale + scheduleZoom({x, y}, factor, velocity) { + this.nextZoom = {x, y, factor, velocity}; } // x, y should be in Sim coordinates - zoom({x, y, factor}) { + // velocity should be in Sim coordinate scale + zoom({x, y, factor, velocity}) { // x, y are the mouse coordinates, which should be the center of the new view frame // the new view origin should be x, y minus half the new view width and height // compute new scale @@ -71,8 +76,18 @@ export class Sim { this.display.viewOrigin.y = y - this.display.height / 2; this.pointer.clearPointerHistory(); - if (this.pointer.panning) { - this.pointer.panning = undefined; + + if (this.playing && velocity) { + // Switch to pan mode + this.setCurrentMode(MODE_PAN_VIEW); + + this.pointer.panning = { + // Convert to screen scale since that's what we normally use for panning + velocity: { + x: -velocity.x * this.display.scale, + y: -velocity.y * this.display.scale, + } + }; } } @@ -91,7 +106,7 @@ export class Sim { // Main loop loop(currentTime) { - const elapsedTime = currentTime - this.time; + const elapsedTime = (currentTime - this.time) * MOTION_TIME_SCALE; this.time = currentTime; if (DISPLAY_CURRENT_MODE) { diff --git a/tool/modes.js b/tool/modes.js index 5a99b63..4e9a6b7 100644 --- a/tool/modes.js +++ b/tool/modes.js @@ -40,14 +40,15 @@ export class ModeSwitch extends Tool { button.innerHTML = `