zoom to fit now also pans to avg momentum
This commit is contained in:
parent
28909a0c82
commit
5c6e685764
@ -35,6 +35,7 @@ export const ARROWHEAD_WIDTH = 5;
|
|||||||
export const OFFSCREEN_OBJECT_LINE_SCALE = 7;
|
export const OFFSCREEN_OBJECT_LINE_SCALE = 7;
|
||||||
export const OFFSCREEN_OBJECT_LINE_WIDTH = 2;
|
export const OFFSCREEN_OBJECT_LINE_WIDTH = 2;
|
||||||
export const OFFSCREEN_OBJECT_ARROWHEAD_LENGTH = 15;
|
export const OFFSCREEN_OBJECT_ARROWHEAD_LENGTH = 15;
|
||||||
|
export const ZOOM_TO_FIT_PADDING = 50;
|
||||||
export const ZOOM_IN_FACTOR = 1;
|
export const ZOOM_IN_FACTOR = 1;
|
||||||
export const ZOOM_OUT_FACTOR = -1;
|
export const ZOOM_OUT_FACTOR = -1;
|
||||||
export const SCALE_POWER_MAX = 8;
|
export const SCALE_POWER_MAX = 8;
|
||||||
|
|||||||
14
objects.js
14
objects.js
@ -4,6 +4,7 @@ import {
|
|||||||
DISPLAY_OBJECTS_INFO,
|
DISPLAY_OBJECTS_INFO,
|
||||||
MOTION_TIME_SCALE,
|
MOTION_TIME_SCALE,
|
||||||
GRAVITATIONAL_CONSTANT,
|
GRAVITATIONAL_CONSTANT,
|
||||||
|
ZOOM_TO_FIT_PADDING,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
|
|
||||||
export class Objects {
|
export class Objects {
|
||||||
@ -94,6 +95,10 @@ export class Objects {
|
|||||||
if (obj.position.y > box.end.y) box.end.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;
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,7 +180,6 @@ export class Objects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
computeFrame(elapsedTime) {
|
computeFrame(elapsedTime) {
|
||||||
elapsedTime = elapsedTime * MOTION_TIME_SCALE;
|
|
||||||
// If we're creating an object, increment its mass
|
// If we're creating an object, increment its mass
|
||||||
// with the mass creation rate accelerating over time
|
// with the mass creation rate accelerating over time
|
||||||
if (this.creatingObject !== undefined) {
|
if (this.creatingObject !== undefined) {
|
||||||
@ -199,6 +203,7 @@ export class Objects {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Collisions
|
// Collisions
|
||||||
|
if (this.sim.getOption('collision.merge')) {
|
||||||
this.forEachObject((A, i) => {
|
this.forEachObject((A, i) => {
|
||||||
this.forEachObject((B, j) => {
|
this.forEachObject((B, j) => {
|
||||||
const dx = (B.position.x - A.position.x);
|
const dx = (B.position.x - A.position.x);
|
||||||
@ -232,6 +237,7 @@ export class Objects {
|
|||||||
}
|
}
|
||||||
}, true, i + 1);
|
}, true, i + 1);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Recompute forces
|
// Recompute forces
|
||||||
this.computeForces();
|
this.computeForces();
|
||||||
@ -255,13 +261,13 @@ export class Objects {
|
|||||||
|
|
||||||
// Display objects info
|
// Display objects info
|
||||||
if (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);
|
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
|
// 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 direction = Math.atan2(-obj.velocity.y, obj.velocity.x) * 180 / Math.PI;
|
||||||
this.sim.info[`Object ${i}`] = [
|
this.sim.info[`Object ${i}`] = [
|
||||||
`${obj.position.x}, `,
|
`${obj.position.x.toPrecision(6)}, `,
|
||||||
`${obj.position.y}, `,
|
`${obj.position.y.toPrecision(6)}, `,
|
||||||
`${obj.mass.toPrecision(6)} kg, `,
|
`${obj.mass.toPrecision(6)} kg, `,
|
||||||
`${speed.toPrecision(2)} m/s, ${direction.toPrecision(2)}°`,
|
`${speed.toPrecision(2)} m/s, ${direction.toPrecision(2)}°`,
|
||||||
];
|
];
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export class Overlay {
|
|||||||
this.sim.div.appendChild(infoBox);
|
this.sim.div.appendChild(infoBox);
|
||||||
this.infoBox = infoBox;
|
this.infoBox = infoBox;
|
||||||
infoBox.style.position = 'relative';
|
infoBox.style.position = 'relative';
|
||||||
|
infoBox.style.display = 'inline-block';
|
||||||
infoBox.style.top = 0;
|
infoBox.style.top = 0;
|
||||||
infoBox.style.left = 0;
|
infoBox.style.left = 0;
|
||||||
infoBox.width = 'fit-content';
|
infoBox.width = 'fit-content';
|
||||||
|
|||||||
19
pointer.js
19
pointer.js
@ -6,7 +6,6 @@ import {
|
|||||||
DRAGGABLE_ELEMENT_CLASSNAME,
|
DRAGGABLE_ELEMENT_CLASSNAME,
|
||||||
MODE_MASS_GENERATION,
|
MODE_MASS_GENERATION,
|
||||||
MODE_PAN_VIEW,
|
MODE_PAN_VIEW,
|
||||||
PAN_VELOCITY_SCALE_FACTOR,
|
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
|
|
||||||
function dispatchEvent(target, eventType, data) {
|
function dispatchEvent(target, eventType, data) {
|
||||||
@ -127,15 +126,13 @@ export class Pointer {
|
|||||||
handlePointerDown({x: clientX, y: clientY}) {
|
handlePointerDown({x: clientX, y: clientY}) {
|
||||||
this.updatePointer({x: clientX, y: clientY});
|
this.updatePointer({x: clientX, y: clientY});
|
||||||
if (this.sim.isCurrentMode(MODE_MASS_GENERATION)) {
|
if (this.sim.isCurrentMode(MODE_MASS_GENERATION)) {
|
||||||
|
this.panning = undefined;
|
||||||
const {x, y} = this.sim.screenToSim(clientX, clientY)
|
const {x, y} = this.sim.screenToSim(clientX, clientY)
|
||||||
this.sim.objects.handlePointerDown({x, y});
|
this.sim.objects.handlePointerDown({x, y});
|
||||||
|
|
||||||
} else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) {
|
} else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) {
|
||||||
this.panning = {
|
this.panning = {
|
||||||
gathering: true,
|
gathering: true,
|
||||||
viewOriginStart: this.sim.display.viewOrigin,
|
|
||||||
pointerStart: {x: clientX, y: clientY},
|
|
||||||
pointerCurrent: {x: clientX, y: clientY},
|
|
||||||
velocity: {x: 0, y: 0},
|
velocity: {x: 0, y: 0},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -170,8 +167,9 @@ export class Pointer {
|
|||||||
} else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) {
|
} else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) {
|
||||||
if (this.panning?.gathering) {
|
if (this.panning?.gathering) {
|
||||||
this.updatePointer({ x: clientX, y: clientY });
|
this.updatePointer({ x: clientX, y: clientY });
|
||||||
this.panning.pointerCurrent = {x: clientX, y: clientY};
|
const velocity = this.getPointerVelocity();
|
||||||
this.panning.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
|
// Apply update to viewOrigin based on panning
|
||||||
if (this.panning) {
|
if (this.panning) {
|
||||||
const {pointerStart, pointerCurrent, viewOriginStart, velocity} = this.sim.pointer.panning;
|
const {velocity} = this.panning;
|
||||||
// Convert pointer velocity to sim internal scale
|
// Convert pointer velocity to sim internal scale
|
||||||
const vx = velocity.x / this.sim.display.scale;
|
const vx = velocity.x / this.sim.display.scale;
|
||||||
const vy = velocity.y / this.sim.display.scale;
|
const vy = velocity.y / this.sim.display.scale;
|
||||||
this.panning.viewOriginStart = this.sim.display.viewOrigin;
|
this.sim.display.viewOrigin.x -= vx * elapsedTime;
|
||||||
this.panning.pointerStart = pointerCurrent;
|
this.sim.display.viewOrigin.y -= vy * elapsedTime;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
27
simulator.js
27
simulator.js
@ -12,6 +12,9 @@ import {
|
|||||||
SCALE_POWER_MIN,
|
SCALE_POWER_MIN,
|
||||||
DISPLAY_CURRENT_SCALE,
|
DISPLAY_CURRENT_SCALE,
|
||||||
DISPLAY_CURRENT_MODE,
|
DISPLAY_CURRENT_MODE,
|
||||||
|
PAN_VELOCITY_SCALE_FACTOR,
|
||||||
|
MOTION_TIME_SCALE,
|
||||||
|
MODE_PAN_VIEW,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
|
|
||||||
export class Sim {
|
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,
|
// 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.
|
// so use this to schedule it and let the event loop pick it up.
|
||||||
scheduleZoom({x, y}, factor) {
|
// velocity should be in Sim coordinate scale
|
||||||
this.nextZoom = {x, y, factor};
|
scheduleZoom({x, y}, factor, velocity) {
|
||||||
|
this.nextZoom = {x, y, factor, velocity};
|
||||||
}
|
}
|
||||||
|
|
||||||
// x, y should be in Sim coordinates
|
// 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
|
// 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
|
// the new view origin should be x, y minus half the new view width and height
|
||||||
// compute new scale
|
// compute new scale
|
||||||
@ -71,8 +76,18 @@ export class Sim {
|
|||||||
this.display.viewOrigin.y = y - this.display.height / 2;
|
this.display.viewOrigin.y = y - this.display.height / 2;
|
||||||
|
|
||||||
this.pointer.clearPointerHistory();
|
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
|
// Main loop
|
||||||
loop(currentTime) {
|
loop(currentTime) {
|
||||||
const elapsedTime = currentTime - this.time;
|
const elapsedTime = (currentTime - this.time) * MOTION_TIME_SCALE;
|
||||||
this.time = currentTime;
|
this.time = currentTime;
|
||||||
|
|
||||||
if (DISPLAY_CURRENT_MODE) {
|
if (DISPLAY_CURRENT_MODE) {
|
||||||
|
|||||||
@ -40,14 +40,15 @@ export class ModeSwitch extends Tool {
|
|||||||
button.innerHTML = `<h3>${modeTitle}</h3>`;
|
button.innerHTML = `<h3>${modeTitle}</h3>`;
|
||||||
button.classList.add('wide');
|
button.classList.add('wide');
|
||||||
|
|
||||||
button.addEventListener('click', (e) => this.setMode(modeID));
|
button.addEventListener('click', (e) => this.setCurrentMode(modeID));
|
||||||
}
|
}
|
||||||
|
|
||||||
// First listed mode is the default
|
// First listed mode is the default
|
||||||
const [[currentModeID, _]] = this.modes;
|
const [[currentModeID, _]] = this.modes;
|
||||||
this.setMode(currentModeID);
|
this.setCurrentMode(currentModeID);
|
||||||
|
|
||||||
// Add global method to get current mode / check mode
|
// Add global method to set/get current mode
|
||||||
|
this.sim.setCurrentMode = (modeID) => this.setCurrentMode(modeID);
|
||||||
this.sim.getCurrentMode = () => this.currentMode;
|
this.sim.getCurrentMode = () => this.currentMode;
|
||||||
this.sim.isCurrentMode = (modeID) => modeID === this.currentMode;
|
this.sim.isCurrentMode = (modeID) => modeID === this.currentMode;
|
||||||
this.sim.onModeLeave = (modeID, cb) => this.onModeLeave(modeID, cb);
|
this.sim.onModeLeave = (modeID, cb) => this.onModeLeave(modeID, cb);
|
||||||
@ -60,7 +61,7 @@ export class ModeSwitch extends Tool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setMode(modeID) {
|
setCurrentMode(modeID) {
|
||||||
if (modeID === this.currentMode) return;
|
if (modeID === this.currentMode) return;
|
||||||
const leave = new CustomEvent(EVENT_MODE_LEAVE, {detail: {modeID: this.currentMode}});
|
const leave = new CustomEvent(EVENT_MODE_LEAVE, {detail: {modeID: this.currentMode}});
|
||||||
const enter = new CustomEvent(EVENT_MODE_LEAVE, {detail: {modeID}});
|
const enter = new CustomEvent(EVENT_MODE_LEAVE, {detail: {modeID}});
|
||||||
|
|||||||
@ -21,6 +21,7 @@ export class PlayPause extends Tool {
|
|||||||
|
|
||||||
pauseButton.addEventListener('click', (e) => {
|
pauseButton.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
this.sim.pointer.panning = undefined;
|
||||||
if (this.playing) {
|
if (this.playing) {
|
||||||
this.playing = false;
|
this.playing = false;
|
||||||
pauseButton.style.opacity = '50%';
|
pauseButton.style.opacity = '50%';
|
||||||
|
|||||||
30
tool/zoom.js
30
tool/zoom.js
@ -43,28 +43,30 @@ export class Zoom extends Tool {
|
|||||||
const box = this.sim.objects.boundingBox;
|
const box = this.sim.objects.boundingBox;
|
||||||
const x = (box.start.x + box.end.x) / 2;
|
const x = (box.start.x + box.end.x) / 2;
|
||||||
const y = (box.start.y + box.end.y) / 2;
|
const y = (box.start.y + box.end.y) / 2;
|
||||||
if (box.start.x !== box.end.x && box.start.y !== box.end.y) {
|
|
||||||
const widthRatio = Math.abs(box.start.x - box.end.x) / this.sim.display.width;
|
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 heightRatio = Math.abs(box.start.y - box.end.y) / this.sim.display.height;
|
||||||
const biggerRatio = Math.max(widthRatio, heightRatio);
|
const biggerRatio = Math.max(widthRatio, heightRatio);
|
||||||
const base2factor = Math.log(1/biggerRatio) / Math.log(2);
|
const base2factor = Math.log(1/biggerRatio) / Math.log(2) - 0.5;
|
||||||
const factor = Math.floor(base2factor) - 1;
|
const factor = Math.floor(base2factor);
|
||||||
this.sim.scheduleZoom({x, y}, factor);
|
// Determine average momentum and set panning velocity to match
|
||||||
}
|
const netMomentum = {x: 0, y: 0};
|
||||||
// Determine average velocity and set panning velocity to match
|
let totalMass = 0;
|
||||||
const totalVelocity = {x: 0, y: 0};
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
this.sim.objects.forEachObject(obj => {
|
this.sim.objects.forEachObject(obj => {
|
||||||
count++;
|
count++;
|
||||||
totalVelocity.x += obj.velocity.x;
|
netMomentum.x += obj.mass * obj.velocity.x;
|
||||||
totalVelocity.y += obj.velocity.y;
|
netMomentum.y += obj.mass * obj.velocity.y;
|
||||||
|
totalMass += obj.mass;
|
||||||
});
|
});
|
||||||
const vx = totalVelocity.x / count;
|
if (!count) {
|
||||||
const vy = totalVelocity.y / count;
|
return;
|
||||||
console.log('zoom, pan', vx, vy);
|
}
|
||||||
this.sim.pointer.panning = {
|
const netVelocity = {
|
||||||
velocity: {x: vx, y: vy},
|
x: netMomentum.x / totalMass,
|
||||||
|
y: netMomentum.y / totalMass,
|
||||||
};
|
};
|
||||||
|
const netSpeed = Math.sqrt(netVelocity.x ** 2 + netVelocity.y ** 2);
|
||||||
|
this.sim.scheduleZoom({x, y}, factor, netVelocity)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user