1/** 2 * Clone tool â Alt-click (desktop) or double-tap (mobile) sets the 3 * sample source; a regular click+drag stamps from that source onto the 4 * active layer. The source point moves WITH the brush so the offset 5 * stays constant across the stroke. 6 * 7 * begin() handles the source-pick and stroke-start branches; the 8 * actual per-sample stamping continues through the shared stroke 9 * pipeline (`_strokeTo`) which knows about clone-mode internally. 10 * 11 * @param {{ 12 * activeLayer: () => object | null, 13 * saveState: (label?: string) => void, 14 * strokeTo: (x: number, y: number) => void, 15 * showToast: (msg: string) => void, 16 * }} deps 17 */ 18import { state } from '../state.js'; 19import { canvasCoords } from '../canvas-coords.js'; 20 21export function createCloneTool({ activeLayer, saveState, strokeTo, showToast }) { 22 return { 23 begin(e) { 24 const layer = activeLayer(); 25 const coords = canvasCoords(e, state.mainCanvas); 26 // Mobile equivalent of Alt-click: double-tap in screen pixels. 27 // Wider tolerances (500 ms, 40 px) than desktop because finger 28 // taps drift more than mouse clicks. 29 const isTouchEvt = e.type && e.type.startsWith('touch'); 30 let isDoubleTap = false; 31 if (isTouchEvt) { 32 const t = e.touches ? e.touches[0] : null; 33 const cx = t ? t.clientX : 0; 34 const cy = t ? t.clientY : 0; 35 const now = Date.now(); 36 const dt = now - state.cloneLastTapTime; 37 const dx = cx - state.cloneLastTapX; 38 const dy = cy - state.cloneLastTapY; 39 if (dt < 500 && Math.hypot(dx, dy) < 40) { 40 isDoubleTap = true; 41 state.cloneLastTapTime = 0; // consume the pair 42 } else { 43 state.cloneLastTapTime = now; 44 state.cloneLastTapX = cx; 45 state.cloneLastTapY = cy; 46 } 47 } 48 if (e.altKey || isDoubleTap) { 49 state.cloneSourceX = coords.x; 50 state.cloneSourceY = coords.y; 51 state.cloneSourceLayerId = (layer && layer.id) || state.activeLayerId; 52 state.cloneSourceSnapshot = null; // captured at first stroke 53 showToast('Clone source set'); 54 return; 55 } 56 if (state.cloneSourceX === null || state.cloneSourceY === null) { 57 showToast(isTouchEvt 58 ? 'Double-tap first to set a clone source' 59 : 'Alt-click first to set a clone source'); 60 return; 61 } 62 if (!layer || layer.locked) return; 63 saveState('Clone stroke'); 64 // Snapshot the source layer's pixels at stroke-start so the 65 // brush samples clean source pixels even after it has painted 66 // over them. Otherwise we'd cascade-clone the same ring. 67 const srcLayer = state.layers.find(l => l.id === state.cloneSourceLayerId) || layer; 68 const snap = document.createElement('canvas'); 69 snap.width = srcLayer.canvas.width; 70 snap.height = srcLayer.canvas.height; 71 snap.getContext('2d').drawImage(srcLayer.canvas, 0, 0); 72 state.cloneSourceSnapshot = snap; 73 state.cloneStrokeStartX = coords.x; 74 state.cloneStrokeStartY = coords.y; 75 state.drawing = true; 76 state.lastX = coords.x; 77 state.lastY = coords.y; 78 strokeTo(coords.x, coords.y); 79 }, 80 }; 81}
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.