gravity/pointer.js
2025-12-27 12:45:42 -06:00

200 lines
6.1 KiB
JavaScript

import {
POINTER_HISTORY_SIZE,
ZOOM_IN_FACTOR,
ZOOM_OUT_FACTOR,
DISPLAY_CURSOR_INFO,
DRAGGABLE_ELEMENT_CLASSNAME,
MODE_MASS_GENERATION,
MODE_PAN_VIEW,
PAN_VELOCITY_SCALE_FACTOR,
} from './config.js';
function dispatchEvent(target, eventType, data) {
const ev = new CustomEvent(eventType, {detail: data});
target.dispatchEvent(ev);
}
export class Pointer {
sim = undefined;
pointerHistory = [];
draggingElement = undefined;
panning = 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}, `, `${e.clientY}`];
}
if (this.draggingElement) {
this.draggingElement.dragging.pointerEnd = {
x: e.clientX,
y: e.clientY,
};
} else {
// const {x, y} = this.sim.screenToSim(e.clientX, e.clientY);
this.handlePointerMove({x: e.clientX, y: e.clientY});
}
});
el.addEventListener('pointerdown', e => {
// If this is a child of a draggable element, handle dragging
let target = e.target;
while (target && !target.classList.contains(DRAGGABLE_ELEMENT_CLASSNAME)) {
target = target.parentElement;
}
if (target?.classList.contains(DRAGGABLE_ELEMENT_CLASSNAME)) {
this.draggingElement = target;
this.draggingElement.dragging = {
elementStart: {
x: parseInt(this.draggingElement.style.left),
y: parseInt(this.draggingElement.style.top),
},
pointerStart: {
x: e.clientX,
y: e.clientY,
},
pointerEnd: {
x: e.clientX,
y: e.clientY,
},
};
} else {
this.handlePointerDown({x: e.clientX, y: e.clientY});
}
});
el.addEventListener('pointerup', e => {
this.clearPointerHistory();
if (this.draggingElement) {
this.draggingElement.dragging = undefined;
this.draggingElement = undefined;
this.lastPosition = {x: undefined, y: undefined};
} else {
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);
});
// When leaving panning mode, clear panning
this.sim.onModeLeave(MODE_PAN_VIEW, () => {
this.panning = undefined;
});
}
getPointerVelocity() {
// Average over pointer history
if (this.pointerHistory.length < 2) {
return {x: 0, y: 0, dt: 1};
}
const start = this.pointerHistory[0];
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() {
this.pointerHistory = [];
}
updatePointer({x, y}) {
const t = document.timeline.currentTime;
this.pointerHistory.push({x, y, t});
if (this.pointerHistory.length > POINTER_HISTORY_SIZE) {
this.pointerHistory.shift();
}
}
handlePointerDown({x: clientX, y: clientY}) {
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.panning = {
gathering: true,
viewOriginStart: this.sim.display.viewOrigin,
pointerStart: {x: clientX, y: clientY},
pointerCurrent: {x: clientX, y: clientY},
velocity: {x: 0, y: 0},
};
}
}
handlePointerUp({x: clientX, y: clientY}) {
this.clearPointerHistory();
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)) {
if (this.panning?.gathering) {
this.panning.gathering = false;
}
}
}
// Handle cursor (mouse or touch) movement
// TODO: If e.touches.length > 1, user may be engaging pinch to zoom
handlePointerMove({x: clientX, y: clientY}) {
if (this.sim.isCurrentMode(MODE_MASS_GENERATION)) {
this.updatePointer({ x: clientX, y: clientY });
const {x, y} = this.sim.screenToSim(clientX, clientY);
const velocity = this.getPointerVelocity();
// Convert pointer velocity to sim internal scale
const vx = velocity.x / this.sim.display.scale;
const vy = velocity.y / this.sim.display.scale;
this.sim.objects.handlePointerMove({x, y, vx, vy});
} else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) {
if (this.panning?.gathering) {
this.updatePointer({ x: clientX, y: clientY });
this.panning.pointerCurrent = {x: clientX, y: clientY};
this.panning.velocity = this.getPointerVelocity();
}
}
}
computeFrame(elapsedTime) {
// Add another entry for the current pointer position
if (this.pointerHistory?.length) {
const currentPointer = this.pointerHistory[this.pointerHistory.length - 1];
this.updatePointer(currentPointer);
}
// Apply update to viewOrigin based on panning
if (this.panning) {
const {pointerStart, pointerCurrent, viewOriginStart, velocity} = this.sim.pointer.panning;
// Convert pointer velocity to sim internal scale
const vx = velocity.x / this.sim.display.scale;
const vy = velocity.y / this.sim.display.scale;
this.panning.viewOriginStart = this.sim.display.viewOrigin;
this.panning.pointerStart = pointerCurrent;
// 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;
}
}
}