82 lines
3.1 KiB
JavaScript
82 lines
3.1 KiB
JavaScript
/**
|
|
* 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);
|
|
},
|
|
};
|
|
}
|