1/** 2 * Crop tool â drag-rect selection that lets the user cut down the 3 * canvas to a smaller region. Supports Shift-lock aspect ratio and 4 * click-inside-rect to reposition an existing crop without redrawing. 5 * 6 * Owns its own begin/drag/end handlers and reads/writes shared state. 7 * The factory takes a small dependency bag for things still living in 8 * galleryEditor.js â `composite` redraws the canvas, `showCropApply` 9 * mounts the floating WÃH + Apply panel after the user finishes 10 * dragging. 11 * 12 * @param {{ 13 * composite: () => void, 14 * showCropApply: () => void, 15 * }} deps 16 */ 17import { state } from '../state.js'; 18import { canvasCoords } from '../canvas-coords.js'; 19import { drawCheckerboard } from '../checkerboard.js'; 20 21export function createCropTool({ composite, showCropApply }) { 22 return { 23 begin(e) { 24 const coords = canvasCoords(e, state.mainCanvas); 25 // Click inside an existing crop rect â switch to move-mode so 26 // the user can reposition without redrawing. 27 if (state.cropRect && 28 coords.x >= state.cropRect.x && coords.x <= state.cropRect.x + state.cropRect.w && 29 coords.y >= state.cropRect.y && coords.y <= state.cropRect.y + state.cropRect.h) { 30 state.cropMoving = true; 31 state.cropMoveStart = { x: coords.x, y: coords.y, rx: state.cropRect.x, ry: state.cropRect.y }; 32 return; 33 } 34 state.cropping = true; 35 state.cropStart = coords; 36 state.cropEnd = { ...state.cropStart }; 37 state.cropRect = null; 38 state.cropAspectLock = null; 39 // Tear down the size panel while the user is drawing a new rect. 40 const old = state.container?.querySelector('.ge-crop-apply'); 41 if (old) old.remove(); 42 }, 43 44 drag(e) { 45 // Move-mode: drag the existing rect around the canvas. 46 if (state.cropMoving && state.cropRect && state.cropMoveStart) { 47 e.preventDefault(); 48 const c = canvasCoords(e, state.mainCanvas); 49 const dx = c.x - state.cropMoveStart.x; 50 const dy = c.y - state.cropMoveStart.y; 51 let nx = state.cropMoveStart.rx + dx; 52 let ny = state.cropMoveStart.ry + dy; 53 // Clamp to canvas bounds so the rect stays fully visible. 54 nx = Math.max(0, Math.min(nx, state.mainCanvas.width - state.cropRect.w)); 55 ny = Math.max(0, Math.min(ny, state.mainCanvas.height - state.cropRect.h)); 56 state.cropRect = { ...state.cropRect, x: nx, y: ny }; 57 composite(); 58 return; 59 } 60 if (!state.cropping) return; 61 e.preventDefault(); 62 state.cropEnd = canvasCoords(e, state.mainCanvas); 63 // Shift-held = lock aspect ratio. First Shift press during the 64 // drag snapshots the current aspect; subsequent moves stay locked. 65 // Releasing Shift resets so the user can re-lock at a new ratio. 66 if (e.shiftKey) { 67 const rawDx = state.cropEnd.x - state.cropStart.x; 68 const rawDy = state.cropEnd.y - state.cropStart.y; 69 if (state.cropAspectLock == null) { 70 const rawW = Math.abs(rawDx) || 1; 71 const rawH = Math.abs(rawDy) || 1; 72 state.cropAspectLock = rawW / rawH; 73 } 74 const absDx = Math.abs(rawDx); 75 const absDy = Math.abs(rawDy); 76 // Whichever axis the user moved more (relative to the lock) is 77 // the driver; scale the other to preserve aspect. 78 let dx, dy; 79 if (absDx >= absDy * state.cropAspectLock) { 80 dx = rawDx; 81 dy = Math.sign(rawDy || 1) * (absDx / state.cropAspectLock); 82 } else { 83 dy = rawDy; 84 dx = Math.sign(rawDx || 1) * (absDy * state.cropAspectLock); 85 } 86 state.cropEnd = { x: state.cropStart.x + dx, y: state.cropStart.y + dy }; 87 } else { 88 state.cropAspectLock = null; 89 } 90 composite(); 91 // Draw crop overlay. 92 const x = Math.min(state.cropStart.x, state.cropEnd.x); 93 const y = Math.min(state.cropStart.y, state.cropEnd.y); 94 const w = Math.abs(state.cropEnd.x - state.cropStart.x); 95 const h = Math.abs(state.cropEnd.y - state.cropStart.y); 96 state.mainCtx.fillStyle = 'rgba(0,0,0,0.4)'; 97 state.mainCtx.fillRect(0, 0, state.mainCanvas.width, state.mainCanvas.height); 98 state.mainCtx.clearRect(x, y, w, h); 99 // Redraw layers inside the crop rect (dim everything outside).
100 state.mainCtx.save(); 101 state.mainCtx.beginPath(); 102 state.mainCtx.rect(x, y, w, h); 103 state.mainCtx.clip(); 104 drawCheckerboard(state.mainCtx, state.mainCanvas.width, state.mainCanvas.height); 105 for (const layer of state.layers) { 106 if (!layer.visible) continue; 107 state.mainCtx.globalAlpha = layer.opacity; 108 const off = state.layerOffsets.get(layer.id) || { x: 0, y: 0 }; 109 state.mainCtx.drawImage(layer.canvas, off.x, off.y); 110 } 111 state.mainCtx.globalAlpha = 1; 112 state.mainCtx.restore(); 113 // Dashed border around the kept region. 114 state.mainCtx.strokeStyle = '#fff'; 115 state.mainCtx.lineWidth = 1; 116 state.mainCtx.setLineDash([4, 4]); 117 state.mainCtx.strokeRect(x, y, w, h); 118 state.mainCtx.setLineDash([]); 119 state.cropRect = { x, y, w, h }; 120 }, 121 122 end() { 123 // Move-mode wrap-up: refresh the floating panel so Apply follows 124 // the rect to its new spot. 125 if (state.cropMoving) { 126 state.cropMoving = false; 127 state.cropMoveStart = null; 128 if (state.cropRect) showCropApply(); 129 return; 130 } 131 state.cropping = false; 132 if (state.cropRect && state.cropRect.w > 5 && state.cropRect.h > 5) { 133 showCropApply(); 134 } 135 }, 136 }; 137}
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.