import { DISPLAY_CURSOR_INFO, MODE_MASS_GENERATION, MODE_OBJECT_SELECT, MODE_PAN_VIEW, POINTER_HISTORY_SIZE, TOOL_CLASSNAME, ZOOM_IN_FACTOR, ZOOM_OUT_FACTOR, } from './config.js'; export class Pointer { sim = undefined; pointerHistory = []; panning = undefined; // { velocity: {x and y in sim coordinates}, paused: boolean } 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}, `, `${e.clientY}`]; } this.handlePointerMove({x: e.clientX, y: e.clientY}); }); el.addEventListener('pointerdown', e => { let target = e.target; while (target && !target.classList?.contains(TOOL_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 {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}); } handlePointerDown({x: clientX, y: clientY}) { this.clearPointerHistory(5); 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.panning = undefined; } else { const v = this.getPointerVelocity(); // Convert pointer velocity to simulation scale v.x /= this.sim.display.scale; v.y /= this.sim.display.scale; this.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.pointerHistory[this.pointerHistory.length - 1]; // Convert pointer velocity to simulation scale v.x /= this.sim.display.scale; v.y /= this.sim.display.scale; // const a = this.getPointerAcceleration(); // v.x = v.x + a.x * v.dt / this.sim.display.scale / 2; // v.y = v.y + a.y * v.dt / this.sim.display.scale / 2; 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, }; } } } 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); } if (this.panTouchStart && this.panTouchLatest) { // Direct translate const start = this.panTouchStart; const latest = this.panTouchLatest; this.sim.display.viewOrigin.x = start.viewOrigin.x - (latest.x - start.x) / this.sim.display.scale; this.sim.display.viewOrigin.y = start.viewOrigin.y - (latest.y - start.y) / this.sim.display.scale; } else if (this.panning && !this.panning.paused) { // Apply update to viewOrigin based on panning const { velocity } = this.panning; this.sim.display.viewOrigin.x -= velocity.x * elapsedTime; this.sim.display.viewOrigin.y -= velocity.y * elapsedTime; } } }