import { Tool } from '../tool.js'; import { TOOL_INFO_CLASSNAME, WIDE_CLASSNAME, ZOOM_IN_FACTOR, ZOOM_OUT_FACTOR, } from '../config.js'; export class Zoom extends Tool { get displayScale() { return this.sim.getScaleDisplay(); } get displayScaleText() { return `Scale: ${this.displayScale}`; } constructor(toolbar) { super(toolbar); const currentScale = document.createElement('button') const zoomOut = document.createElement('button'); const zoomIn = document.createElement('button'); const zoomAll = document.createElement('button'); const zeroVelocity = document.createElement('button'); this.div.appendChild(currentScale); this.div.appendChild(zoomOut); this.div.appendChild(zoomIn); this.div.appendChild(zoomAll); this.div.appendChild(zeroVelocity); zoomAll.classList.add(WIDE_CLASSNAME); zeroVelocity.classList.add(WIDE_CLASSNAME); currentScale.classList.add(WIDE_CLASSNAME); currentScale.classList.add(TOOL_INFO_CLASSNAME); zoomOut.innerHTML = 'Zoom
Out'; zoomIn.innerHTML = 'Zoom
In'; zoomAll.innerHTML = 'Zoom to Fit'; zeroVelocity.innerHTML = 'Zero Momentum'; currentScale.innerHTML = this.displayScaleText; this.sim.onZoom(() => { currentScale.innerHTML = this.displayScaleText; }); zoomOut.addEventListener('click', () => { // Aim at center of view const x = this.sim.display.width * this.sim.display.scale / 2; const y = this.sim.display.height * this.sim.display.scale / 2; this.sim.scheduleZoom(this.sim.screenToSim(x, y), ZOOM_OUT_FACTOR); }); zoomIn.addEventListener('click', () => { // Aim at center of view const x = this.sim.display.width * this.sim.display.scale / 2; const y = this.sim.display.height * this.sim.display.scale / 2; this.sim.scheduleZoom(this.sim.screenToSim(x, y), ZOOM_IN_FACTOR); }); zoomAll.addEventListener('click', () => { // Determine bounding box const box = this.sim.objects.boundingBox; const x = (box.start.x + box.end.x) / 2; const y = (box.start.y + box.end.y) / 2; const widthRatio = Math.abs(box.start.x - box.end.x) / this.sim.display.width; const heightRatio = Math.abs(box.start.y - box.end.y) / this.sim.display.height; const biggerRatio = Math.max(widthRatio, heightRatio); const base2factor = Math.log(1/biggerRatio) / Math.log(2) - 1; const factor = Math.floor(base2factor); // Determine average momentum and set panning velocity to match 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 netVelocity = { x: netMomentum.x / totalMass, y: netMomentum.y / totalMass, }; this.sim.scheduleZoom({x, y}, factor, netVelocity) }); 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 = 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; }); // Cancel panning this.sim.pointer.panning = undefined; }); } }