175 lines
7.4 KiB
JavaScript
175 lines
7.4 KiB
JavaScript
/**
|
|
* Transform-drag tool — handle drag interactions for the Transform
|
|
* tool (resize via corner/edge handles, rotation via the rot grip).
|
|
*
|
|
* The transform UI runs in TWO modes: the floating popup (W/H/rot
|
|
* numeric inputs, lives elsewhere) AND direct drag on the canvas
|
|
* handles. Both ultimately mutate `state.transformPendingW/H/Rot` and
|
|
* call `reapplyTransform()` to redraw. This module owns the drag
|
|
* branch.
|
|
*
|
|
* The dispatcher in galleryEditor.js calls `tryBegin/tryContinue/
|
|
* tryEnd` which return `true` when the event was for the transform
|
|
* tool and was handled (so the dispatcher can short-circuit).
|
|
*
|
|
* @param {{
|
|
* beginMove: (e: Event) => void,
|
|
* composite: () => void,
|
|
* drawTransformHandles: () => void,
|
|
* reapplyTransform: () => void,
|
|
* getTransformHandle: (x: number, y: number) => string | null,
|
|
* cursorForHandle: (id: string | null) => string,
|
|
* }} deps
|
|
*/
|
|
import { state } from '../state.js';
|
|
import { canvasCoords } from '../canvas-coords.js';
|
|
|
|
export function createTransformDragTool({
|
|
beginMove, composite, drawTransformHandles, reapplyTransform,
|
|
getTransformHandle, cursorForHandle,
|
|
}) {
|
|
return {
|
|
/**
|
|
* Called on pointerdown. Returns true if the transform tool handled
|
|
* the event (the dispatcher should NOT fall through to other tools).
|
|
*/
|
|
tryBegin(e) {
|
|
if (!state.transformActive) return false;
|
|
const coords = canvasCoords(e, state.mainCanvas);
|
|
state.transformHandle = getTransformHandle(coords.x, coords.y);
|
|
if (state.transformHandle) {
|
|
state.transformStartX = coords.x;
|
|
state.transformStartY = coords.y;
|
|
// Snapshot offset + size at drag-start so each frame computes
|
|
// "start + dx" (correct delta) rather than accumulating off the
|
|
// running offset, which was making top/left grabs drift.
|
|
const layer = state.transformLayer;
|
|
const off = state.layerOffsets.get(layer.id) || { x: 0, y: 0 };
|
|
state.transformStartOffX = off.x;
|
|
state.transformStartOffY = off.y;
|
|
state.transformOrigW = layer.canvas.width;
|
|
state.transformOrigH = layer.canvas.height;
|
|
return true;
|
|
}
|
|
// No corner hit — if click inside the layer's bounding box, act
|
|
// like Move so the user can drag the layer around without
|
|
// switching tools.
|
|
if (state.transformLayer) {
|
|
const off = state.layerOffsets.get(state.transformLayer.id) || { x: 0, y: 0 };
|
|
const w = state.transformLayer.canvas.width;
|
|
const h = state.transformLayer.canvas.height;
|
|
if (coords.x >= off.x && coords.x <= off.x + w &&
|
|
coords.y >= off.y && coords.y <= off.y + h) {
|
|
beginMove(e);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Called on pointermove. Returns true if handled.
|
|
*
|
|
* When transformActive but no handle is grabbed, updates the
|
|
* hover cursor + pulse. When a handle is grabbed, drives the
|
|
* resize / rotation pipeline.
|
|
*/
|
|
tryContinue(e) {
|
|
if (!state.transformActive) return false;
|
|
// No drag in progress — just hover-cursor + pulse.
|
|
if (!state.transformHandle && state.mainCanvas) {
|
|
const coords = canvasCoords(e, state.mainCanvas);
|
|
const hovered = getTransformHandle(coords.x, coords.y);
|
|
state.mainCanvas.style.cursor = hovered ? cursorForHandle(hovered) : 'default';
|
|
if (hovered !== state.hoveredHandle) {
|
|
state.hoveredHandle = hovered;
|
|
composite();
|
|
}
|
|
return false; // didn't fully consume the event
|
|
}
|
|
if (!state.transformHandle) return false;
|
|
e.preventDefault();
|
|
const coords = canvasCoords(e, state.mainCanvas);
|
|
// Rotation grip — angle measured from the layer's geometric
|
|
// centre to the cursor. Mirror into the popup if it's open.
|
|
if (state.transformHandle === 'rot') {
|
|
const layer = state.transformLayer;
|
|
const off = state.layerOffsets.get(layer.id) || { x: 0, y: 0 };
|
|
const cx = off.x + layer.canvas.width / 2;
|
|
const cy = off.y + layer.canvas.height / 2;
|
|
const rad = Math.atan2(coords.y - cy, coords.x - cx) + Math.PI / 2;
|
|
let deg = Math.round((rad * 180) / Math.PI);
|
|
if (e.shiftKey) deg = Math.round(deg / 15) * 15; // 15° snap
|
|
while (deg > 180) deg -= 360;
|
|
while (deg <= -180) deg += 360;
|
|
state.transformPendingRot = deg;
|
|
reapplyTransform();
|
|
if (state.transformPopup) {
|
|
const rotIn = state.transformPopup.querySelector('#ge-transform-rot');
|
|
if (rotIn) rotIn.value = String(deg);
|
|
}
|
|
return true;
|
|
}
|
|
// Resize via corner / edge handle.
|
|
const dx = coords.x - state.transformStartX;
|
|
const dy = coords.y - state.transformStartY;
|
|
const layer = state.transformLayer;
|
|
let newW = layer.canvas.width;
|
|
let newH = layer.canvas.height;
|
|
if (state.transformHandle.includes('r')) newW = state.transformOrigW + dx;
|
|
if (state.transformHandle.includes('l')) newW = state.transformOrigW - dx;
|
|
if (state.transformHandle.includes('b')) newH = state.transformOrigH + dy;
|
|
if (state.transformHandle.includes('t')) newH = state.transformOrigH - dy;
|
|
// Shift = lock aspect ratio. Use whichever axis moved more
|
|
// (relative to the original) as the driver.
|
|
if (e.shiftKey && state.transformOrigW > 0 && state.transformOrigH > 0) {
|
|
const aspect = state.transformOrigW / state.transformOrigH;
|
|
const wDelta = Math.abs(newW - state.transformOrigW);
|
|
const hDelta = Math.abs(newH - state.transformOrigH);
|
|
if (wDelta >= hDelta) {
|
|
newH = Math.max(1, Math.round(newW / aspect));
|
|
} else {
|
|
newW = Math.max(1, Math.round(newH * aspect));
|
|
}
|
|
}
|
|
newW = Math.max(1, Math.round(newW));
|
|
newH = Math.max(1, Math.round(newH));
|
|
// Route through the popup-driven pipeline so popup + drag stay
|
|
// in sync. Anchor the opposite corner via transformOrigOffset so
|
|
// handles don't slide while the user drags.
|
|
state.transformPendingW = newW;
|
|
state.transformPendingH = newH;
|
|
const anchorOffX = state.transformStartOffX +
|
|
(state.transformHandle.includes('l') ? (state.transformOrigW - newW) : 0);
|
|
const anchorOffY = state.transformStartOffY +
|
|
(state.transformHandle.includes('t') ? (state.transformOrigH - newH) : 0);
|
|
state.transformOrigOffset = {
|
|
x: anchorOffX + newW / 2 - state.transformOrigW / 2,
|
|
y: anchorOffY + newH / 2 - state.transformOrigH / 2,
|
|
};
|
|
reapplyTransform();
|
|
// Mirror the new W/H into the popup if it's open.
|
|
if (state.transformPopup) {
|
|
const wIn = state.transformPopup.querySelector('#ge-transform-w');
|
|
const hIn = state.transformPopup.querySelector('#ge-transform-h');
|
|
if (wIn) wIn.value = String(state.transformPendingFlipH ? -newW : newW);
|
|
if (hIn) hIn.value = String(state.transformPendingFlipV ? -newH : newH);
|
|
}
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Called on pointerup. Returns true if handled.
|
|
*/
|
|
tryEnd() {
|
|
if (!(state.transformActive && state.transformHandle)) return false;
|
|
state.transformHandle = null;
|
|
state.transformOrigW = state.transformLayer?.canvas.width || 0;
|
|
state.transformOrigH = state.transformLayer?.canvas.height || 0;
|
|
composite();
|
|
drawTransformHandles();
|
|
return true;
|
|
},
|
|
};
|
|
}
|