253 lines
7.4 KiB
JavaScript
253 lines
7.4 KiB
JavaScript
import {
|
|
MODE_MASS_GENERATION,
|
|
MODE_OBJECT_SELECT,
|
|
MODE_PAN_VIEW,
|
|
POINTER_HISTORY_SIZE,
|
|
TOOLBAR_CLASSNAME,
|
|
ZOOM_IN_FACTOR,
|
|
ZOOM_OUT_FACTOR,
|
|
} from './config.js';
|
|
|
|
export class Pointer {
|
|
sim = undefined;
|
|
|
|
pointerHistory = [];
|
|
touchStart = undefined; // {x: undefined, y: undefined, t: undefined};
|
|
touchLatest = undefined; // {x: undefined, y: undefined, t: undefined};
|
|
suppressClick = false;
|
|
|
|
constructor(sim) {
|
|
this.sim = sim;
|
|
|
|
// Monitor mouse movements
|
|
const el = window;
|
|
|
|
el.addEventListener('pointermove', e => {
|
|
if (this.sim.getOption('debug.cursorInfo')) {
|
|
this.sim.info['pointermove'] = [`${e.clientX.toPrecision(6)}, `, `${e.clientY.toPrecision(6)}`];
|
|
}
|
|
this.handlePointerMove({x: e.clientX, y: e.clientY});
|
|
});
|
|
|
|
el.addEventListener('pointerdown', e => {
|
|
let target = e.target;
|
|
while (target && !target.classList?.contains(TOOLBAR_CLASSNAME)) {
|
|
target = target.parentNode;
|
|
}
|
|
if (target) {
|
|
return;
|
|
}
|
|
|
|
this.handlePointerDown({x: e.clientX, y: e.clientY});
|
|
});
|
|
|
|
el.addEventListener('pointerup', e => {
|
|
this.handlePointerUp({x: e.clientX, y: e.clientY});
|
|
});
|
|
|
|
el.addEventListener('pointerleave', e => {
|
|
console.log('pointerleave', {x: e.clientX, y: e.clientY});
|
|
this.handlePointerUp({x: e.clientX, y: e.clientY});
|
|
});
|
|
|
|
// Monitor wheel events
|
|
el.addEventListener('wheel', e => {
|
|
const factor = e.deltaY < 0 ? ZOOM_IN_FACTOR : ZOOM_OUT_FACTOR;
|
|
const {x, y} = this.sim.screenToSim(e.clientX, e.clientY);
|
|
this.sim.scheduleZoom({x, y}, factor);
|
|
});
|
|
|
|
el.addEventListener('focus', () => {
|
|
console.log('window focus');
|
|
});
|
|
el.addEventListener('blur', () => {
|
|
console.log('window blur');
|
|
});
|
|
}
|
|
|
|
handlePointerDown({x: clientX, y: clientY}) {
|
|
// this.clearPointerHistory(POINTER_DOWN_HISTORY_SIZE);
|
|
this.updatePointer({x: clientX, y: clientY});
|
|
|
|
switch (this.sim.getCurrentMode()) {
|
|
case MODE_MASS_GENERATION: {
|
|
const {x, y} = this.sim.screenToSim(clientX, clientY)
|
|
this.sim.system.handlePointerDown({x, y});
|
|
break;
|
|
}
|
|
case MODE_PAN_VIEW: {
|
|
this.touchStart = {
|
|
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;
|
|
}
|
|
case MODE_OBJECT_SELECT: {
|
|
this.sim.select.handlePointerDown({x: clientX, y: clientY});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle cursor (mouse or touch) movement
|
|
handlePointerMove({x: clientX, y: clientY}) {
|
|
// TODO: If e.touches.length > 1, user may be engaging pinch to zoom
|
|
this.updatePointer({x: clientX, y: clientY});
|
|
|
|
switch (this.sim.getCurrentMode()) {
|
|
case MODE_MASS_GENERATION: {
|
|
const {x, y} = this.sim.screenToSim(clientX, clientY);
|
|
this.sim.system.handlePointerMove({x, y});
|
|
break;
|
|
}
|
|
case MODE_PAN_VIEW: {
|
|
if (this.touchStart) {
|
|
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;
|
|
}
|
|
case MODE_OBJECT_SELECT: {
|
|
this.sim.select.handlePointerMove({x: clientX, y: clientY});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
handlePointerUp({x: clientX, y: clientY}) {
|
|
switch (this.sim.getCurrentMode()) {
|
|
case MODE_MASS_GENERATION: {
|
|
const {x, y} = this.sim.screenToSim(clientX, clientY);
|
|
this.sim.system.handlePointerUp({x, y});
|
|
break;
|
|
}
|
|
case MODE_PAN_VIEW: {
|
|
// Set panning velocity
|
|
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;
|
|
}
|
|
case MODE_OBJECT_SELECT: {
|
|
this.sim.select.handlePointerUp({x: clientX, y: clientY});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
frame() {
|
|
// Add another entry for the current pointer position
|
|
const {pointerHistory} = this;
|
|
if (pointerHistory.length) {
|
|
const currentPointer = pointerHistory[pointerHistory.length - 1];
|
|
this.updatePointer(currentPointer);
|
|
}
|
|
}
|
|
|
|
getPointerVelocity(points = POINTER_HISTORY_SIZE) {
|
|
// Average over pointer history
|
|
if (this.pointerHistory.length < 2) {
|
|
return this.latestVelocity;
|
|
}
|
|
points = Math.min(points, POINTER_HISTORY_SIZE, this.pointerHistory.length);
|
|
const start = this.pointerHistory[this.pointerHistory.length - points];
|
|
const end = this.pointerHistory[this.pointerHistory.length - 1];
|
|
const dt = (end.t - start.t);
|
|
return {
|
|
x: (end.x - start.x) / dt,
|
|
y: (end.y - start.y) / dt,
|
|
dt
|
|
};
|
|
}
|
|
|
|
// 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}) {
|
|
const t = this.sim.rawTime;
|
|
while (this.pointerHistory.length >= POINTER_HISTORY_SIZE) {
|
|
this.pointerHistory.shift();
|
|
}
|
|
const v = this.getPointerVelocity();
|
|
this.pointerHistory.push({t, x, y, v});
|
|
}
|
|
|
|
get latestVelocity() {
|
|
const latestPointer = this.pointerHistory[this.pointerHistory.length - 1];
|
|
return {
|
|
x: 0,
|
|
y: 0,
|
|
...latestPointer?.v
|
|
}
|
|
}
|
|
}
|