gravity/pointer.js
2025-12-29 16:06:08 -06:00

172 lines
5.0 KiB
JavaScript

import {
DISPLAY_CURSOR_INFO,
MODE_MASS_GENERATION,
MODE_OBJECT_SELECT,
MODE_PAN_VIEW,
POINTER_HISTORY_SIZE,
POINTER_DOWN_HISTORY_SIZE,
TOOLBAR_CLASSNAME,
ZOOM_IN_FACTOR,
ZOOM_OUT_FACTOR,
} from './config.js';
export class Pointer {
sim = undefined;
pointerHistory = [];
panTouchStart = undefined; // {x: undefined, y: undefined, t: undefined};
panTouchLatest = 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 (DISPLAY_CURSOR_INFO) {
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 => {
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);
});
}
getPointerVelocity(points = POINTER_HISTORY_SIZE) {
// Average over pointer history
if (this.pointerHistory.length < 2) {
return this.latestPointerVelocity ?? {x: 0, y: 0, dt: 1};
}
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) / 1000;
return {
x: (end.x - start.x) / dt,
y: (end.y - start.y) / dt,
dt
};
}
clearPointerHistory(keep = 0) {
this.pointerHistory.splice(0, this.pointerHistory.length - keep)
}
updatePointer({x, y}) {
const t = document.timeline.currentTime;
while (this.pointerHistory.length >= POINTER_HISTORY_SIZE) {
this.pointerHistory.shift();
}
const v = this.getPointerVelocity();
this.pointerHistory.push({t, x, y, v});
}
get latestPointerVelocity() {
const latestPointer = this.pointerHistory[this.pointerHistory.length - 1];
return latestPointer?.v;
}
handlePointerDown({x: clientX, y: clientY}) {
this.clearPointerHistory(POINTER_DOWN_HISTORY_SIZE);
this.updatePointer({x: clientX, y: clientY});
if (this.sim.isCurrentMode(MODE_MASS_GENERATION)) {
const {x, y} = this.sim.screenToSim(clientX, clientY)
this.sim.objects.handlePointerDown({x, y});
} else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) {
this.panTouchStart = {
x: clientX,
y: clientY,
t: document.timeline.currentTime,
viewOrigin: {...this.sim.display.viewOrigin},
};
this.panTouchLatest = {...this.panTouchStart};
} else if (this.sim.isCurrentMode(MODE_OBJECT_SELECT)) {
// TODO: Start a selection box
}
}
handlePointerUp({x: clientX, y: clientY}) {
if (this.sim.isCurrentMode(MODE_MASS_GENERATION)) {
const {x, y} = this.sim.screenToSim(clientX, clientY);
this.sim.objects.handlePointerUp({x, y});
} else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) {
// Set panning velocity
if (this.panTouchStart && this.panTouchLatest) {
const dt = (this.panTouchLatest.t - this.panTouchStart.t) / 1000;
if (!dt) {
this.sim.panning = undefined;
} else {
const v = {...this.latestPointerVelocity};
// Convert pointer velocity to simulation scale
v.x /= this.sim.display.scale;
v.y /= this.sim.display.scale;
this.sim.panning = {
velocity: v
};
}
this.panTouchStart = undefined;
}
}
}
// Handle cursor (mouse or touch) movement
// TODO: If e.touches.length > 1, user may be engaging pinch to zoom
handlePointerMove({x: clientX, y: clientY}) {
this.updatePointer({x: clientX, y: clientY});
const v = this.latestPointerVelocity;
// Convert pointer velocity to simulation scale
v.x /= this.sim.display.scale;
v.y /= this.sim.display.scale;
if (this.sim.isCurrentMode(MODE_MASS_GENERATION)) {
const {x, y} = this.sim.screenToSim(clientX, clientY);
this.sim.objects.handlePointerMove({x, y, vx: v.x, vy: v.y});
} else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) {
if (this.panTouchStart) {
// Event loop should be able to read
this.panTouchLatest = {
x: clientX,
y: clientY,
t: document.timeline.currentTime,
};
}
}
}
}