refactored panning to its own class
This commit is contained in:
parent
0483a8ab52
commit
eac659822b
@ -45,6 +45,7 @@ export const EVENT_MODE_LEAVE = 'lhg-mode-leave';
|
|||||||
export const EVENT_MODE_ENTER = 'lhg-mode-enter';
|
export const EVENT_MODE_ENTER = 'lhg-mode-enter';
|
||||||
export const EVENT_ZOOM = 'lhg-zoom-event';
|
export const EVENT_ZOOM = 'lhg-zoom-event';
|
||||||
export const EVENT_OPTION_SET = 'lhg-option-set';
|
export const EVENT_OPTION_SET = 'lhg-option-set';
|
||||||
|
export const EVENT_PLAY_PAUSE = 'lhg-play-pause';
|
||||||
|
|
||||||
// MODES
|
// MODES
|
||||||
export const MODE_MASS_GENERATION = 'mass-gen';
|
export const MODE_MASS_GENERATION = 'mass-gen';
|
||||||
|
|||||||
36
display.js
36
display.js
@ -22,41 +22,7 @@ export class Display {
|
|||||||
window.addEventListener('resize', () => this.fullscreen());
|
window.addEventListener('resize', () => this.fullscreen());
|
||||||
}
|
}
|
||||||
|
|
||||||
frame(elapsedTime) {
|
frame() {
|
||||||
const {
|
|
||||||
touchStart: start,
|
|
||||||
touchLatest: latest,
|
|
||||||
} = this.sim.pointer ?? {};
|
|
||||||
|
|
||||||
// Direct translate, unless using fast panning
|
|
||||||
if (start && latest && !this.sim.getOption('compensate.fastPanning')) {
|
|
||||||
this.viewOrigin.x = start.viewOrigin.x - (latest.x - start.x) / this.scale;
|
|
||||||
this.viewOrigin.y = start.viewOrigin.y - (latest.y - start.y) / this.scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply update to viewOrigin based on panning
|
|
||||||
if (this.sim.panning && !this.sim.panning.paused) {
|
|
||||||
// elapsedTime is scaled by time scale, is that what we want?
|
|
||||||
// Yes because if panning.velocity == obj.velocity, object should stay in view
|
|
||||||
this.viewOrigin.x += this.sim.panning.velocity.x * elapsedTime;
|
|
||||||
this.viewOrigin.y += this.sim.panning.velocity.y * elapsedTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update what's considered start
|
|
||||||
if (start && latest) {
|
|
||||||
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
|
||||||
this.fillCanvas();
|
this.fillCanvas();
|
||||||
}
|
}
|
||||||
|
|||||||
62
object.js
62
object.js
@ -13,6 +13,7 @@ import {
|
|||||||
VELOCITY_VECTOR_COLOR,
|
VELOCITY_VECTOR_COLOR,
|
||||||
VELOCITY_VECTOR_WIDTH,
|
VELOCITY_VECTOR_WIDTH,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
|
import {add, components, direction, div, magnitude} from './vector.js';
|
||||||
|
|
||||||
export class MassObject {
|
export class MassObject {
|
||||||
sim = undefined;
|
sim = undefined;
|
||||||
@ -80,7 +81,7 @@ export class MassObject {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
display: {
|
display: {
|
||||||
ctx,
|
ctx,
|
||||||
height: H,
|
height: H,
|
||||||
width: W,
|
width: W,
|
||||||
viewOrigin: {x: ox, y: oy},
|
viewOrigin: {x: ox, y: oy},
|
||||||
@ -175,36 +176,35 @@ export class MassObject {
|
|||||||
|
|
||||||
// Draw arrow for the velocity
|
// Draw arrow for the velocity
|
||||||
if (sim.getOption('display.velocity')) {
|
if (sim.getOption('display.velocity')) {
|
||||||
// If this object is being dragged by the user,
|
|
||||||
// show the pointer velocity instead of object velocity
|
|
||||||
const vecScale = this.sim.getOption('display.velocityScale');
|
const vecScale = this.sim.getOption('display.velocityScale');
|
||||||
const selected = this.sim.system.getSelectedOrCreating();
|
const selected = this.sim.system.getSelectedOrCreating();
|
||||||
const isSelected = selected?.id === this.id;
|
const isSelected = selected?.id === this.id;
|
||||||
let velocity = {x: vx, y: vy};
|
let velocity = {x: vx, y: vy};
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
const pointerV = this.sim.pointer.latestVelocity;
|
// If this object is being dragged by the user,
|
||||||
// const panning = this.sim.panning?.velocity ?? {x: 0, y: 0};
|
// show the pointer velocity instead of object velocity
|
||||||
// velocity.x = vx + (pointerV.x + panning.x) * scale;
|
let pointerV = this.sim.pointer.latestVelocity;
|
||||||
// velocity.y = vy + (pointerV.y + panning.y) * scale;
|
|
||||||
if (this.sim.getOption('compensate.timeScale')) {
|
if (this.sim.getOption('compensate.timeScale')) {
|
||||||
velocity.x = vx + pointerV.x / this.sim.timeScale;
|
pointerV = div(pointerV, this.sim.timeScale);
|
||||||
velocity.y = vy + pointerV.y / this.sim.timeScale;
|
|
||||||
}
|
}
|
||||||
|
velocity = add(velocity, pointerV);
|
||||||
}
|
}
|
||||||
const speed = Math.sqrt(velocity.x ** 2, velocity.y ** 2) / this.sim.display.scale;
|
const speed = magnitude(velocity);
|
||||||
const arrowDirection = Math.atan2(velocity.y, velocity.x);
|
const arrowDirection = direction(velocity);
|
||||||
// Prevent negative numbers by adding 1
|
// Prevent negative numbers by adding 1
|
||||||
// TODO: Make logarithmic vector length scale optional
|
// TODO: Make logarithmic vector length scale optional
|
||||||
const arrowLength = Math.log10(speed + 1) * vecScale;
|
// Make arrow lengths appear consistent regardless of display scale
|
||||||
const offsetX = radius * Math.cos(arrowDirection);
|
let arrowLength = Math.log10(speed + 1) * vecScale;
|
||||||
const offsetY = radius * Math.sin(arrowDirection);
|
if (!this.sim.getOption('display.zoomVectors')) {
|
||||||
const startVx = x + offsetX;
|
arrowLength /= this.sim.display.scale;
|
||||||
const startVy = y + offsetY;
|
}
|
||||||
const endVx = startVx + arrowLength * Math.cos(arrowDirection);
|
// const offset = mult({x: Math.cos(
|
||||||
const endVy = startVy + arrowLength * Math.sin(arrowDirection);
|
const offset = components(radius, arrowDirection)
|
||||||
|
const start = add({x, y}, offset);
|
||||||
|
const end = add(start, components(arrowLength, arrowDirection));
|
||||||
const style = VELOCITY_VECTOR_COLOR === 'object color' ?
|
const style = VELOCITY_VECTOR_COLOR === 'object color' ?
|
||||||
`rgb(${r}, ${g}, ${b})` : VELOCITY_VECTOR_COLOR;
|
`rgb(${r}, ${g}, ${b})` : VELOCITY_VECTOR_COLOR;
|
||||||
sim.display.drawArrow(startVx, startVy, endVx, endVy, {
|
sim.display.drawArrow(start.x, start.y, end.x, end.y, {
|
||||||
style,
|
style,
|
||||||
width: VELOCITY_VECTOR_WIDTH,
|
width: VELOCITY_VECTOR_WIDTH,
|
||||||
arrowhead: VELOCITY_VECTOR_ARROWHEAD,
|
arrowhead: VELOCITY_VECTOR_ARROWHEAD,
|
||||||
@ -216,21 +216,21 @@ export class MassObject {
|
|||||||
// Draw arrow for acceleration
|
// Draw arrow for acceleration
|
||||||
if (sim.getOption('display.acceleration')) {
|
if (sim.getOption('display.acceleration')) {
|
||||||
const vecScale = this.sim.getOption('display.accelerationScale');
|
const vecScale = this.sim.getOption('display.accelerationScale');
|
||||||
const accelerationMagnitude = Math.sqrt(acceleration.x ** 2 + acceleration.y ** 2) /
|
const accel = magnitude(acceleration);
|
||||||
this.sim.display.scale;
|
const arrowDirection = direction(acceleration);
|
||||||
const arrowDirection = Math.atan2(acceleration.y, acceleration.x);
|
|
||||||
// Prevent negative numbers by adding 1
|
// Prevent negative numbers by adding 1
|
||||||
const arrowLength = Math.log10(accelerationMagnitude + 1) * vecScale;
|
// TODO: Make logarithmic vector length scale optional
|
||||||
//const arrowLength = accelerationMagnitude * vecScale;
|
// Make arrow lengths appear consistent regardless of display scale
|
||||||
const offsetX = radius * Math.cos(arrowDirection);
|
let arrowLength = Math.log10(accel + 1) * vecScale;
|
||||||
const offsetY = radius * Math.sin(arrowDirection);
|
if (!this.sim.getOption('display.zoomVectors')) {
|
||||||
const startAx = x + offsetX;
|
arrowLength /= this.sim.display.scale;
|
||||||
const startAy = y + offsetY;
|
}
|
||||||
const endAx = startAx + arrowLength * Math.cos(arrowDirection);
|
const offset = components(radius, arrowDirection)
|
||||||
const endAy = startAy + arrowLength * Math.sin(arrowDirection);
|
const start = add({x, y}, offset);
|
||||||
|
const end = add(start, components(arrowLength, arrowDirection));
|
||||||
const style = ACCELERATION_VECTOR_COLOR === 'object color' ?
|
const style = ACCELERATION_VECTOR_COLOR === 'object color' ?
|
||||||
`rgb(${r}, ${g}, ${b})` : ACCELERATION_VECTOR_COLOR;
|
`rgb(${r}, ${g}, ${b})` : ACCELERATION_VECTOR_COLOR;
|
||||||
sim.display.drawArrow(startAx, startAy, endAx, endAy, {
|
sim.display.drawArrow(start.x, start.y, end.x, end.y, {
|
||||||
style,
|
style,
|
||||||
width: ACCELERATION_VECTOR_WIDTH,
|
width: ACCELERATION_VECTOR_WIDTH,
|
||||||
arrowhead: ACCELERATION_VECTOR_ARROWHEAD,
|
arrowhead: ACCELERATION_VECTOR_ARROWHEAD,
|
||||||
|
|||||||
129
options-1.js
129
options-1.js
@ -1,129 +0,0 @@
|
|||||||
import {
|
|
||||||
EVENT_OPTION_SET,
|
|
||||||
OBJECT_MAGIC_PROP_PREFIX,
|
|
||||||
} from './config.js';
|
|
||||||
|
|
||||||
export class Options {
|
|
||||||
sim = undefined;
|
|
||||||
options = undefined;
|
|
||||||
values = {};
|
|
||||||
undefinedObj = {[OBJECT_MAGIC_PROP_PREFIX + 'undefined']: true};
|
|
||||||
nullObj = {[OBJECT_MAGIC_PROP_PREFIX + 'null']: true};
|
|
||||||
|
|
||||||
getStorageKey(path) {
|
|
||||||
return `${path}:options`;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(sim, options) {
|
|
||||||
this.sim = sim;
|
|
||||||
this.options = options;
|
|
||||||
|
|
||||||
// Global methods to get/set current option values
|
|
||||||
this.sim.getOption = (path) => this.getOption(path);
|
|
||||||
this.sim.setOption = (path, value) => this.setOption(path, value);
|
|
||||||
this.sim.onOptionSet = (path, cb) => this.onOptionSet(path, cb);
|
|
||||||
|
|
||||||
// Initialize values from localStorage
|
|
||||||
for (const groupName of Object.keys(options)) {
|
|
||||||
for (const [name, [, , defaultValue]] of Object.entries(this.options[groupName])) {
|
|
||||||
const path = [groupName, name].join('.');
|
|
||||||
let value = this.getFromLocalStorage(path);
|
|
||||||
if (value === undefined) {
|
|
||||||
value = defaultValue;
|
|
||||||
}
|
|
||||||
this.values[path] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toStored(value) {
|
|
||||||
if (value === undefined) {
|
|
||||||
// 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);
|
|
||||||
} else if (value === null) {
|
|
||||||
return JSON.stringify(this.nullObj);
|
|
||||||
}
|
|
||||||
return JSON.stringify(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// value: string
|
|
||||||
fromStored(value) {
|
|
||||||
if (value === null) {
|
|
||||||
return undefined;
|
|
||||||
} else if (value === JSON.stringify(this.undefinedObj)) {
|
|
||||||
return undefined;
|
|
||||||
} else if (value === JSON.stringify(this.nullObj)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return JSON.parse(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
getFromLocalStorage(path) {
|
|
||||||
const storageKey = this.getStorageKey(path);
|
|
||||||
const value = this.fromStored(window.localStorage.getItem(storageKey));
|
|
||||||
this.values[path] = value;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
getOption(path) {
|
|
||||||
const [groupName, name] = path.split('.');
|
|
||||||
const group = this.options[groupName];
|
|
||||||
const item = group[name];
|
|
||||||
const {type} = item;
|
|
||||||
const value = this.values[path];
|
|
||||||
switch (type) {
|
|
||||||
case 'number': return Number(value);
|
|
||||||
case 'boolean': return value === true || value === 'true';
|
|
||||||
default: {
|
|
||||||
console.log({
|
|
||||||
path, groupName, name,
|
|
||||||
group, item,
|
|
||||||
type, value
|
|
||||||
});
|
|
||||||
throw new Error('unknown option type');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setOption(path, value) {
|
|
||||||
this.values[path] = value;
|
|
||||||
const storageKey = this.getStorageKey(path);
|
|
||||||
window.localStorage.setItem(storageKey, this.toStored(value));
|
|
||||||
const e = new CustomEvent(EVENT_OPTION_SET, {detail: {path, value}});
|
|
||||||
this.sim.div.dispatchEvent(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// cb: (value) => undefined
|
|
||||||
onOptionSet(path, cb) {
|
|
||||||
this.sim.div.addEventListener(EVENT_OPTION_SET, (e) => {
|
|
||||||
if (path === e.detail.path) {
|
|
||||||
cb(e.detail.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getSection(sectionName) {
|
|
||||||
const section = this.options[sectionName];
|
|
||||||
const group = {
|
|
||||||
type: 'group',
|
|
||||||
name: sectionName,
|
|
||||||
title: section._title,
|
|
||||||
items: [],
|
|
||||||
};
|
|
||||||
for (const name in section) {
|
|
||||||
if (name.startsWith('_')) continue;
|
|
||||||
const [title, type, defaultValue, opts] = section[name];
|
|
||||||
group.items.push({
|
|
||||||
name,
|
|
||||||
type,
|
|
||||||
title,
|
|
||||||
default: defaultValue,
|
|
||||||
...opts
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return group;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -96,11 +96,11 @@ export class Options {
|
|||||||
this.sim.div.dispatchEvent(e);
|
this.sim.div.dispatchEvent(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// cb: (value) => undefined
|
// cb: ({path, value}) => undefined
|
||||||
onOptionSet(path, cb) {
|
onOptionSet(path, cb) {
|
||||||
this.sim.div.addEventListener(EVENT_OPTION_SET, (e) => {
|
this.sim.div.addEventListener(EVENT_OPTION_SET, (e) => {
|
||||||
if (path === e.detail.path) {
|
if (!path || path === e.detail.path) {
|
||||||
cb(e.detail.value);
|
cb({ path, value: e.detail.value });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
117
panning.js
Normal file
117
panning.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import {add, copy, div, mult, sub, zero} from "./vector.js";
|
||||||
|
|
||||||
|
export class Panning {
|
||||||
|
sim = undefined;
|
||||||
|
touchStart = undefined;
|
||||||
|
touchLatest = undefined;
|
||||||
|
paused = false;
|
||||||
|
velocity = zero;
|
||||||
|
|
||||||
|
constructor(sim) {
|
||||||
|
this.sim = sim;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerDown({x, y}) {
|
||||||
|
this.initializeTouch({x, y});
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeTouch({x, y}) {
|
||||||
|
this.touchStart = {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
t: this.sim.rawTime,
|
||||||
|
viewOrigin: copy(this.sim.display.viewOrigin),
|
||||||
|
};
|
||||||
|
this.touchLatest = {
|
||||||
|
...this.touchStart,
|
||||||
|
dx: 0,
|
||||||
|
dy: 0,
|
||||||
|
dt: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// With fast panning, panning velocity calculation happens every move;
|
||||||
|
// With normal panning, calculation only happens at pointer up.
|
||||||
|
handlePointerMove({x, y}) {
|
||||||
|
if (this.touchStart) {
|
||||||
|
this.touchLatest = {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
t: this.sim.rawTime,
|
||||||
|
dx: x - this.touchStart.x,
|
||||||
|
dy: x - this.touchStart.y,
|
||||||
|
dt: this.sim.rawTime - this.touchStart.t,
|
||||||
|
};
|
||||||
|
if (this.sim.getOption('compensate.fastPanning')) {
|
||||||
|
this.updateVelocity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerUp() {
|
||||||
|
if (this.touchStart && this.touchLatest) {
|
||||||
|
if (this.touchLatest.dt === 0) {
|
||||||
|
this.velocity = zero;
|
||||||
|
}
|
||||||
|
this.touchStart = undefined;
|
||||||
|
|
||||||
|
if (this.sim.getOption('compensate.fastPanning')) {
|
||||||
|
this.velocity = zero;
|
||||||
|
} else {
|
||||||
|
this.updateVelocity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frame(elapsedTime) {
|
||||||
|
const {touchStart: start, touchLatest: latest} = this;
|
||||||
|
const {display} = this.sim;
|
||||||
|
|
||||||
|
// Direct translate, unless using fast panning
|
||||||
|
if (start && latest && !this.sim.getOption('compensate.fastPanning')) {
|
||||||
|
// start and latest are in screen coordinates, need to convert to sim scale
|
||||||
|
const delta = div(sub(latest, start), display.scale);
|
||||||
|
display.viewOrigin = sub(start.viewOrigin, delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply update to viewOrigin based on panning
|
||||||
|
if (!this.paused) {
|
||||||
|
// elapsedTime is scaled by time scale, is that what we want?
|
||||||
|
// Yes because if panning.velocity == obj.velocity, object should stay in view
|
||||||
|
const delta = mult(this.velocity, elapsedTime);
|
||||||
|
display.viewOrigin = add(display.viewOrigin, delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update what's considered start
|
||||||
|
if (start && latest) {
|
||||||
|
this.initializeTouch(this.touchLatest);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVelocity() {
|
||||||
|
// Convert pointer velocity to simulation scale, and multiply by -1
|
||||||
|
// because the camera is panning opposite to the pointer velocity.
|
||||||
|
let velocity = div(this.sim.pointer.latestVelocity, -this.sim.display.scale);
|
||||||
|
if (this.sim.getOption('compensate.timeScale')) {
|
||||||
|
velocity = div(velocity, this.sim.timeScale);
|
||||||
|
}
|
||||||
|
// Also add current panning
|
||||||
|
velocity = add(velocity, this.velocity);
|
||||||
|
this.velocity = velocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVelocity(velocity) {
|
||||||
|
this.velocity = velocity;
|
||||||
|
if (!this.sim.playing) {
|
||||||
|
this.paused = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
pointer.js
89
pointer.js
@ -46,7 +46,6 @@ export class Pointer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('pointerleave', e => {
|
el.addEventListener('pointerleave', e => {
|
||||||
console.log('pointerleave', {x: e.clientX, y: e.clientY});
|
|
||||||
this.handlePointerUp({x: e.clientX, y: e.clientY});
|
this.handlePointerUp({x: e.clientX, y: e.clientY});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -66,7 +65,6 @@ export class Pointer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlePointerDown({x: clientX, y: clientY}) {
|
handlePointerDown({x: clientX, y: clientY}) {
|
||||||
// this.clearPointerHistory(POINTER_DOWN_HISTORY_SIZE);
|
|
||||||
this.updatePointer({x: clientX, y: clientY});
|
this.updatePointer({x: clientX, y: clientY});
|
||||||
|
|
||||||
switch (this.sim.getCurrentMode()) {
|
switch (this.sim.getCurrentMode()) {
|
||||||
@ -76,19 +74,7 @@ export class Pointer {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MODE_PAN_VIEW: {
|
case MODE_PAN_VIEW: {
|
||||||
this.touchStart = {
|
this.sim.panning.handlePointerDown({x: clientX, y: clientY});
|
||||||
x: clientX,
|
|
||||||
y: clientY,
|
|
||||||
t: this.sim.rawTime,
|
|
||||||
viewOrigin: {...this.sim.display.viewOrigin},
|
|
||||||
};
|
|
||||||
// Since we've processed this increment, reset
|
|
||||||
this.touchLatest = {
|
|
||||||
...this.touchStart,
|
|
||||||
dx: 0,
|
|
||||||
dy: 0,
|
|
||||||
dt: 0,
|
|
||||||
};
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MODE_OBJECT_SELECT: {
|
case MODE_OBJECT_SELECT: {
|
||||||
@ -110,41 +96,7 @@ export class Pointer {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MODE_PAN_VIEW: {
|
case MODE_PAN_VIEW: {
|
||||||
if (this.touchStart) {
|
this.sim.panning.handlePointerMove({x: clientX, y: clientY});
|
||||||
this.touchLatest = {
|
|
||||||
x: clientX,
|
|
||||||
y: clientY,
|
|
||||||
t: this.sim.rawTime,
|
|
||||||
dx: clientX - this.touchStart.x,
|
|
||||||
dy: clientY - this.touchStart.y,
|
|
||||||
dt: this.sim.rawTime - this.touchStart.t,
|
|
||||||
};
|
|
||||||
|
|
||||||
// With fast panning, this calculation happens every move
|
|
||||||
// With normal panning, this calculation only happens at pointer up
|
|
||||||
if (this.sim.getOption('compensate.fastPanning')) {
|
|
||||||
const panning = {...this.latestVelocity};
|
|
||||||
// Convert pointer velocity to simulation scale.
|
|
||||||
// Also multiply by -1 because the camera is
|
|
||||||
// panning opposite to the pointer velocity.
|
|
||||||
panning.x /= -this.sim.display.scale;
|
|
||||||
panning.y /= -this.sim.display.scale;
|
|
||||||
|
|
||||||
if (this.sim.getOption('compensate.timeScale')) {
|
|
||||||
panning.x /= this.sim.timeScale;
|
|
||||||
panning.y /= this.sim.timeScale;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also add current panning
|
|
||||||
panning.x += this.sim.panning?.velocity.x ?? 0;
|
|
||||||
panning.y += this.sim.panning?.velocity.y ?? 0;
|
|
||||||
|
|
||||||
this.sim.panning = {
|
|
||||||
velocity: panning
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MODE_OBJECT_SELECT: {
|
case MODE_OBJECT_SELECT: {
|
||||||
@ -162,37 +114,7 @@ export class Pointer {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MODE_PAN_VIEW: {
|
case MODE_PAN_VIEW: {
|
||||||
// Set panning velocity
|
this.sim.panning.handlePointerUp({x: clientX, y: clientY});
|
||||||
if (this.touchStart && this.touchLatest) {
|
|
||||||
if (this.touchLatest.dt === 0) {
|
|
||||||
this.sim.panning = undefined;
|
|
||||||
}
|
|
||||||
this.touchStart = undefined;
|
|
||||||
|
|
||||||
if (this.sim.getOption('compensate.fastPanning')) {
|
|
||||||
this.sim.panning = undefined;
|
|
||||||
} else {
|
|
||||||
const panning = {...this.latestVelocity};
|
|
||||||
// Convert pointer velocity to simulation scale.
|
|
||||||
// Also multiply by -1 because the camera is
|
|
||||||
// panning opposite to the pointer velocity.
|
|
||||||
panning.x /= -this.sim.display.scale;
|
|
||||||
panning.y /= -this.sim.display.scale;
|
|
||||||
|
|
||||||
if (this.sim.getOption('compensate.timeScale')) {
|
|
||||||
panning.x /= this.sim.timeScale;
|
|
||||||
panning.y /= this.sim.timeScale;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also add current panning
|
|
||||||
panning.x += this.sim.panning?.velocity.x ?? 0;
|
|
||||||
panning.y += this.sim.panning?.velocity.y ?? 0;
|
|
||||||
|
|
||||||
this.sim.panning = {
|
|
||||||
velocity: panning
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MODE_OBJECT_SELECT: {
|
case MODE_OBJECT_SELECT: {
|
||||||
@ -227,11 +149,6 @@ export class Pointer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the specified number of entries at the end of the array (most recent)
|
|
||||||
clearPointerHistory(keep = 0) {
|
|
||||||
this.pointerHistory.splice(keep, this.pointerHistory.length - keep)
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePointer({x, y}) {
|
updatePointer({x, y}) {
|
||||||
const t = this.sim.rawTime;
|
const t = this.sim.rawTime;
|
||||||
while (this.pointerHistory.length >= POINTER_HISTORY_SIZE) {
|
while (this.pointerHistory.length >= POINTER_HISTORY_SIZE) {
|
||||||
|
|||||||
@ -4,13 +4,14 @@ export const simOptions = {
|
|||||||
selection: ['Pause While Selecting', 'boolean', true],
|
selection: ['Pause While Selecting', 'boolean', true],
|
||||||
},
|
},
|
||||||
display: {
|
display: {
|
||||||
traces: ['Path Traces', 'boolean', true],
|
targetFrameRate: ['Frame Rate', 'number', 60],
|
||||||
dashedTraces: ['Dashed', 'boolean', false, {tall: true}],
|
|
||||||
velocity: ['Velocity Vectors', 'boolean', true],
|
velocity: ['Velocity Vectors', 'boolean', true],
|
||||||
acceleration: ['Accel Vectors', 'boolean', true],
|
acceleration: ['Accel Vectors', 'boolean', true],
|
||||||
|
traces: ['Path Traces', 'boolean', true],
|
||||||
|
dashedTraces: ['Dashed', 'boolean', false, {tall: true, hideUnless: 'display.traces'}],
|
||||||
velocityScale: ['Velocity<br>Vec Scale', 'number', 80, {hideUnless: 'display.velocity'}],
|
velocityScale: ['Velocity<br>Vec Scale', 'number', 80, {hideUnless: 'display.velocity'}],
|
||||||
accelerationScale: ['Accel<br>Vec Scale', 'number', 800, {hideUnless: 'display.acceleration'}],
|
accelerationScale: ['Accel<br>Vec Scale', 'number', 800, {hideUnless: 'display.acceleration'}],
|
||||||
targetFrameRate: ['Frame Rate', 'number', 60],
|
zoomVectors: ['Zoom Vectors', 'boolean', true]
|
||||||
},
|
},
|
||||||
compensate: {
|
compensate: {
|
||||||
timeScale: ['Time Scale Compensator', 'boolean', false, {wide: true}],
|
timeScale: ['Time Scale Compensator', 'boolean', false, {wide: true}],
|
||||||
|
|||||||
24
simulator.js
24
simulator.js
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
EVENT_PLAY_PAUSE,
|
||||||
EVENT_ZOOM,
|
EVENT_ZOOM,
|
||||||
FRAMERATE_SAMPLE_DURATION,
|
FRAMERATE_SAMPLE_DURATION,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
@ -8,6 +9,7 @@ import {Overlay} from './overlay.js';
|
|||||||
import {Pointer} from './pointer.js';
|
import {Pointer} from './pointer.js';
|
||||||
import {Options} from './options.js';
|
import {Options} from './options.js';
|
||||||
import {Zoom} from './zoom.js';
|
import {Zoom} from './zoom.js';
|
||||||
|
import {Panning} from './panning.js';
|
||||||
import {Select} from './select.js';
|
import {Select} from './select.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';
|
||||||
@ -20,7 +22,6 @@ export class Sim {
|
|||||||
nextZoom = undefined;
|
nextZoom = undefined;
|
||||||
playing = true;
|
playing = true;
|
||||||
recentFrames = [];
|
recentFrames = [];
|
||||||
panning = undefined;
|
|
||||||
frameRate = 0;
|
frameRate = 0;
|
||||||
|
|
||||||
system = undefined;
|
system = undefined;
|
||||||
@ -28,6 +29,7 @@ export class Sim {
|
|||||||
overlay = undefined;
|
overlay = undefined;
|
||||||
pointer = undefined;
|
pointer = undefined;
|
||||||
zoom = undefined;
|
zoom = undefined;
|
||||||
|
panning = undefined;
|
||||||
select = undefined;
|
select = undefined;
|
||||||
|
|
||||||
toolbarGroups = {};
|
toolbarGroups = {};
|
||||||
@ -52,6 +54,7 @@ export class Sim {
|
|||||||
this.overlay = new Overlay(this);
|
this.overlay = new Overlay(this);
|
||||||
this.pointer = new Pointer(this);
|
this.pointer = new Pointer(this);
|
||||||
this.zoom = new Zoom(this);
|
this.zoom = new Zoom(this);
|
||||||
|
this.panning = new Panning(this);
|
||||||
this.select = new Select(this);
|
this.select = new Select(this);
|
||||||
|
|
||||||
initializeTools(this);
|
initializeTools(this);
|
||||||
@ -82,14 +85,15 @@ export class Sim {
|
|||||||
if (this.getOption('debug.frameRate')) {
|
if (this.getOption('debug.frameRate')) {
|
||||||
this.info['Frame Rate'] = this.frameRate.toPrecision(3);
|
this.info['Frame Rate'] = this.frameRate.toPrecision(3);
|
||||||
}
|
}
|
||||||
this.zoom.frame();
|
this.zoom.frame(elapsedTime);
|
||||||
this.pointer.frame();
|
this.panning.frame(elapsedTime);
|
||||||
|
this.pointer.frame(elapsedTime);
|
||||||
this.display.frame(elapsedTime);
|
this.display.frame(elapsedTime);
|
||||||
this.select.frame();
|
this.select.frame(elapsedTime);
|
||||||
this.system.frame(elapsedTime);
|
this.system.frame(elapsedTime);
|
||||||
this.overlay.frame();
|
this.overlay.frame(elapsedTime);
|
||||||
for (const group in this.toolbarGroups) {
|
for (const group in this.toolbarGroups) {
|
||||||
this.toolbarGroups[group].frame();
|
this.toolbarGroups[group].frame(elapsedTime);
|
||||||
}
|
}
|
||||||
requestAnimationFrame(t => this.frame(t));
|
requestAnimationFrame(t => this.frame(t));
|
||||||
}
|
}
|
||||||
@ -131,9 +135,12 @@ export class Sim {
|
|||||||
play() {
|
play() {
|
||||||
this.playing = true;
|
this.playing = true;
|
||||||
|
|
||||||
if (this.panning?.paused) {
|
if (this.panning.paused) {
|
||||||
this.panning.paused = false;
|
this.panning.paused = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const e = new CustomEvent(EVENT_PLAY_PAUSE, {detail: {playing: this.playing}});
|
||||||
|
this.div.dispatchEvent(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
pause() {
|
pause() {
|
||||||
@ -142,6 +149,9 @@ export class Sim {
|
|||||||
if (this.panning?.velocity) {
|
if (this.panning?.velocity) {
|
||||||
this.panning.paused = true;
|
this.panning.paused = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const e = new CustomEvent(EVENT_PLAY_PAUSE, {detail: {playing: this.playing}});
|
||||||
|
this.div.dispatchEvent(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
getScaleDisplay() {
|
getScaleDisplay() {
|
||||||
|
|||||||
24
system.js
24
system.js
@ -1,9 +1,9 @@
|
|||||||
import {OBJECT_HISTORY_SIZE} from './config.js';
|
import {OBJECT_HISTORY_SIZE} from './config.js';
|
||||||
import {MassObject} from './object.js';
|
import {MassObject} from './object.js';
|
||||||
import {
|
import {
|
||||||
add, cross, direction, div, magnitude, mult,
|
add, copy, cross, degrees,
|
||||||
square,
|
direction, div, magnitude, mult,
|
||||||
sub, weightedAvg, zero
|
square, sub, weightedAvg, zero
|
||||||
} from './vector.js';
|
} from './vector.js';
|
||||||
|
|
||||||
export class System {
|
export class System {
|
||||||
@ -43,9 +43,7 @@ export class System {
|
|||||||
obj.velocity = div(obj.velocity, this.sim.timeScale);
|
obj.velocity = div(obj.velocity, this.sim.timeScale);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.sim.panning?.velocity) {
|
obj.velocity = add(obj.velocity, this.sim.panning.velocity);
|
||||||
obj.velocity = add(obj.velocity, this.sim.panning.velocity);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePointerMove(r) {
|
handlePointerMove(r) {
|
||||||
@ -125,8 +123,8 @@ export class System {
|
|||||||
// Combine forces
|
// Combine forces
|
||||||
// Add masses
|
// Add masses
|
||||||
// Average color
|
// Average color
|
||||||
S.position = weightedAvg(S.position, S.mass, T.position, T.mass);
|
S.position = weightedAvg([[S.position, S.mass], [T.position, T.mass]]);
|
||||||
S.velocity = weightedAvg(S.velocity, S.mass, T.velocity, T.mass);
|
S.velocity = weightedAvg([[S.velocity, S.mass], [T.velocity, T.mass]]);
|
||||||
S.forces.push(...T.forces);
|
S.forces.push(...T.forces);
|
||||||
S.mass += T.mass;
|
S.mass += T.mass;
|
||||||
S.color = {
|
S.color = {
|
||||||
@ -171,9 +169,9 @@ export class System {
|
|||||||
this.forEachObject((obj, i) => {
|
this.forEachObject((obj, i) => {
|
||||||
const speed = magnitude(obj.velocity);
|
const speed = magnitude(obj.velocity);
|
||||||
const accel = magnitude(obj.acceleration);
|
const accel = magnitude(obj.acceleration);
|
||||||
// Invert y so that the angle is counterclockwise from x-axis
|
// Give angle counterclockwise from horizontal
|
||||||
const velocityDir = direction(obj.velocity);
|
const velocityDir = -1 * degrees(direction(obj.velocity));
|
||||||
const accelDir = direction(obj.acceleration);
|
const accelDir = -1 * degrees(direction(obj.acceleration));
|
||||||
const {r, g, b} = obj.color;
|
const {r, g, b} = obj.color;
|
||||||
this.sim.info[`Object ${i}`] = [
|
this.sim.info[`Object ${i}`] = [
|
||||||
`<span style="background-color: rgb(${r},${g},${b});"> </span>`,
|
`<span style="background-color: rgb(${r},${g},${b});"> </span>`,
|
||||||
@ -215,9 +213,7 @@ export class System {
|
|||||||
this.pause();
|
this.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.sim.panning?.velocity) {
|
obj.velocity = copy(this.sim.panning.velocity);
|
||||||
obj.velocity = {...this.sim.panning.velocity};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
doneCreatingObject() {
|
doneCreatingObject() {
|
||||||
|
|||||||
@ -48,7 +48,7 @@ export class OptionsTool extends Tool {
|
|||||||
if (item.tall === true) button.classList.add(TALL_CLASSNAME);
|
if (item.tall === true) button.classList.add(TALL_CLASSNAME);
|
||||||
const value = this.sim.getOption(path);
|
const value = this.sim.getOption(path);
|
||||||
button.style.opacity = value ? '100%' : '50%';
|
button.style.opacity = value ? '100%' : '50%';
|
||||||
this.sim.onOptionSet(path, value => {
|
this.sim.onOptionSet(path, ({value}) => {
|
||||||
button.style.opacity = value ? '100%' : '50%';
|
button.style.opacity = value ? '100%' : '50%';
|
||||||
});
|
});
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
@ -81,7 +81,7 @@ export class OptionsTool extends Tool {
|
|||||||
this.sim.setOption(path, input.value);
|
this.sim.setOption(path, input.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.sim.onOptionSet(path, value => {
|
this.sim.onOptionSet(path, ({value}) => {
|
||||||
input.value = value;
|
input.value = value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
EVENT_PLAY_PAUSE,
|
||||||
TALL_CLASSNAME
|
TALL_CLASSNAME
|
||||||
} from '../config.js';
|
} from '../config.js';
|
||||||
import {Tool} from '../tool.js';
|
import {Tool} from '../tool.js';
|
||||||
@ -17,6 +18,10 @@ export class PlayPause extends Tool {
|
|||||||
setContainer(container) {
|
setContainer(container) {
|
||||||
super.setContainer(container);
|
super.setContainer(container);
|
||||||
this.updateButtons();
|
this.updateButtons();
|
||||||
|
|
||||||
|
this.sim.div.addEventListener(EVENT_PLAY_PAUSE, () => {
|
||||||
|
this.updateButtons();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(container) {
|
constructor(container) {
|
||||||
@ -38,18 +43,13 @@ export class PlayPause extends Tool {
|
|||||||
pauseButton.classList.add(TALL_CLASSNAME);
|
pauseButton.classList.add(TALL_CLASSNAME);
|
||||||
|
|
||||||
pauseButton.addEventListener('click', () => {
|
pauseButton.addEventListener('click', () => {
|
||||||
this.sim.panning = undefined;
|
this.sim.pause();
|
||||||
if (this.sim.playing) {
|
this.updateButtons();
|
||||||
this.sim.playing = false;
|
|
||||||
this.updateButtons();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
playButton.addEventListener('click', () => {
|
playButton.addEventListener('click', () => {
|
||||||
if (!this.sim.playing) {
|
this.sim.play();
|
||||||
this.sim.playing = true;
|
this.updateButtons();
|
||||||
this.updateButtons();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
tool/zoom.js
14
tool/zoom.js
@ -1,10 +1,11 @@
|
|||||||
import { Tool } from '../tool.js';
|
|
||||||
import {
|
import {
|
||||||
|
TOOL_INFO_CLASSNAME,
|
||||||
|
WIDE_CLASSNAME,
|
||||||
ZOOM_IN_FACTOR,
|
ZOOM_IN_FACTOR,
|
||||||
ZOOM_OUT_FACTOR,
|
ZOOM_OUT_FACTOR,
|
||||||
WIDE_CLASSNAME,
|
|
||||||
TOOL_INFO_CLASSNAME,
|
|
||||||
} from '../config.js';
|
} from '../config.js';
|
||||||
|
import {Tool} from '../tool.js';
|
||||||
|
import {zero} from '../vector.js';
|
||||||
|
|
||||||
export class Zoom extends Tool {
|
export class Zoom extends Tool {
|
||||||
currentScaleEl = undefined;
|
currentScaleEl = undefined;
|
||||||
@ -73,7 +74,7 @@ export class Zoom extends Tool {
|
|||||||
const factor = Math.ceil(Math.log2(1 / ratio));
|
const factor = Math.ceil(Math.log2(1 / ratio));
|
||||||
|
|
||||||
// Determine average momentum and set panning velocity to match
|
// Determine average momentum and set panning velocity to match
|
||||||
const { netMomentum, totalMass } = this.sim.system.computeSystemCenter();
|
const {netMomentum, totalMass} = this.sim.system.computeSystemCenter();
|
||||||
const netVelocity = {
|
const netVelocity = {
|
||||||
x: netMomentum.x / totalMass,
|
x: netMomentum.x / totalMass,
|
||||||
y: netMomentum.y / totalMass,
|
y: netMomentum.y / totalMass,
|
||||||
@ -83,7 +84,7 @@ export class Zoom extends Tool {
|
|||||||
|
|
||||||
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();
|
||||||
const netVelocity = {
|
const netVelocity = {
|
||||||
x: netMomentum.x / totalMass,
|
x: netMomentum.x / totalMass,
|
||||||
y: netMomentum.y / totalMass,
|
y: netMomentum.y / totalMass,
|
||||||
@ -96,8 +97,7 @@ export class Zoom extends Tool {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Cancel panning
|
// Cancel panning
|
||||||
this.sim.panning = undefined;
|
this.sim.panning.setVelocity(zero);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
39
vector.js
39
vector.js
@ -1,15 +1,21 @@
|
|||||||
|
export const copy = (v) => ({x: v.x, y: v.y});
|
||||||
export const dot = (a, b) => a.x * b.x + a.y * b.y;
|
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 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 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 sub = (a, b) => ({x: a.x - b.x, y: a.y - b.y});
|
||||||
export const square = ({x, y}) => x ** 2 + y ** 2;
|
export const square = ({x, y}) => x ** 2 + y ** 2;
|
||||||
export const magnitude = ({x, y}) => Math.sqrt(square({x, y}));
|
export const magnitude = ({x, y}) => Math.sqrt(square({x, y}));
|
||||||
|
|
||||||
|
// Arguments must be one vector and one scalar, but can be transposed
|
||||||
export const mult = (v, m) => {
|
export const mult = (v, m) => {
|
||||||
if (v.x !== undefined) return {x: v.x * m, y: v.y * m};
|
if (typeof v === 'object') return {x: v.x * m, y: v.y * m};
|
||||||
else return {x: m.x * v, y: m.y * v};
|
else if (typeof m === 'object') return {x: m.x * v, y: m.y * v};
|
||||||
|
else throw new Error('mult: no vector provided');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const div = (v, m) => ({x: v.x / m, y: v.y / m});
|
export const div = (v, m) => ({x: v.x / m, y: v.y / m});
|
||||||
export const zero = {x: 0, y: 0};
|
export const zero = {x: 0, y: 0};
|
||||||
|
|
||||||
export const weightedAvg = (items) => {
|
export const weightedAvg = (items) => {
|
||||||
let res = zero;
|
let res = zero;
|
||||||
let W = 0;
|
let W = 0;
|
||||||
@ -20,27 +26,10 @@ export const weightedAvg = (items) => {
|
|||||||
return div(res, W);
|
return div(res, W);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Angle is given counterclockwise, assuming screen coordinates
|
export const components = (mag, dir) => mult(mag, {
|
||||||
export const direction = ({x, y}) => Math.atan2(-y, x) * 180 / Math.PI;
|
x: Math.cos(dir),
|
||||||
|
y: Math.sin(dir),
|
||||||
|
});
|
||||||
|
|
||||||
export class Vector {
|
export const direction = ({x, y}) => Math.atan2(y, x);
|
||||||
x = undefined;
|
export const degrees = (rad) => rad * 180 / Math.PI;
|
||||||
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};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
16
zoom.js
16
zoom.js
@ -3,6 +3,7 @@ import {
|
|||||||
SCALE_POWER_MAX,
|
SCALE_POWER_MAX,
|
||||||
SCALE_POWER_MIN,
|
SCALE_POWER_MIN,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
|
import {div, sub} from './vector.js';
|
||||||
|
|
||||||
export class Zoom {
|
export class Zoom {
|
||||||
sim = undefined;
|
sim = undefined;
|
||||||
@ -27,7 +28,7 @@ export class Zoom {
|
|||||||
// x, y should be in Sim coordinates
|
// x, y should be in Sim coordinates
|
||||||
// velocity should be in Sim coordinate scale
|
// velocity should be in Sim coordinate scale
|
||||||
zoom({x, y, factor, velocity}) {
|
zoom({x, y, factor, velocity}) {
|
||||||
const { display } = this.sim;
|
const {display} = this.sim;
|
||||||
// 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
|
||||||
@ -35,22 +36,15 @@ export class Zoom {
|
|||||||
let scalePower = display.scalePower + factor;
|
let scalePower = display.scalePower + factor;
|
||||||
scalePower = Math.max(scalePower, SCALE_POWER_MIN);
|
scalePower = Math.max(scalePower, SCALE_POWER_MIN);
|
||||||
scalePower = Math.min(scalePower, SCALE_POWER_MAX);
|
scalePower = Math.min(scalePower, SCALE_POWER_MAX);
|
||||||
this.sim.display.scalePower = scalePower;
|
display.scalePower = scalePower;
|
||||||
|
|
||||||
// compute coordinates of new view frame
|
// compute coordinates of new view frame
|
||||||
display.viewOrigin.x = x - display.width / 2;
|
display.viewOrigin = sub({x, y}, div({x: display.width, y: display.height}, 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
|
// TODO: If paused, set panning velocity on resume
|
||||||
// if (this.sim.playing && velocity) {
|
// if (this.sim.playing && velocity) {
|
||||||
if (velocity) {
|
if (velocity) {
|
||||||
this.sim.panning = {
|
this.sim.panning.setVelocity(velocity);
|
||||||
velocity: { ...velocity },
|
|
||||||
paused: !this.sim.playing,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const e = new CustomEvent(EVENT_ZOOM);
|
const e = new CustomEvent(EVENT_ZOOM);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user