1/** 2 * Canvas event wiring â mouse, touch (including pinch-zoom on two 3 * fingers), and the canvas-area pan handler. 4 * 5 * Mouse: 6 * mousedown on canvas â beginDraw 7 * mousemove on window â continueDraw (window so a drag can 8 * continue past the canvas edge) 9 * mouseup on window â endDraw 10 * mouseenter/mouseleave â show/hide the brush-cursor overlay 11 * mousedown on canvas-area (NOT on the canvas itself, lasso only) 12 * â beginDraw (lasso starts outside canvas) 13 * 14 * Touch: 15 * touchstart 1 finger â beginDraw 16 * touchmove 1 finger â continueDraw 17 * touchend / touchcancel â endDraw 18 * touchstart 2 fingers â pinch-zoom + 2-finger pan 19 * 20 * Pan (any free space around the canvas): 21 * pointerdown / pointermove / pointerup on canvas-area, skipping 22 * the canvas + transform overlay + UI elements above them. Sets 23 * canvasArea.dataset.panX/Y + CSS transform on both canvases. 24 * 25 * Exposes `canvasArea._resetPan()` so the zoom/fit reset can clear 26 * the pan offset. 27 * 28 * @param {{ 29 * canvasArea: HTMLDivElement, 30 * beginDraw: (e: Event) => void, 31 * continueDraw: (e: Event) => void, 32 * endDraw: (e?: Event) => void, 33 * updateBrushCursor: (e: Event) => void, 34 * syncZoomControls?: () => void, 35 * }} ctx 36 */ 37import { state } from './state.js'; 38 39export function wireCanvasEvents({ canvasArea, beginDraw, continueDraw, endDraw, updateBrushCursor, syncZoomControls }) { 40 // Mouse â mousedown stays on the canvas; mousemove/up are bound to 41 // the WINDOW so a drag can continue (and end) past the canvas edge. 42 // Critical for the Resize tool where users overshoot. 43 state.mainCanvas.addEventListener('mousedown', beginDraw); 44 window.addEventListener('mousemove', continueDraw); 45 window.addEventListener('mouseup', endDraw); 46 // Lasso can start OUTSIDE the canvas â fallback mousedown on the 47 // surrounding canvas-area so the user can begin a lasso path in 48 // the empty space around the image. Other tools stay canvas-only. 49 canvasArea.addEventListener('mousedown', (e) => { 50 if (state.tool !== 'lasso') return; 51 if (e.target === state.mainCanvas) return; // already handled 52 beginDraw(e); 53 }); 54 state.mainCanvas.addEventListener('mouseenter', (e) => { 55 if (['brush', 'eraser', 'inpaint', 'lasso', 'clone'].includes(state.tool)) updateBrushCursor(e); 56 }); 57 state.mainCanvas.addEventListener('mouseleave', () => { 58 // Only hide the brush-cursor overlay on leave â DO NOT end the 59 // drag, so the user can drag a resize handle past the canvas edge. 60 if (state.cursorEl) state.cursorEl.style.display = 'none'; 61 }); 62 63 // Touch â single finger draws; two fingers pan + pinch-zoom. 64 let multiActive = false; 65 let multiStartDist = 0; 66 let multiStartZoom = 1; 67 let multiStartCenter = { x: 0, y: 0 }; 68 let multiStartPan = { x: 0, y: 0 }; 69 const touchInfo = (e) => { 70 const t1 = e.touches[0], t2 = e.touches[1]; 71 const cx = (t1.clientX + t2.clientX) / 2; 72 const cy = (t1.clientY + t2.clientY) / 2; 73 const dx = t2.clientX - t1.clientX; 74 const dy = t2.clientY - t1.clientY; 75 return { cx, cy, dist: Math.hypot(dx, dy) }; 76 }; 77 const applyCanvasOffset = (x, y) => { 78 canvasArea.dataset.panX = String(x); 79 canvasArea.dataset.panY = String(y); 80 const t = `translate3d(${x}px, ${y}px, 0)`; 81 state.mainCanvas.style.transform = t; 82 if (state.transformOverlay) state.transformOverlay.style.transform = t; 83 }; 84 state.mainCanvas.addEventListener('touchstart', (e) => { 85 e.preventDefault(); 86 if (e.touches.length >= 2) { 87 // End any in-progress single-finger draw before switching modes. 88 if (!multiActive) endDraw(); 89 multiActive = true; 90 const info = touchInfo(e); 91 multiStartDist = info.dist; 92 multiStartZoom = state.zoom; 93 multiStartCenter = { x: info.cx, y: info.cy }; 94 multiStartPan = { 95 x: parseFloat(canvasArea.dataset.panX || '0') || 0, 96 y: parseFloat(canvasArea.dataset.panY || '0') || 0, 97 }; 98 return; 99 } 100 if (multiActive) return; 101 beginDraw(e); 102 }, { passive: false }); 103 state.mainCanvas.addEventListener('touchmove', (e) => { 104 e.preventDefault(); 105 if (multiActive && e.touches.length >= 2) { 106 const info = touchInfo(e); 107 const ratio = info.dist / Math.max(1, multiStartDist);
108 const newZoom = Math.max(0.1, Math.min(5, multiStartZoom * ratio)); 109 if (Math.abs(newZoom - state.zoom) > 0.001) { 110 state.zoom = newZoom; 111 state.mainCanvas.style.width = (state.imgWidth * state.zoom) + 'px'; 112 state.mainCanvas.style.height = (state.imgHeight * state.zoom) + 'px'; 113 const label = state.container.querySelector('.ge-zoom-label'); 114 if (label) label.textContent = Math.round(state.zoom * 100) + '%'; 115 syncZoomControls?.(); 116 } 117 const dx = info.cx - multiStartCenter.x; 118 const dy = info.cy - multiStartCenter.y; 119 applyCanvasOffset(multiStartPan.x + dx, multiStartPan.y + dy); 120 return; 121 } 122 if (multiActive) return; 123 continueDraw(e); 124 }, { passive: false }); 125 state.mainCanvas.addEventListener('touchend', (e) => { 126 if (multiActive) { 127 if (e.touches.length < 2) multiActive = false; 128 return; 129 } 130 endDraw(e); 131 }); 132 state.mainCanvas.addEventListener('touchcancel', () => { 133 multiActive = false; 134 endDraw(); 135 }); 136 137 // Press-and-drag in the empty space AROUND the canvas pans the 138 // canvas + overlay via CSS transform. Works even when the image 139 // fits the viewport (no scroll needed). Skips presses on the canvas 140 // itself (the canvas owns its own drawing input) or on UI elements 141 // above it. 142 let panning = false; 143 let pid = null; 144 let startX = 0, startY = 0; 145 const getOffset = () => { 146 const v = canvasArea.dataset.panX || '0'; 147 const u = canvasArea.dataset.panY || '0'; 148 return { x: parseFloat(v) || 0, y: parseFloat(u) || 0 }; 149 }; 150 const applyOffset = (x, y) => { 151 canvasArea.dataset.panX = String(x); 152 canvasArea.dataset.panY = String(y); 153 const t = `translate3d(${x}px, ${y}px, 0)`; 154 state.mainCanvas.style.transform = t; 155 if (state.transformOverlay) state.transformOverlay.style.transform = t; 156 }; 157 canvasArea.addEventListener('pointerdown', (e) => { 158 if (state.tool === 'lasso') return; 159 if (e.target === state.mainCanvas || e.target === state.transformOverlay) return; 160 if (e.target.closest('button, input, .ge-adj-popup, .ge-transform-popup, .ge-fx-popup, .ge-inpaint-popup, .ge-controls, .ge-right-panel, .ge-fx-menu')) return; 161 // During an active transform the corner/rotation handles render 162 // OUTSIDE the canvas (over the surrounding area), and the overlay is 163 // pointer-events:none â so a grab on an outside handle lands here. 164 // Route it to the transform tool (getHandleAt works in image space, 165 // even for points beyond the canvas) instead of panning the canvas. 166 if (state.transformActive) { 167 beginDraw(e); 168 // Only swallow the event (skip pan) if a handle was grabbed OR the 169 // layer-move fallback engaged; otherwise let the pan logic below 170 // run so empty space still pans while the transform tool is open. 171 if (state.transformHandle || state.moving) return; 172 } 173 const off = getOffset(); 174 panning = true; 175 pid = e.pointerId; 176 startX = e.clientX - off.x; 177 startY = e.clientY - off.y; 178 try { canvasArea.setPointerCapture(pid); } catch {} 179 canvasArea.style.cursor = 'grabbing'; 180 e.preventDefault(); 181 }); 182 canvasArea.addEventListener('pointermove', (e) => { 183 if (!panning || e.pointerId !== pid) return; 184 applyOffset(e.clientX - startX, e.clientY - startY); 185 }); 186 const endPan = () => { 187 if (!panning) return; 188 panning = false; 189 try { canvasArea.releasePointerCapture(pid); } catch {} 190 pid = null; 191 canvasArea.style.cursor = ''; 192 }; 193 canvasArea.addEventListener('pointerup', endPan); 194 canvasArea.addEventListener('pointercancel', endPan); 195 // Reset offset whenever zoom/fit changes the canvas size. 196 canvasArea._resetPan = () => applyOffset(0, 0); 197}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.