diff --git a/pointer.js b/pointer.js index d1779fc..ac1e72f 100644 --- a/pointer.js +++ b/pointer.js @@ -46,6 +46,7 @@ export class Pointer { }); el.addEventListener('pointerleave', e => { + console.log('pointerleave', {x: e.clientX, y: e.clientY}); this.handlePointerUp({x: e.clientX, y: e.clientY}); }); @@ -55,6 +56,13 @@ export class Pointer { const {x, y} = this.sim.screenToSim(e.clientX, e.clientY); this.sim.scheduleZoom({x, y}, factor); }); + + el.addEventListener('focus', () => { + console.log('window focus'); + }); + el.addEventListener('blur', () => { + console.log('window blur'); + }); } handlePointerDown({x: clientX, y: clientY}) { diff --git a/system.js b/system.js index df5794b..446381c 100644 --- a/system.js +++ b/system.js @@ -1,5 +1,10 @@ import {OBJECT_HISTORY_SIZE} from './config.js'; import {MassObject} from './object.js'; +import { + add, cross, direction, div, magnitude, mult, + square, + sub, weightedAvg, zero +} from './vector.js'; export class System { objects = []; @@ -31,41 +36,31 @@ export class System { this.doneCreatingObject(); this.deselect(); // Convert pointer velocity to simulation scale - const pointer = {...this.sim.pointer.latestVelocity}; - obj.velocity.x = pointer.x / this.sim.display.scale; - obj.velocity.y = pointer.y / this.sim.display.scale; + obj.velocity = div(this.sim.pointer.latestVelocity, this.sim.display.scale); // Including time scale - if time is slow, our motion is relatively faster if (this.sim.getOption('compensate.timeScale')) { - obj.velocity.x /= this.sim.timeScale; - obj.velocity.y /= this.sim.timeScale; + obj.velocity = div(obj.velocity, this.sim.timeScale); } if (this.sim.panning?.velocity) { - obj.velocity.x += this.sim.panning.velocity.x; - obj.velocity.y += this.sim.panning.velocity.y; + obj.velocity = add(obj.velocity, this.sim.panning.velocity); } } - handlePointerMove({x, y}) { + handlePointerMove(r) { // If the cursor moves while creating an object, or while an object is selected, // update the position using the pointer motion but the velocity using the pointer velocity const obj = this.getSelectedOrCreating(); if (obj === undefined) return; const start = this.selectedObjectStart; - obj.position.x = start.x + (x - start.pointer.x); - obj.position.y = start.y + (y - start.pointer.y); - obj.velocity.x = 0; - obj.velocity.y = 0; + obj.position = add(start, sub(r, start.pointer)); + obj.velocity = zero; } - // elapsedTime is given in milliseconds frame(elapsedTime) { // If we're creating an object, increment its mass // with the mass creation rate accelerating over time - - // Scaling this parameter because of millisecond conversion - if (this.creatingObject !== undefined) { const obj = this.objects[this.creatingObject]; let massCreationRate = this.sim.getOption('param.massCreationRate'); @@ -90,10 +85,13 @@ export class System { return; } - 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); + obj.position = add(obj.position, mult( + elapsedTime, + add( + obj.velocity, + mult(obj.currentAcceleration, elapsedTime / 2) + ), + )); }); // Collisions @@ -125,12 +123,10 @@ export class System { // Set position = center of mass // Set velocity = total momentum / total mass // Combine forces - // Sdd masses - // Sverage color - S.position.x = (S.position.x * S.mass + T.position.x * T.mass) / (S.mass + T.mass); - S.position.y = (S.position.y * S.mass + T.position.y * T.mass) / (S.mass + T.mass); - S.velocity.x = (S.velocity.x * S.mass + T.velocity.x * T.mass) / (S.mass + T.mass); - S.velocity.y = (S.velocity.y * S.mass + T.velocity.y * T.mass) / (S.mass + T.mass); + // Add masses + // Average color + S.position = weightedAvg(S.position, S.mass, T.position, T.mass); + S.velocity = weightedAvg(S.velocity, S.mass, T.velocity, T.mass); S.forces.push(...T.forces); S.mass += T.mass; S.color = { @@ -150,12 +146,8 @@ export class System { // 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; + obj.acceleration = div(add(obj.currentAcceleration, acceleration), 2); + obj.velocity = add(obj.velocity, mult(obj.acceleration, elapsedTime)); // Append to object history obj.history.push({position: {...obj.position}}); @@ -177,18 +169,18 @@ export class System { if (this.sim.getOption('debug.objectsInfo')) { const aliveOnly = this.sim.getOption('debug.aliveObjects'); this.forEachObject((obj, i) => { - const speed = Math.pow(obj.velocity.x ** 2 + obj.velocity.y ** 2, 1 / 2); - const accel = Math.pow(obj.acceleration.x ** 2 + obj.acceleration.y ** 2, 1 / 2); + const speed = magnitude(obj.velocity); + const accel = magnitude(obj.acceleration); // Invert y so that the angle is counterclockwise from x-axis - const direction = Math.atan2(-obj.velocity.y, obj.velocity.x) * 180 / Math.PI; - const accelDir = Math.atan2(-obj.acceleration.y, obj.acceleration.x) * 180 / Math.PI; + const velocityDir = direction(obj.velocity); + const accelDir = direction(obj.acceleration); const {r, g, b} = obj.color; this.sim.info[`Object ${i}`] = [ `  `, `${obj.position.x.toPrecision(4)}, `, `${obj.position.y.toPrecision(4)}, `, `${obj.mass.toPrecision(4)} kg, `, - `${speed.toPrecision(2)} m/s, ${direction.toPrecision(2)}°`, + `${speed.toPrecision(2)} m/s, ${velocityDir.toPrecision(2)}°`, `${accel.toPrecision(2)} m/s2, ${accelDir.toPrecision(2)}°`, `Alive: ${obj.alive}`, ]; @@ -307,7 +299,7 @@ export class System { this.selectedObjectStart = 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); + const dist = magnitude(sub(obj.position, {x, y})); if (dist <= obj.radius) { idx = i; return null; @@ -352,13 +344,13 @@ export class System { }); 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 r = sub(B.position, A.position); + const dSquared = square(r); const d = Math.sqrt(dSquared); const F = gravity * A.mass * B.mass / dSquared; - const Fx = F * dx / d; - const Fy = F * dy / d; + const Fx = F * r.x / d; + const Fy = F * r.y / d; + // Equal and opposite forces A.forces.push({x: Fx, y: Fy}); B.forces.push({x: -Fx, y: -Fy}); }, {alive: true, startWith: i + 1}); @@ -375,26 +367,19 @@ export class System { this.reduce((acc, obj) => ({ count: acc.count + 1, totalMass: acc.totalMass + obj.mass, - totalMassLocation: { - x: acc.totalMassLocation.x + obj.position.x * obj.mass, - y: acc.totalMassLocation.y + obj.position.y * obj.mass, - }, + totalMassLocation: add(acc.totalMassLocation, + mult(obj.position, obj.mass)), }), { totalMassLocation: {x: 0, y: 0}, totalMass: 0, count: 0, }); - const centerOfMass = count ? { - x: totalMassLocation.x / totalMass, - y: totalMassLocation.y / totalMass, - } : {x: 0, y: 0}; + const centerOfMass = count ? div(totalMassLocation, totalMass) : zero; // Determine average momentum - const netMomentum = this.reduce((acc, obj) => ({ - x: acc.x + obj.mass * obj.velocity.x, - y: acc.y + obj.mass * obj.velocity.y, - }), {x: 0, y: 0}); + const netMomentum = this.reduce((acc, obj) => + add(acc, mult(obj.velocity, obj.mass)), zero); return {totalMass, count, totalMassLocation, centerOfMass, netMomentum}; } @@ -408,12 +393,8 @@ export class System { // Angular momentum for each object is m * s / d // where d is the distance of the object from the global center of mass // and s is the magnitude of the cross product of v and r - const r = { - x: obj.position.x - centerOfMass.x, - y: obj.position.y - centerOfMass.y, - }; - const v = obj.velocity; - const s = v.x * r.y - v.y * r.x; + const r = sub(obj.position, centerOfMass); + const s = cross(obj.velocity, r); const d = Math.sqrt(r.x ** 2 + r.y ** 2); return acc + obj.mass * s / d; }, 0); diff --git a/vector.js b/vector.js new file mode 100644 index 0000000..1ed2934 --- /dev/null +++ b/vector.js @@ -0,0 +1,46 @@ +export const dot = (a, b) => a.x * b.x + a.y * b.y; +export const cross = (a, b) => a.x * b.y - a.y * b.x; +export const add = (a, b) => ({x: a.x + b.x, y: a.y + b.y}); +export const sub = (a, b) => ({x: a.x - b.x, y: a.y - b.y}); +export const square = ({x, y}) => x ** 2 + y ** 2; +export const magnitude = ({x, y}) => Math.sqrt(square({x, y})); +export const mult = (v, m) => { + if (v.x !== undefined) return {x: v.x * m, y: v.y * m}; + else return {x: m.x * v, y: m.y * v}; +}; +export const div = (v, m) => ({x: v.x / m, y: v.y / m}); +export const zero = {x: 0, y: 0}; +export const weightedAvg = (items) => { + let res = zero; + let W = 0; + for (const [v, w] of items) { + res = add(res, mult(v, w)); + W += w; + } + return div(res, W); +}; + +// Angle is given counterclockwise, assuming screen coordinates +export const direction = ({x, y}) => Math.atan2(-y, x) * 180 / Math.PI; + +export class Vector { + x = undefined; + y = undefined; + + constructor({x, y}) { + this.x = x; + this.y = y; + } + + dot(v) {return dot(this, v);} + cross(v) {return cross(this, v);} + add(v) {return new Vector(add(this, v));} + sub(v) {return new Vector(sub(this, v));} + magnitude() {return magnitude(this);} + mult(m) {return new Vector(mult(this, m));} + div(m) {return new Vector(div(this, m));} + + toJSON() { + return {x: this.x, y: this.y}; + } +}