diff --git a/display.js b/display.js index b21d3f7..73d7596 100644 --- a/display.js +++ b/display.js @@ -72,7 +72,7 @@ export class Display { } drawObjects() { - this.sim.objects.forEachObject(obj => obj.drawObject(this.sim), null); + this.sim.objects.forEachObject(obj => obj.drawObject(this.sim), {alive: null}); } drawArrow(startX, startY, endX, endY, {style, width, arrowhead, arrowheadLength, fill, ifShort}) { diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..546de50 Binary files /dev/null and b/favicon.ico differ diff --git a/objects.js b/objects.js index 5763eae..2522b4c 100644 --- a/objects.js +++ b/objects.js @@ -86,25 +86,25 @@ export class Objects { } get boundingBox() { - const box = { + const box = this.reduce((acc, obj) => { + if (acc.start.x === undefined) { + acc.start = {...obj.position}; + acc.end = {...obj.position}; + } else { + if (obj.position.x < acc.start.x) acc.start.x = obj.position.x; + if (obj.position.x > acc.end.x) acc.end.x = obj.position.x; + if (obj.position.y < acc.start.y) acc.start.y = obj.position.y; + if (obj.position.y > acc.end.y) acc.end.y = obj.position.y; + } + }, { start: {x: undefined, y: undefined}, end: {x: undefined, y: undefined}, - }; - this.forEachObject(obj => { - 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; - } }); - 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; + + box.start.x = (box.start.x ?? 0) - ZOOM_TO_FIT_PADDING; + box.start.y = (box.start.y ?? 0) - ZOOM_TO_FIT_PADDING; + box.end.x = (box.end.x ?? 0) + ZOOM_TO_FIT_PADDING; + box.end.y = (box.end.y ?? 0) + ZOOM_TO_FIT_PADDING; return box; } @@ -155,9 +155,9 @@ export class Objects { } // cb: (obj, idx) => {} - // TODO: Reducer - forEachObject(cb, alive = true, startWith = 0) { - for (let i = startWith; i < this.objects.length; i++) { + forEachObject(cb, {alive, startWith} = {}) { + if (alive === undefined) alive = true; + for (let i = startWith ?? 0; i < this.objects.length; i++) { const obj = this.objects[i]; if (alive === null || alive == obj.alive) { const ret = cb(obj, i); @@ -166,6 +166,19 @@ export class Objects { } } + // cb: (acc, obj, idx) => {} + reduce(cb, initial, opts) { + let acc = initial; + console.log('reduce, initial', acc); + this.forEachObject((obj, idx) => { + const ret = cb(acc, obj, idx); + if (ret !== undefined) { + acc = ret; + } + }, opts); + return acc; + } + computeForces() { const gravity = this.sim.getOption('param.gravity'); if (this.objects.length < 2) return; @@ -183,7 +196,7 @@ export class Objects { const Fy = F * dy / d; A.forces.push({ x: Fx, y: Fy }); B.forces.push({ x: -Fx, y: -Fy }); - }, true, i + 1); + }, {alive: true, startWith: i + 1}); }); // Also compute acceleration this.forEachObject(obj => { @@ -262,7 +275,7 @@ export class Objects { T.alive = false; T.forces = []; } - }, true, i + 1); + }, {alive: true, startWith: i + 1}); }); } diff --git a/tool/play-pause.js b/tool/play-pause.js index b3860a7..85572a6 100644 --- a/tool/play-pause.js +++ b/tool/play-pause.js @@ -92,7 +92,7 @@ export class PlayPause extends Tool { // Obliterate object histories this.sim.objects.forEachObject(obj => { obj.history = []; - }, null); + }, {alive: null}); }); } } diff --git a/tool/zoom.js b/tool/zoom.js index 2b1dce7..b06b59c 100644 --- a/tool/zoom.js +++ b/tool/zoom.js @@ -90,31 +90,70 @@ export class Zoom extends Tool { }); zeroVelocity.addEventListener('click', () => { + // TODO: Zero net angular momentum + // Determine center of mass + const { totalMass, count, totalMassLocation } = + this.sim.objects.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: {x: 0, y: 0}, + totalMass: 0, + count: 0, + }); + + if (!count) return; + + const centerOfMass = { + x: totalMassLocation.x / totalMass, + y: totalMassLocation.y / totalMass, + }; + + console.log({totalMass, count, totalMassLocation, centerOfMass}); + + // Determine total angular momentum + const netAngularMomentum = this.sim.objects.reduce((acc, obj, idx) => { + // 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 d = Math.sqrt(r.x ** 2 + r.y ** 2); + console.log(`obj ${idx} s`, s, 'd', d); + return acc + obj.mass * s / d; + }, 0); + + console.log('net angular momentum', netAngularMomentum); + const netAngularVelocity = netAngularMomentum / totalMass; + console.log('net angular velocity', netAngularVelocity); + + // TODO: Apply rotation... + // Determine average momentum - const netMomentum = {x: 0, y: 0}; - let totalMass = 0; - let count = 0; - this.sim.objects.forEachObject(obj => { - count++; - netMomentum.x += obj.mass * obj.velocity.x; - netMomentum.y += obj.mass * obj.velocity.y; - totalMass += obj.mass; - }); - if (!count) { - return; - } + const netMomentum = this.sim.objects.reduce((acc, obj) => ({ + x: acc.x + obj.mass * obj.velocity.x, + y: acc.y + obj.mass * obj.velocity.y, + }), { x: 0, y: 0 }); + const netVelocity = { x: netMomentum.x / totalMass, y: netMomentum.y / totalMass, }; + // Apply offset to all object velocities this.sim.objects.forEachObject(obj => { obj.velocity.x -= netVelocity.x; obj.velocity.y -= netVelocity.y; }); - // TODO: Zero net angular momentum - // Cancel panning this.sim.pointer.panning = undefined; });