UNPKG

@teachinglab/omd

Version:

omd

445 lines (396 loc) 15.4 kB
import { pointerEventHandler } from './pointerEventHandler.js'; /** * Event manager for canvas interactions * Handles pointer, keyboard, and wheel events */ export class EventManager { /** * @param {OMDCanvas} canvas - Canvas instance */ constructor(canvas) { this.canvas = canvas; this.isInitialized = false; // Event handlers this.pointerEventHandler = new pointerEventHandler(canvas); // Bound event listeners this._onPointerDown = this._onPointerDown.bind(this); this._onPointerMove = this._onPointerMove.bind(this); this._onPointerUp = this._onPointerUp.bind(this); this._onPointerCancel = this._onPointerCancel.bind(this); this._onPointerEnter = this._onPointerEnter.bind(this); this._onPointerLeave = this._onPointerLeave.bind(this); this._onKeyDown = this._onKeyDown.bind(this); this._onKeyUp = this._onKeyUp.bind(this); this._onWheel = this._onWheel.bind(this); this._onContextMenu = this._onContextMenu.bind(this); // State tracking this.activePointers = new Map(); this.isDrawing = false; this.lastEventTime = 0; } /** * Initialize event listeners */ initialize() { if (this.isInitialized) return; const svg = this.canvas.svg; // Pointer events (unified mouse/touch/pen handling) svg.addEventListener('pointerdown', this._onPointerDown); svg.addEventListener('pointermove', this._onPointerMove); svg.addEventListener('pointerup', this._onPointerUp); svg.addEventListener('pointercancel', this._onPointerCancel); svg.addEventListener('pointerenter', this._onPointerEnter); svg.addEventListener('pointerleave', this._onPointerLeave); // Keyboard events (on document for global shortcuts) if (this.canvas.config.enableKeyboardShortcuts) { document.addEventListener('keydown', this._onKeyDown); document.addEventListener('keyup', this._onKeyUp); } // Mouse wheel events svg.addEventListener('wheel', this._onWheel); // Prevent context menu svg.addEventListener('contextmenu', this._onContextMenu); // Set touch-action for better touch handling svg.style.touchAction = 'none'; this.isInitialized = true; } /** * Handle pointer down events * @private */ _onPointerDown(event) { event.preventDefault(); // Track active pointer this.activePointers.set(event.pointerId, event); // Set pointer capture so canvas continues to receive events event.target.setPointerCapture(event.pointerId); // Convert to canvas coordinates const canvasCoords = this.canvas.clientToSVG(event.clientX, event.clientY); // Create normalized event const normalizedEvent = this._normalizePointerEvent(event, canvasCoords); // Handle multi-touch if (this.canvas.config.enableMultiTouch && this.activePointers.size > 1) { this.pointerEventHandler.handleMultiTouchStart(this.activePointers); return; } // Delegate to pointer event handler this.pointerEventHandler.handlePointerDown(event, normalizedEvent); // Delegate to active tool const activeTool = this.canvas.toolManager.getActiveTool(); if (activeTool) { // Let the tool decide if it's drawing (e.g., PointerTool won't set isDrawing) activeTool.onPointerDown(normalizedEvent); // If tool hasn't explicitly set isDrawing, set it for backwards compatibility // (tools like PencilTool and EraserTool expect isDrawing to be true) if (this.isDrawing === false && activeTool.constructor.name !== 'PointerTool') { this.isDrawing = true; } } // Emit canvas event this.canvas.emit('pointerDown', normalizedEvent); } /** * Handle pointer move events * @private */ _onPointerMove(event) { // Throttle events for performance const now = Date.now(); if (now - this.lastEventTime < 16) return; // ~60fps this.lastEventTime = now; // Update active pointer if (this.activePointers.has(event.pointerId)) { this.activePointers.set(event.pointerId, event); } // Convert to canvas coordinates const canvasCoords = this.canvas.clientToSVG(event.clientX, event.clientY); // Create normalized event const normalizedEvent = this._normalizePointerEvent(event, canvasCoords); // Handle multi-touch if (this.canvas.config.enableMultiTouch && this.activePointers.size > 1) { this.pointerEventHandler.handleMultiTouchMove(this.activePointers); return; } // Update cursor position if (this.canvas.cursor) { this.canvas.cursor.setPosition(canvasCoords.x, canvasCoords.y); // Update pressure feedback if available if (event.pressure !== undefined) { this.canvas.cursor.setPressureFeedback(event.pressure); } } // Delegate to pointer event handler this.pointerEventHandler.handlePointerMove(event, normalizedEvent); // Delegate to active tool if drawing if (this.isDrawing) { const activeTool = this.canvas.toolManager.getActiveTool(); if (activeTool) { activeTool.onPointerMove(normalizedEvent); } } // Emit canvas event this.canvas.emit('pointerMove', normalizedEvent); } /** * Handle pointer up events * @private */ _onPointerUp(event) { // Remove from active pointers this.activePointers.delete(event.pointerId); // Release pointer capture event.target.releasePointerCapture(event.pointerId); // Convert to canvas coordinates const canvasCoords = this.canvas.clientToSVG(event.clientX, event.clientY); // Create normalized event const normalizedEvent = this._normalizePointerEvent(event, canvasCoords); // Handle multi-touch if (this.canvas.config.enableMultiTouch && this.activePointers.size >= 1) { this.pointerEventHandler.handleMultiTouchEnd(this.activePointers); return; } // Clear drawing state this.isDrawing = false; // Delegate to pointer event handler this.pointerEventHandler.handlePointerUp(event, normalizedEvent); // Delegate to active tool const activeTool = this.canvas.toolManager.getActiveTool(); if (activeTool) { activeTool.onPointerUp(normalizedEvent); } // Emit canvas event this.canvas.emit('pointerUp', normalizedEvent); } /** * Handle pointer cancel events * @private */ _onPointerCancel(event) { // Remove from active pointers this.activePointers.delete(event.pointerId); // Release pointer capture event.target.releasePointerCapture(event.pointerId); // Clear drawing state this.isDrawing = false; // Cancel active tool const activeTool = this.canvas.toolManager.getActiveTool(); if (activeTool) { activeTool.onCancel(); } // Emit canvas event this.canvas.emit('pointerCancel', { pointerId: event.pointerId }); } /** * Handle pointer enter events * @private */ _onPointerEnter(event) { // Show custom cursor when entering canvas if (this.canvas.cursor) { this.canvas.cursor.show(); // Update cursor from current tool config const activeTool = this.canvas.toolManager.getActiveTool(); if (activeTool && activeTool.config) { this.canvas.cursor.updateFromToolConfig(activeTool.config); } } // If mouse is down (event.buttons !== 0), start drawing if (event.buttons !== 0) { // Convert to canvas coordinates const canvasCoords = this.canvas.clientToSVG(event.clientX, event.clientY); const normalizedEvent = this._normalizePointerEvent(event, canvasCoords); this.isDrawing = true; const activeTool = this.canvas.toolManager.getActiveTool(); if (activeTool) { activeTool.onPointerDown(normalizedEvent); } } // Emit canvas event this.canvas.emit('pointerEnter', { event }); } /** * Handle pointer leave events * @private */ _onPointerLeave(event) { // Hide custom cursor when leaving canvas if (this.canvas.cursor) { this.canvas.cursor.hide(); } // Do NOT cancel drawing on pointerleave; drawing continues outside canvas // But if no buttons are pressed, we can safely clear the drawing state if (event.buttons === 0) { this.isDrawing = false; } // Clear active pointers only if no buttons pressed if (event.buttons === 0) { this.activePointers.clear(); } // Emit canvas event this.canvas.emit('pointerLeave', { event }); } /** * Handle keyboard down events * @private */ _onKeyDown(event) { // Ignore if typing in input fields if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') { return; } const key = event.key.toLowerCase(); // Global shortcuts switch (key) { case 'p': if (this.canvas.config.enabledTools.includes('pencil')) { this.canvas.toolManager.setActiveTool('pencil'); event.preventDefault(); } break; case 'e': if (this.canvas.config.enabledTools.includes('eraser')) { this.canvas.toolManager.setActiveTool('eraser'); event.preventDefault(); } break; case 's': if (this.canvas.config.enabledTools.includes('select')) { this.canvas.toolManager.setActiveTool('select'); event.preventDefault(); } break; case 'escape': // Cancel current operation const activeTool = this.canvas.toolManager.getActiveTool(); if (activeTool) { activeTool.onCancel(); } event.preventDefault(); break; } // Delegate to active tool const activeTool = this.canvas.toolManager.getActiveTool(); if (activeTool && activeTool.onKeyboardShortcut) { const handled = activeTool.onKeyboardShortcut(key, event); if (handled) { event.preventDefault(); } } // Emit canvas event this.canvas.emit('keyDown', { key, event }); } /** * Handle keyboard up events * @private */ _onKeyUp(event) { // Ignore if typing in input fields if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') { return; } const key = event.key.toLowerCase(); // Emit canvas event this.canvas.emit('keyUp', { key, event }); } /** * Handle wheel events (zoom/pan) * @private */ _onWheel(event) { // Prevent default scrolling event.preventDefault(); // Convert to canvas coordinates const canvasCoords = this.canvas.clientToSVG(event.clientX, event.clientY); // Emit canvas event this.canvas.emit('wheel', { deltaX: event.deltaX, deltaY: event.deltaY, deltaZ: event.deltaZ, x: canvasCoords.x, y: canvasCoords.y, ctrlKey: event.ctrlKey, metaKey: event.metaKey }); } /** * Prevent context menu * @private */ _onContextMenu(event) { event.preventDefault(); } /** * Normalize pointer event data * @private */ _normalizePointerEvent(event, canvasCoords) { return { pointerId: event.pointerId, pointerType: event.pointerType, isPrimary: event.isPrimary, x: canvasCoords.x, y: canvasCoords.y, clientX: event.clientX, clientY: event.clientY, pressure: this._normalizePressure(event.pressure), tiltX: event.tiltX || 0, tiltY: event.tiltY || 0, twist: event.twist || 0, width: event.width || 1, height: event.height || 1, tangentialPressure: event.tangentialPressure || 0, buttons: event.buttons, shiftKey: event.shiftKey, ctrlKey: event.ctrlKey, altKey: event.altKey, metaKey: event.metaKey, timestamp: event.timeStamp || Date.now() }; } /** * Normalize pressure values * @private */ _normalizePressure(pressure) { if (pressure === undefined || pressure === null) { return 0.5; // Default pressure for devices without pressure sensitivity } // Clamp pressure between 0 and 1 return Math.max(0, Math.min(1, pressure)); } /** * Get current pointer information * @returns {Object} Pointer information */ getPointerInfo() { return { activePointers: this.activePointers.size, isDrawing: this.isDrawing, multiTouch: this.activePointers.size > 1 }; } /** * Destroy event manager */ destroy() { if (!this.isInitialized) return; const svg = this.canvas.svg; // Remove pointer events svg.removeEventListener('pointerdown', this._onPointerDown); svg.removeEventListener('pointermove', this._onPointerMove); svg.removeEventListener('pointerup', this._onPointerUp); svg.removeEventListener('pointercancel', this._onPointerCancel); svg.removeEventListener('pointerenter', this._onPointerEnter); svg.removeEventListener('pointerleave', this._onPointerLeave); // Remove keyboard events document.removeEventListener('keydown', this._onKeyDown); document.removeEventListener('keyup', this._onKeyUp); // Remove other events svg.removeEventListener('wheel', this._onWheel); svg.removeEventListener('contextmenu', this._onContextMenu); // Clear state this.activePointers.clear(); this.isDrawing = false; this.isInitialized = false; } }