import { DISPLAY_CURSOR_INFO, DRAGGABLE_ELEMENT_CLASSNAME, MODE_MASS_GENERATION, MODE_PAN_VIEW, POINTER_HISTORY_SIZE, ZOOM_IN_FACTOR, ZOOM_OUT_FACTOR, } from './config.js'; 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); }); } 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; // Bonus scale factor for pointer power return { x: (end.x - start.x) / dt * this.sim.display.scale, y: (end.y - start.y) / dt * this.sim.display.scale, dt }; } getPointerAcceleration() { // 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.v.x - start.v.x) / dt, y: (end.v.y - start.v.y) / dt, dt }; } clearPointerHistory() { this.pointerHistory = []; } updatePointer({x, y}) { const t = document.timeline.currentTime; while (this.pointerHistory.length >= POINTER_HISTORY_SIZE) { this.pointerHistory.shift(); } const v = this.getPointerVelocity(); const a = this.getPointerAcceleration(); this.pointerHistory.push({t, x, y, v, a}); } handlePointerDown({x: clientX, y: clientY}) { this.updatePointer({x: clientX, y: clientY}); if (this.sim.isCurrentMode(MODE_MASS_GENERATION)) { // this.panning = undefined; const {x, y} = this.sim.screenToSim(clientX, clientY) this.sim.objects.handlePointerDown({x, y}); } else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) { this.panning = this.panning || {}; this.panning.gathering = true; this.panning.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}); const velocity = this.getPointerVelocity(); const acceleration = this.getPointerAcceleration(); // Convet to sim coordinates // Let's try incorporating pointer acceleration this.panning.velocity.x = velocity.x + acceleration.x * velocity.dt; this.panning.velocity.y = velocity.y + acceleration.y * velocity.dt; } } } 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 {velocity} = this.panning; // Convert pointer velocity to sim internal scale this.sim.display.viewOrigin.x -= velocity.x * elapsedTime; this.sim.display.viewOrigin.y -= velocity.y * elapsedTime; } } }