fix zoom and pan
This commit is contained in:
parent
f5512c1be3
commit
a50c7c2a58
@ -23,7 +23,6 @@ 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;
|
||||||
@ -50,6 +49,8 @@ export const MODE_MASS_GENERATION = 'mass-gen';
|
|||||||
export const MODE_PAN_VIEW = 'pan-view';
|
export const MODE_PAN_VIEW = 'pan-view';
|
||||||
export const MODE_OBJECT_SELECT = 'select';
|
export const MODE_OBJECT_SELECT = 'select';
|
||||||
|
|
||||||
// LOCAL STORAGE PREFIXES/SUFFIXES
|
// LOCAL STORAGE NAMESPACES
|
||||||
export const TOOLBAR_EXPANDED_SUFFIX = 'lhg-toolbar-expanded';
|
export const TOOLBAR_EXPANDED_SUFFIX = 'lhg-toolbar-expanded';
|
||||||
|
export const OBJECT_MAGIC_PROP_PREFIX = '_lhg_';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
31
display.js
31
display.js
@ -145,12 +145,33 @@ export class Display {
|
|||||||
// Direct translate
|
// Direct translate
|
||||||
this.viewOrigin.x = start.viewOrigin.x - (latest.x - start.x) / this.scale;
|
this.viewOrigin.x = start.viewOrigin.x - (latest.x - start.x) / this.scale;
|
||||||
this.viewOrigin.y = start.viewOrigin.y - (latest.y - start.y) / this.scale;
|
this.viewOrigin.y = start.viewOrigin.y - (latest.y - start.y) / this.scale;
|
||||||
} else if (this.sim.panning && !this.sim.panning.paused) {
|
}
|
||||||
|
|
||||||
|
let pdx = 0;
|
||||||
|
let pdy = 0;
|
||||||
|
|
||||||
|
if (this.sim.panning && !this.sim.panning.paused) {
|
||||||
// Apply update to viewOrigin based on panning
|
// Apply update to viewOrigin based on panning
|
||||||
const { velocity } = this.sim.panning;
|
pdx = this.sim.panning.velocity.x * elapsedTime;
|
||||||
// TODO: something with time scale? Panning is too fast.
|
pdy = this.sim.panning.velocity.y * elapsedTime;
|
||||||
this.viewOrigin.x += velocity.x * elapsedTime;
|
}
|
||||||
this.viewOrigin.y += velocity.y * elapsedTime;
|
|
||||||
|
this.viewOrigin.x += pdx;
|
||||||
|
this.viewOrigin.y += pdy;
|
||||||
|
|
||||||
|
if (start && latest) {
|
||||||
|
// Update what's considered start
|
||||||
|
start.viewOrigin = {...this.viewOrigin};
|
||||||
|
start.x = latest.x;
|
||||||
|
start.y = latest.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.sim.getOption('debug.panningInfo')) {
|
||||||
|
const {x, y} = this.sim.panning?.velocity ?? {};
|
||||||
|
this.sim.info['Panning Velocity'] = [`${x?.toPrecision(6)}, `, y?.toPrecision(6)];
|
||||||
|
const { centerOfMass } = this.sim.system.computeSystemCenter();
|
||||||
|
this.sim.info['Center of Mass'] = [`${centerOfMass.x.toPrecision(6)}, `, centerOfMass.y.toPrecision(6)];
|
||||||
|
this.sim.info['Net Angular Momentum'] = this.sim.system.computeSystemAngularMomentum().toPrecision(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear canvas in preparation for other modules to render this frame
|
// Clear canvas in preparation for other modules to render this frame
|
||||||
|
|||||||
@ -145,7 +145,7 @@ export class MassObject {
|
|||||||
const arrowDirection = Math.atan2(py - cy, px - cx);
|
const arrowDirection = Math.atan2(py - cy, px - cx);
|
||||||
// Length of arrow based on distance (logarithmic scale)
|
// Length of arrow based on distance (logarithmic scale)
|
||||||
const distance = Math.sqrt((x - px) ** 2, (y - py) ** 2);
|
const distance = Math.sqrt((x - px) ** 2, (y - py) ** 2);
|
||||||
const arrowLength = Math.log(distance) * OFFSCREEN_OBJECT_LINE_SCALE / this.sim.display.scale;
|
const arrowLength = Math.log(distance + 1) * OFFSCREEN_OBJECT_LINE_SCALE / this.sim.display.scale;
|
||||||
const startAx = px - arrowLength * Math.cos(arrowDirection);
|
const startAx = px - arrowLength * Math.cos(arrowDirection);
|
||||||
const startAy = py - arrowLength * Math.sin(arrowDirection);
|
const startAy = py - arrowLength * Math.sin(arrowDirection);
|
||||||
sim.display.drawArrow(startAx, startAy, px, py, {
|
sim.display.drawArrow(startAx, startAy, px, py, {
|
||||||
@ -176,8 +176,8 @@ export class MassObject {
|
|||||||
{x: vx, y: vy};
|
{x: vx, y: vy};
|
||||||
const speed = Math.sqrt(velocity.x ** 2, velocity.y ** 2);
|
const speed = Math.sqrt(velocity.x ** 2, velocity.y ** 2);
|
||||||
const arrowDirection = Math.atan2(velocity.y, velocity.x);
|
const arrowDirection = Math.atan2(velocity.y, velocity.x);
|
||||||
// Prevent negative numbers by adding e
|
// Prevent negative numbers by adding 1
|
||||||
const arrowLength = Math.log(speed + 3) * vecScale / this.sim.display.scale;
|
const arrowLength = Math.log(speed + 1) * vecScale / this.sim.display.scale;
|
||||||
const endVx = x + arrowLength * Math.cos(arrowDirection);
|
const endVx = x + arrowLength * Math.cos(arrowDirection);
|
||||||
const endVy = y + arrowLength * Math.sin(arrowDirection);
|
const endVy = y + arrowLength * Math.sin(arrowDirection);
|
||||||
const style = VELOCITY_VECTOR_COLOR === 'object color' ?
|
const style = VELOCITY_VECTOR_COLOR === 'object color' ?
|
||||||
@ -197,7 +197,7 @@ export class MassObject {
|
|||||||
const accelerationMagnitude = Math.sqrt(acceleration.x ** 2 + acceleration.y ** 2);
|
const accelerationMagnitude = Math.sqrt(acceleration.x ** 2 + acceleration.y ** 2);
|
||||||
const arrowDirection = Math.atan2(acceleration.y, acceleration.x);
|
const arrowDirection = Math.atan2(acceleration.y, acceleration.x);
|
||||||
// Prevent negative numbers by adding e
|
// Prevent negative numbers by adding e
|
||||||
const arrowLength = Math.log(accelerationMagnitude + 3) * vecScale / this.sim.display.scale;
|
const arrowLength = Math.log(accelerationMagnitude + 1) * vecScale / this.sim.display.scale;
|
||||||
const endAx = x + arrowLength * Math.cos(arrowDirection);
|
const endAx = x + arrowLength * Math.cos(arrowDirection);
|
||||||
const endAy = y + arrowLength * Math.sin(arrowDirection);
|
const endAy = y + arrowLength * Math.sin(arrowDirection);
|
||||||
const style = ACCELERATION_VECTOR_COLOR === 'object color' ?
|
const style = ACCELERATION_VECTOR_COLOR === 'object color' ?
|
||||||
|
|||||||
17
options.js
17
options.js
@ -1,12 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
EVENT_OPTION_SET,
|
EVENT_OPTION_SET,
|
||||||
|
OBJECT_MAGIC_PROP_PREFIX,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
|
|
||||||
export class Options {
|
export class Options {
|
||||||
sim = undefined;
|
sim = undefined;
|
||||||
options = undefined;
|
options = undefined;
|
||||||
values = {};
|
values = {};
|
||||||
undefinedObj = {_undefined: true};
|
undefinedObj = { [OBJECT_MAGIC_PROP_PREFIX + 'undefined']: true};
|
||||||
|
nullObj = { [OBJECT_MAGIC_PROP_PREFIX + 'null']: true};
|
||||||
|
|
||||||
getStorageKey(path) {
|
getStorageKey(path) {
|
||||||
return `${path}:options`;
|
return `${path}:options`;
|
||||||
@ -29,7 +31,6 @@ export class Options {
|
|||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
value = defaultValue;
|
value = defaultValue;
|
||||||
}
|
}
|
||||||
console.log('initializing option', {path, defaultValue, value});
|
|
||||||
this.values[path] = value;
|
this.values[path] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -37,15 +38,25 @@ export class Options {
|
|||||||
|
|
||||||
toStored(value) {
|
toStored(value) {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
|
// TODO: Do we want to interpret this as removing from storage?
|
||||||
|
// Let's just treat it as a value for now;
|
||||||
|
// Semantically it works because when retrieved, it will return undefined,
|
||||||
|
// which is the same result you get if the key is not set
|
||||||
return JSON.stringify(this.undefinedObj);
|
return JSON.stringify(this.undefinedObj);
|
||||||
|
} else if (value === null) {
|
||||||
|
return JSON.stringify(this.nullObj);
|
||||||
}
|
}
|
||||||
return JSON.stringify(value);
|
return JSON.stringify(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// value: string
|
// value: string
|
||||||
fromStored(value) {
|
fromStored(value) {
|
||||||
if (value === JSON.stringify(this.undefinedObj)) {
|
if (value === null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
} else if (value === JSON.stringify(this.undefinedObj)) {
|
||||||
|
return undefined;
|
||||||
|
} else if (value === JSON.stringify(this.nullObj)) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return JSON.parse(value);
|
return JSON.parse(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -132,8 +132,10 @@ export class Pointer {
|
|||||||
} else {
|
} else {
|
||||||
const v = {...this.latestVelocity};
|
const v = {...this.latestVelocity};
|
||||||
// Convert pointer velocity to simulation scale
|
// Convert pointer velocity to simulation scale
|
||||||
v.x /= this.sim.display.scale;
|
// Also multiply by -1 because the camera is panning opposite to
|
||||||
v.y /= this.sim.display.scale;
|
// the pointer velocity
|
||||||
|
v.x /= -this.sim.display.scale;
|
||||||
|
v.y /= -this.sim.display.scale;
|
||||||
|
|
||||||
this.sim.panning = {
|
this.sim.panning = {
|
||||||
velocity: v
|
velocity: v
|
||||||
@ -172,11 +174,9 @@ export class Pointer {
|
|||||||
frame() {
|
frame() {
|
||||||
// Add another entry for the current pointer position
|
// Add another entry for the current pointer position
|
||||||
const { pointerHistory } = this.sim.pointer ?? {};
|
const { pointerHistory } = this.sim.pointer ?? {};
|
||||||
console.log('pointer history length', pointerHistory?.length);
|
|
||||||
if (pointerHistory?.length) {
|
if (pointerHistory?.length) {
|
||||||
const currentPointer = pointerHistory[pointerHistory.length - 1];
|
const currentPointer = pointerHistory[pointerHistory.length - 1];
|
||||||
this.sim.pointer.updatePointer(currentPointer);
|
this.sim.pointer.updatePointer(currentPointer);
|
||||||
console.log('updating in case of idle pointer', currentPointer.x, currentPointer.y);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,9 +12,9 @@ export function initializeTools(sim) {
|
|||||||
sim.toolbars = {
|
sim.toolbars = {
|
||||||
tools: new Toolbar(sim, 'Tools'),
|
tools: new Toolbar(sim, 'Tools'),
|
||||||
modes: new Toolbar(sim, 'Modes'),
|
modes: new Toolbar(sim, 'Modes'),
|
||||||
utils: new Toolbar(sim, 'Utility', { expanded: false }),
|
utils: new Toolbar(sim, 'Utils', { expanded: false }),
|
||||||
options: new Toolbar(sim, 'Options'),
|
options: new Toolbar(sim, 'Options'),
|
||||||
params: new Toolbar(sim, 'Parameters'),
|
params: new Toolbar(sim, 'Params'),
|
||||||
debug: new Toolbar(sim, 'Debug', { expanded: false }),
|
debug: new Toolbar(sim, 'Debug', { expanded: false }),
|
||||||
};
|
};
|
||||||
const { tools, modes, options, params, debug, utils } = sim.toolbars;
|
const { tools, modes, options, params, debug, utils } = sim.toolbars;
|
||||||
|
|||||||
89
simulator.js
89
simulator.js
@ -1,12 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
EVENT_ZOOM,
|
EVENT_ZOOM,
|
||||||
FRAMERATE_SAMPLE_DURATION,
|
FRAMERATE_SAMPLE_DURATION,
|
||||||
SCALE_POWER_MAX,
|
|
||||||
SCALE_POWER_MIN,
|
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
import { Display } from './display.js';
|
import { Display } from './display.js';
|
||||||
import { System } from './system.js';
|
import { System } from './system.js';
|
||||||
import { Options } from './options.js';
|
import { Options } from './options.js';
|
||||||
|
import { Zoom } from './zoom.js';
|
||||||
import { simOptions } from './sim-options.js';
|
import { simOptions } from './sim-options.js';
|
||||||
import { initializeTools } from './sim-tools.js';
|
import { initializeTools } from './sim-tools.js';
|
||||||
|
|
||||||
@ -19,12 +18,14 @@ export class Sim {
|
|||||||
playing = true;
|
playing = true;
|
||||||
recentFrames = [];
|
recentFrames = [];
|
||||||
frameRate = 0;
|
frameRate = 0;
|
||||||
|
panning = undefined;
|
||||||
|
|
||||||
system = undefined;
|
system = undefined;
|
||||||
display = undefined;
|
display = undefined;
|
||||||
overlay = undefined;
|
overlay = undefined;
|
||||||
pointer = undefined;
|
pointer = undefined;
|
||||||
panning = undefined;
|
zoom = undefined;
|
||||||
|
|
||||||
toolbarGroups = {};
|
toolbarGroups = {};
|
||||||
toolbars = {};
|
toolbars = {};
|
||||||
|
|
||||||
@ -36,6 +37,24 @@ export class Sim {
|
|||||||
onModeEnter = () => undefined;
|
onModeEnter = () => undefined;
|
||||||
onModeLeave = () => undefined;
|
onModeLeave = () => undefined;
|
||||||
|
|
||||||
|
constructor(divId) {
|
||||||
|
this.divId = divId;
|
||||||
|
const div = document.getElementById(this.divId);
|
||||||
|
this.div = div;
|
||||||
|
|
||||||
|
this.options = new Options(this, simOptions);
|
||||||
|
this.display = new Display(this);
|
||||||
|
this.system = new System(this);
|
||||||
|
this.zoom = new Zoom(this);
|
||||||
|
|
||||||
|
initializeTools(this);
|
||||||
|
|
||||||
|
// Initiate main loop
|
||||||
|
this.rawTime = document.timeline.currentTime;
|
||||||
|
this.time = 0;
|
||||||
|
requestAnimationFrame(t => this.loop(t));
|
||||||
|
}
|
||||||
|
|
||||||
markFrame(t) {
|
markFrame(t) {
|
||||||
const { recentFrames: rfs } = this;
|
const { recentFrames: rfs } = this;
|
||||||
rfs.push(t);
|
rfs.push(t);
|
||||||
@ -50,54 +69,9 @@ export class Sim {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(divId) {
|
|
||||||
this.divId = divId;
|
|
||||||
const div = document.getElementById(this.divId);
|
|
||||||
this.div = div;
|
|
||||||
|
|
||||||
this.options = new Options(this, simOptions);
|
|
||||||
this.display = new Display(this);
|
|
||||||
this.system = new System(this);
|
|
||||||
|
|
||||||
initializeTools(this);
|
|
||||||
|
|
||||||
// Initiate main loop
|
|
||||||
this.rawTime = document.timeline.currentTime;
|
|
||||||
this.time = 0;
|
|
||||||
requestAnimationFrame(t => this.loop(t));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
// velocity should be in Sim coordinate scale
|
// velocity should be in Sim coordinate scale
|
||||||
scheduleZoom({x, y}, factor, velocity) {
|
scheduleZoom({x, y}, factor, velocity) {
|
||||||
this.nextZoom = {x, y, factor, velocity};
|
this.zoom.scheduleZoom({x, y}, factor, velocity);
|
||||||
}
|
|
||||||
|
|
||||||
// x, y should be in Sim coordinates
|
|
||||||
// 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
|
|
||||||
this.display.scalePower += factor;
|
|
||||||
// TODO: Lossy rescaling to expand zoom range
|
|
||||||
if (this.display.scalePower > SCALE_POWER_MAX) this.display.scalePower = SCALE_POWER_MAX;
|
|
||||||
if (this.display.scalePower < SCALE_POWER_MIN) this.display.scalePower = SCALE_POWER_MIN;
|
|
||||||
// compute coordinates of new view frame
|
|
||||||
this.display.viewOrigin.x = x - this.display.width / 2;
|
|
||||||
this.display.viewOrigin.y = y - this.display.height / 2;
|
|
||||||
|
|
||||||
this.pointer.clearPointerHistory();
|
|
||||||
|
|
||||||
if (this.playing && velocity) {
|
|
||||||
this.panning = {
|
|
||||||
velocity: { ...velocity }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const e = new CustomEvent(EVENT_ZOOM);
|
|
||||||
this.div.dispatchEvent(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform display coordinates to simulator coordinates using scale and viewOrigin
|
// Transform display coordinates to simulator coordinates using scale and viewOrigin
|
||||||
@ -115,7 +89,8 @@ export class Sim {
|
|||||||
|
|
||||||
getScaleDisplay() {
|
getScaleDisplay() {
|
||||||
const scale = 2 ** Math.abs(this.display.scalePower);
|
const scale = 2 ** Math.abs(this.display.scalePower);
|
||||||
return this.display.scalePower >= 0 ? `${scale}` : `1/${scale}`;
|
const scaleText = this.display.scalePower >= 0 ? `${scale}` : `1/${scale}`;
|
||||||
|
return `${scaleText} (${this.display.scalePower})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// cb: () => undefined
|
// cb: () => undefined
|
||||||
@ -145,19 +120,7 @@ export class Sim {
|
|||||||
this.info['Frame Rate'] = this.frameRate?.toPrecision(3);
|
this.info['Frame Rate'] = this.frameRate?.toPrecision(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.nextZoom) {
|
this.zoom.frame();
|
||||||
this.zoom(this.nextZoom);
|
|
||||||
this.nextZoom = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.getOption('debug.panningInfo')) {
|
|
||||||
const {x, y} = this.panning?.velocity ?? {};
|
|
||||||
this.info['Panning Velocity'] = [`${x?.toPrecision(6)}, `, y?.toPrecision(6)];
|
|
||||||
const { centerOfMass } = this.system.computeSystemCenter();
|
|
||||||
this.info['Center of Mass'] = [`${centerOfMass.x.toPrecision(6)}, `, centerOfMass.y.toPrecision(6)];
|
|
||||||
this.info['Net Angular Momentum'] = this.system.computeSystemAngularMomentum().toPrecision(6);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.display.frame(elapsedTime);
|
this.display.frame(elapsedTime);
|
||||||
this.system.frame(elapsedTime);
|
this.system.frame(elapsedTime);
|
||||||
this.overlay.frame();
|
this.overlay.frame();
|
||||||
|
|||||||
41
system.js
41
system.js
@ -1,5 +1,4 @@
|
|||||||
import { MassObject } from './object.js';
|
import { MassObject } from './object.js';
|
||||||
import { ZOOM_TO_FIT_PADDING } from './config.js';
|
|
||||||
|
|
||||||
export class System {
|
export class System {
|
||||||
objects = [];
|
objects = [];
|
||||||
@ -88,25 +87,39 @@ export class System {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get boundingBox() {
|
get boundingBox() {
|
||||||
const box = this.reduce((acc, obj) => {
|
const box = this.reduce(({start, end}, obj) => {
|
||||||
if (acc.start.x === undefined) {
|
const lx = obj.position.x - obj.radius;
|
||||||
acc.start = {...obj.position};
|
const gx = obj.position.x + obj.radius;
|
||||||
acc.end = {...obj.position};
|
const ly = obj.position.y - obj.radius;
|
||||||
} else {
|
const gy = obj.position.y + obj.radius;
|
||||||
if (obj.position.x < acc.start.x) acc.start.x = obj.position.x;
|
let ret;
|
||||||
if (obj.position.x > acc.end.x) acc.end.x = obj.position.x;
|
if (start.x === undefined) {
|
||||||
if (obj.position.y < acc.start.y) acc.start.y = obj.position.y;
|
ret = {
|
||||||
if (obj.position.y > acc.end.y) acc.end.y = obj.position.y;
|
start: {x: lx, y: ly},
|
||||||
|
end: {x: gx, y: gy},
|
||||||
|
};
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
ret = {
|
||||||
|
start: {
|
||||||
|
x: Math.min(start.x, lx),
|
||||||
|
y: Math.min(start.y, ly),
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
x: Math.max(end.x, gx),
|
||||||
|
y: Math.max(end.y, gy),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return ret;
|
||||||
}, {
|
}, {
|
||||||
start: {x: undefined, y: undefined},
|
start: {x: undefined, y: undefined},
|
||||||
end: {x: undefined, y: undefined},
|
end: {x: undefined, y: undefined},
|
||||||
});
|
});
|
||||||
|
|
||||||
box.start.x = (box.start.x ?? 0) - ZOOM_TO_FIT_PADDING;
|
box.start.x = (box.start.x ?? 0);
|
||||||
box.start.y = (box.start.y ?? 0) - ZOOM_TO_FIT_PADDING;
|
box.start.y = (box.start.y ?? 0);
|
||||||
box.end.x = (box.end.x ?? 0) + ZOOM_TO_FIT_PADDING;
|
box.end.x = (box.end.x ?? 0);
|
||||||
box.end.y = (box.end.y ?? 0) + ZOOM_TO_FIT_PADDING;
|
box.end.y = (box.end.y ?? 0);
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,10 +7,6 @@ import {
|
|||||||
export class UtilityTool extends Tool {
|
export class UtilityTool extends Tool {
|
||||||
currentTimeEl = undefined;
|
currentTimeEl = undefined;
|
||||||
|
|
||||||
get displayScaleText() {
|
|
||||||
return `Scale: ${this.sim.getScaleDisplay()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
get timeText() {
|
get timeText() {
|
||||||
let time = this.sim.time;
|
let time = this.sim.time;
|
||||||
// Time in milliseconds
|
// Time in milliseconds
|
||||||
@ -45,40 +41,27 @@ export class UtilityTool extends Tool {
|
|||||||
|
|
||||||
const zeroVelocity = document.createElement('button');
|
const zeroVelocity = document.createElement('button');
|
||||||
const clearTraces = document.createElement('button');
|
const clearTraces = document.createElement('button');
|
||||||
const zoomAll = document.createElement('button');
|
|
||||||
const currentScale = document.createElement('button')
|
|
||||||
const currentTime = document.createElement('button');
|
const currentTime = document.createElement('button');
|
||||||
const clearDebug = document.createElement('button');
|
const clearDebug = document.createElement('button');
|
||||||
|
|
||||||
this.currentTimeEl = currentTime;
|
this.currentTimeEl = currentTime;
|
||||||
|
|
||||||
this.div.appendChild(currentTime);
|
this.div.appendChild(currentTime);
|
||||||
this.div.appendChild(currentScale);
|
|
||||||
this.div.appendChild(zoomAll);
|
|
||||||
this.div.appendChild(zeroVelocity);
|
this.div.appendChild(zeroVelocity);
|
||||||
this.div.appendChild(clearTraces);
|
this.div.appendChild(clearTraces);
|
||||||
this.div.appendChild(clearDebug);
|
this.div.appendChild(clearDebug);
|
||||||
|
|
||||||
zeroVelocity.classList.add(WIDE_CLASSNAME);
|
zeroVelocity.classList.add(WIDE_CLASSNAME);
|
||||||
clearTraces.classList.add(WIDE_CLASSNAME);
|
clearTraces.classList.add(WIDE_CLASSNAME);
|
||||||
zoomAll.classList.add(WIDE_CLASSNAME);
|
|
||||||
currentScale.classList.add(WIDE_CLASSNAME);
|
|
||||||
currentScale.classList.add(TOOL_INFO_CLASSNAME);
|
|
||||||
currentTime.classList.add(TOOL_INFO_CLASSNAME);
|
currentTime.classList.add(TOOL_INFO_CLASSNAME);
|
||||||
currentTime.classList.add(WIDE_CLASSNAME);
|
currentTime.classList.add(WIDE_CLASSNAME);
|
||||||
clearDebug.classList.add(WIDE_CLASSNAME);
|
clearDebug.classList.add(WIDE_CLASSNAME);
|
||||||
|
|
||||||
zeroVelocity.innerHTML = 'Zero Momentum';
|
zeroVelocity.innerHTML = 'Zero Momentum';
|
||||||
clearTraces.innerHTML = 'Clear Traces';
|
clearTraces.innerHTML = 'Clear Traces';
|
||||||
zoomAll.innerHTML = 'Zoom to Fit';
|
|
||||||
currentScale.innerHTML = this.displayScaleText;
|
|
||||||
currentTime.innerHTML = this.timeText;
|
currentTime.innerHTML = this.timeText;
|
||||||
clearDebug.innerHTML = 'Clear Debug';
|
clearDebug.innerHTML = 'Clear Debug';
|
||||||
|
|
||||||
this.sim.onZoom(() => {
|
|
||||||
currentScale.innerHTML = this.displayScaleText;
|
|
||||||
});
|
|
||||||
|
|
||||||
zeroVelocity.addEventListener('click', () => {
|
zeroVelocity.addEventListener('click', () => {
|
||||||
// Determine center of mass and average momentum
|
// Determine center of mass and average momentum
|
||||||
const { totalMass, netMomentum } = this.sim.system.computeSystemCenter();
|
const { totalMass, netMomentum } = this.sim.system.computeSystemCenter();
|
||||||
@ -104,36 +87,6 @@ export class UtilityTool extends Tool {
|
|||||||
}, {alive: null});
|
}, {alive: null});
|
||||||
});
|
});
|
||||||
|
|
||||||
zoomAll.addEventListener('click', () => {
|
|
||||||
// Determine bounding box
|
|
||||||
const box = this.sim.system.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) - 0.5;
|
|
||||||
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.system.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)
|
|
||||||
});
|
|
||||||
|
|
||||||
clearDebug.addEventListener('click', () => {
|
clearDebug.addEventListener('click', () => {
|
||||||
this.sim.info = {};
|
this.sim.info = {};
|
||||||
});
|
});
|
||||||
|
|||||||
42
tool/zoom.js
42
tool/zoom.js
@ -2,20 +2,42 @@ import { Tool } from '../tool.js';
|
|||||||
import {
|
import {
|
||||||
ZOOM_IN_FACTOR,
|
ZOOM_IN_FACTOR,
|
||||||
ZOOM_OUT_FACTOR,
|
ZOOM_OUT_FACTOR,
|
||||||
|
WIDE_CLASSNAME,
|
||||||
|
TALL_CLASSNAME,
|
||||||
|
TOOL_INFO_CLASSNAME,
|
||||||
} from '../config.js';
|
} from '../config.js';
|
||||||
|
|
||||||
export class Zoom extends Tool {
|
export class Zoom extends Tool {
|
||||||
|
get displayScaleText() {
|
||||||
|
return `Scale: ${this.sim.getScaleDisplay()}`;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(container) {
|
constructor(container) {
|
||||||
super(container);
|
super(container);
|
||||||
|
|
||||||
|
const currentScale = document.createElement('button')
|
||||||
const zoomOut = document.createElement('button');
|
const zoomOut = document.createElement('button');
|
||||||
const zoomIn = document.createElement('button');
|
const zoomIn = document.createElement('button');
|
||||||
|
const zoomAll = document.createElement('button');
|
||||||
|
|
||||||
|
this.div.appendChild(currentScale);
|
||||||
this.div.appendChild(zoomOut);
|
this.div.appendChild(zoomOut);
|
||||||
this.div.appendChild(zoomIn);
|
this.div.appendChild(zoomIn);
|
||||||
|
this.div.appendChild(zoomAll);
|
||||||
|
|
||||||
|
currentScale.classList.add(WIDE_CLASSNAME);
|
||||||
|
currentScale.classList.add(TOOL_INFO_CLASSNAME);
|
||||||
|
zoomAll.classList.add(WIDE_CLASSNAME);
|
||||||
|
zoomAll.classList.add(TALL_CLASSNAME);
|
||||||
|
|
||||||
|
currentScale.innerHTML = this.displayScaleText;
|
||||||
zoomOut.innerHTML = 'Zoom<br>Out';
|
zoomOut.innerHTML = 'Zoom<br>Out';
|
||||||
zoomIn.innerHTML = 'Zoom<br>In';
|
zoomIn.innerHTML = 'Zoom<br>In';
|
||||||
|
zoomAll.innerHTML = 'Zoom to Fit';
|
||||||
|
|
||||||
|
this.sim.onZoom(() => {
|
||||||
|
currentScale.innerHTML = this.displayScaleText;
|
||||||
|
});
|
||||||
|
|
||||||
zoomOut.addEventListener('click', () => {
|
zoomOut.addEventListener('click', () => {
|
||||||
// Aim at center of view
|
// Aim at center of view
|
||||||
@ -30,5 +52,25 @@ export class Zoom extends Tool {
|
|||||||
const y = this.sim.display.height * 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);
|
this.sim.scheduleZoom(this.sim.screenToSim(x, y), ZOOM_IN_FACTOR);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
zoomAll.addEventListener('click', () => {
|
||||||
|
// Determine bounding box
|
||||||
|
const box = this.sim.system.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.log2(1 / biggerRatio) - 1;
|
||||||
|
const factor = Math.ceil(base2factor);
|
||||||
|
|
||||||
|
// Determine average momentum and set panning velocity to match
|
||||||
|
const { netMomentum, totalMass } = this.sim.system.computeSystemCenter();
|
||||||
|
const netVelocity = {
|
||||||
|
x: netMomentum.x / totalMass,
|
||||||
|
y: netMomentum.y / totalMass,
|
||||||
|
};
|
||||||
|
this.sim.scheduleZoom({x, y}, factor, netVelocity)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
zoom.js
Normal file
58
zoom.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
EVENT_ZOOM,
|
||||||
|
SCALE_POWER_MAX,
|
||||||
|
SCALE_POWER_MIN,
|
||||||
|
} from './config.js';
|
||||||
|
|
||||||
|
export class Zoom {
|
||||||
|
sim = undefined;
|
||||||
|
nextZoom = undefined;
|
||||||
|
|
||||||
|
constructor(sim) {
|
||||||
|
this.sim = sim;
|
||||||
|
}
|
||||||
|
|
||||||
|
// velocity should be in Sim coordinate scale
|
||||||
|
scheduleZoom({x, y}, factor, velocity) {
|
||||||
|
this.nextZoom = {x, y, factor, velocity};
|
||||||
|
}
|
||||||
|
|
||||||
|
frame() {
|
||||||
|
if (this.nextZoom) {
|
||||||
|
this.zoom(this.nextZoom);
|
||||||
|
this.nextZoom = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// x, y should be in Sim coordinates
|
||||||
|
// velocity should be in Sim coordinate scale
|
||||||
|
zoom({x, y, factor, velocity}) {
|
||||||
|
const { display } = this.sim;
|
||||||
|
// 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
|
||||||
|
// TODO: Lossy rescaling to expand zoom range
|
||||||
|
let scalePower = display.scalePower + factor;
|
||||||
|
scalePower = Math.max(scalePower, SCALE_POWER_MIN);
|
||||||
|
scalePower = Math.min(scalePower, SCALE_POWER_MAX);
|
||||||
|
this.sim.display.scalePower = scalePower;
|
||||||
|
|
||||||
|
// compute coordinates of new view frame
|
||||||
|
display.viewOrigin.x = x - display.width / 2;
|
||||||
|
display.viewOrigin.y = y - display.height / 2;
|
||||||
|
|
||||||
|
// Pointer history is stored in client coordinates, so we shouldn't need to clear it?
|
||||||
|
// this.pointer.clearPointerHistory();
|
||||||
|
|
||||||
|
// TODO: If paused, set panning velocity on resume
|
||||||
|
if (this.sim.playing && velocity) {
|
||||||
|
this.sim.panning = {
|
||||||
|
velocity: { ...velocity }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const e = new CustomEvent(EVENT_ZOOM);
|
||||||
|
this.sim.div.dispatchEvent(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user