/** * Clone tool — Alt-click (desktop) or double-tap (mobile) sets the * sample source; a regular click+drag stamps from that source onto the * active layer. The source point moves WITH the brush so the offset * stays constant across the stroke. * * begin() handles the source-pick and stroke-start branches; the * actual per-sample stamping continues through the shared stroke * pipeline (`_strokeTo`) which knows about clone-mode internally. * * @param {{ * activeLayer: () => object | null, * saveState: (label?: string) => void, * strokeTo: (x: number, y: number) => void, * showToast: (msg: string) => void, * }} deps */ import { state } from '../state.js'; import { canvasCoords } from '../canvas-coords.js'; export function createCloneTool({ activeLayer, saveState, strokeTo, showToast }) { return { begin(e) { const layer = activeLayer(); const coords = canvasCoords(e, state.mainCanvas); // Mobile equivalent of Alt-click: double-tap in screen pixels. // Wider tolerances (500 ms, 40 px) than desktop because finger // taps drift more than mouse clicks. const isTouchEvt = e.type && e.type.startsWith('touch'); let isDoubleTap = false; if (isTouchEvt) { const t = e.touches ? e.touches[0] : null; const cx = t ? t.clientX : 0; const cy = t ? t.clientY : 0; const now = Date.now(); const dt = now - state.cloneLastTapTime; const dx = cx - state.cloneLastTapX; const dy = cy - state.cloneLastTapY; if (dt < 500 && Math.hypot(dx, dy) < 40) { isDoubleTap = true; state.cloneLastTapTime = 0; // consume the pair } else { state.cloneLastTapTime = now; state.cloneLastTapX = cx; state.cloneLastTapY = cy; } } if (e.altKey || isDoubleTap) { state.cloneSourceX = coords.x; state.cloneSourceY = coords.y; state.cloneSourceLayerId = (layer && layer.id) || state.activeLayerId; state.cloneSourceSnapshot = null; // captured at first stroke showToast('Clone source set'); return; } if (state.cloneSourceX === null || state.cloneSourceY === null) { showToast(isTouchEvt ? 'Double-tap first to set a clone source' : 'Alt-click first to set a clone source'); return; } if (!layer || layer.locked) return; saveState('Clone stroke'); // Snapshot the source layer's pixels at stroke-start so the // brush samples clean source pixels even after it has painted // over them. Otherwise we'd cascade-clone the same ring. const srcLayer = state.layers.find(l => l.id === state.cloneSourceLayerId) || layer; const snap = document.createElement('canvas'); snap.width = srcLayer.canvas.width; snap.height = srcLayer.canvas.height; snap.getContext('2d').drawImage(srcLayer.canvas, 0, 0); state.cloneSourceSnapshot = snap; state.cloneStrokeStartX = coords.x; state.cloneStrokeStartY = coords.y; state.drawing = true; state.lastX = coords.x; state.lastY = coords.y; strokeTo(coords.x, coords.y); }, }; }