PageSourceSearch

https://odysseus-lxcq.onrender.com/static/js/editor/canvas-events.js

js odysseus-lxcq.onrender.com collected 2026-10-03 11:45:17 UTC 8,193 bytes, 197 lines download raw bytes

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.