mirror of
https://github.com/YuzuZensai/Pien-Studio.git
synced 2026-09-02 14:18:35 +00:00
549 lines
20 KiB
TypeScript
549 lines
20 KiB
TypeScript
import React from "react";
|
|
import type { Layer } from "@pien-studio/types";
|
|
import { getToolDefinition } from "@pien-studio/editor-core";
|
|
|
|
type Viewport = {
|
|
x: number;
|
|
y: number;
|
|
scale: number;
|
|
};
|
|
|
|
type UseCanvasInteractionsOptions = {
|
|
canvasWidth: number;
|
|
canvasHeight: number;
|
|
tool: string;
|
|
onSelectLayer: (id: string | null) => void;
|
|
onMoveLayer: (id: string, x: number, y: number) => void;
|
|
onMoveLayerEnd?: (id: string, x: number, y: number) => void;
|
|
onResizeLayer?: (id: string, width: number, height: number) => void;
|
|
onResizeLayerEnd?: (id: string, width: number, height: number) => void;
|
|
onRotateLayer?: (id: string, rotation: number) => void;
|
|
onRotateLayerEnd?: (id: string, rotation: number) => void;
|
|
onInteractionStart?: () => void;
|
|
onInteractionEnd?: () => void;
|
|
onContextMenu?: (x: number, y: number) => void;
|
|
onFillLayer?: (layerId: string, x: number, y: number) => void;
|
|
onBrushStrokeStart?: (layerId: string, x: number, y: number, layerWidth: number, layerHeight: number) => void;
|
|
onBrushStrokeMove?: (layerId: string, x: number, y: number) => void;
|
|
onBrushStrokeEnd?: (layerId: string) => void;
|
|
};
|
|
|
|
const MIN_SCALE = 0.1;
|
|
const MAX_SCALE = 10;
|
|
const ZOOM_FACTOR = 0.001;
|
|
const FIT_PADDING = 32;
|
|
|
|
export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
|
|
const {
|
|
canvasWidth,
|
|
canvasHeight,
|
|
tool,
|
|
onSelectLayer,
|
|
onMoveLayer,
|
|
onMoveLayerEnd,
|
|
onResizeLayer,
|
|
onResizeLayerEnd,
|
|
onRotateLayer,
|
|
onRotateLayerEnd,
|
|
onInteractionStart,
|
|
onInteractionEnd,
|
|
onContextMenu,
|
|
onFillLayer,
|
|
onBrushStrokeStart,
|
|
onBrushStrokeMove,
|
|
onBrushStrokeEnd,
|
|
} = options;
|
|
const [viewport, setViewport] = React.useState<Viewport>({ x: 0, y: 0, scale: 1 });
|
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
const isPanning = React.useRef(false);
|
|
const lastPos = React.useRef({ x: 0, y: 0 });
|
|
const isMiddleMousePan = React.useRef(false);
|
|
const [isSpacePan, setIsSpacePan] = React.useState(false);
|
|
const [, setIsShiftPressed] = React.useState(false);
|
|
const didFitRef = React.useRef(false);
|
|
const interactionActiveRef = React.useRef(false);
|
|
const dragMoveRafRef = React.useRef<number | null>(null);
|
|
const resizeMoveRafRef = React.useRef<number | null>(null);
|
|
const resizeMovePendingRef = React.useRef<{ width: number; height: number; x: number; y: number } | null>(null);
|
|
const dragRef = React.useRef<{
|
|
id: string;
|
|
startLayerX: number;
|
|
startLayerY: number;
|
|
startEventX: number;
|
|
startEventY: number;
|
|
lastX: number;
|
|
lastY: number;
|
|
} | null>(null);
|
|
const resizeRef = React.useRef<{
|
|
id: string;
|
|
startWidth: number;
|
|
startHeight: number;
|
|
startX: number;
|
|
startY: number;
|
|
aspect: number;
|
|
lastWidth: number;
|
|
lastHeight: number;
|
|
corner: string;
|
|
startLayerX: number;
|
|
startLayerY: number;
|
|
} | null>(null);
|
|
const rotateRef = React.useRef<{
|
|
id: string;
|
|
centerX: number;
|
|
centerY: number;
|
|
startAngle: number;
|
|
startRotation: number;
|
|
lastRotation: number;
|
|
} | null>(null);
|
|
const brushRef = React.useRef<{ id: string; lastX: number; lastY: number; rect: DOMRect } | null>(null);
|
|
|
|
const pinchRef = React.useRef<{
|
|
active: boolean;
|
|
initialPinchPx: number;
|
|
initialScale: number;
|
|
initialX: number;
|
|
initialY: number;
|
|
pivotX: number;
|
|
pivotY: number;
|
|
} | null>(null);
|
|
|
|
function beginInteraction() {
|
|
if (interactionActiveRef.current) return;
|
|
interactionActiveRef.current = true;
|
|
onInteractionStart?.();
|
|
}
|
|
|
|
function endInteraction() {
|
|
if (!interactionActiveRef.current) return;
|
|
interactionActiveRef.current = false;
|
|
onInteractionEnd?.();
|
|
}
|
|
|
|
function clientDist(t0: Pick<Touch, "clientX" | "clientY">, t1: Pick<Touch, "clientX" | "clientY">) {
|
|
const dx = t0.clientX - t1.clientX;
|
|
const dy = t0.clientY - t1.clientY;
|
|
return Math.sqrt(dx * dx + dy * dy);
|
|
}
|
|
|
|
function wheelZoom(e: WheelEvent) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const rect = containerRef.current?.getBoundingClientRect();
|
|
if (!rect) return;
|
|
const factor = e.ctrlKey || e.metaKey ? 1 - e.deltaY * 0.01 : 1 - e.deltaY * ZOOM_FACTOR;
|
|
if (!Number.isFinite(factor) || factor === 0) return;
|
|
const pivotX = e.clientX - rect.left;
|
|
const pivotY = e.clientY - rect.top;
|
|
setViewport((vp) => {
|
|
const nextScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, vp.scale * factor));
|
|
const scaleChange = nextScale / vp.scale;
|
|
return {
|
|
x: pivotX - (pivotX - vp.x) * scaleChange,
|
|
y: pivotY - (pivotY - vp.y) * scaleChange,
|
|
scale: nextScale,
|
|
};
|
|
});
|
|
}
|
|
|
|
function zoomBy(factor: number, pivotX: number, pivotY: number) {
|
|
setViewport((vp) => {
|
|
const newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, vp.scale * factor));
|
|
const scaleChange = newScale / vp.scale;
|
|
return {
|
|
x: pivotX - (pivotX - vp.x) * scaleChange,
|
|
y: pivotY - (pivotY - vp.y) * scaleChange,
|
|
scale: newScale,
|
|
};
|
|
});
|
|
}
|
|
|
|
React.useEffect(() => {
|
|
const el = containerRef.current;
|
|
if (!el) return;
|
|
const handler = (event: WheelEvent) => {
|
|
wheelZoom(event);
|
|
};
|
|
el.addEventListener("wheel", handler, { passive: false });
|
|
return () => el.removeEventListener("wheel", handler);
|
|
}, []);
|
|
|
|
React.useLayoutEffect(() => {
|
|
didFitRef.current = false;
|
|
}, [canvasWidth, canvasHeight]);
|
|
|
|
React.useLayoutEffect(() => {
|
|
if (!containerRef.current) return;
|
|
const observer = new ResizeObserver(() => {
|
|
if (!containerRef.current) return;
|
|
const rect = containerRef.current.getBoundingClientRect();
|
|
if (rect.width === 0 || rect.height === 0) return;
|
|
if (didFitRef.current) return;
|
|
const maxW = Math.max(1, rect.width - FIT_PADDING * 2);
|
|
const maxH = Math.max(1, rect.height - FIT_PADDING * 2);
|
|
const fitScale = Math.min(maxW / canvasWidth, maxH / canvasHeight, 1);
|
|
const nextX = (rect.width - canvasWidth * fitScale) / 2;
|
|
const nextY = (rect.height - canvasHeight * fitScale) / 2;
|
|
setViewport({ x: nextX, y: nextY, scale: fitScale });
|
|
didFitRef.current = true;
|
|
});
|
|
observer.observe(containerRef.current);
|
|
return () => observer.disconnect();
|
|
}, [canvasWidth, canvasHeight]);
|
|
|
|
React.useEffect(() => {
|
|
function handleKeyDown(e: KeyboardEvent) {
|
|
if (e.code === "Space") {
|
|
if (!(e.target instanceof HTMLElement) || /^(input|textarea|select)$/i.test(e.target.tagName)) return;
|
|
if (!isSpacePan) setIsSpacePan(true);
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
if (e.key === "Shift") {
|
|
if (!(e.target instanceof HTMLElement) || /^(input|textarea|select)$/i.test(e.target.tagName)) return;
|
|
setIsShiftPressed(true);
|
|
return;
|
|
}
|
|
if (!e.ctrlKey && !e.metaKey) return;
|
|
if (e.key === "-" || e.key === "_") {
|
|
e.preventDefault();
|
|
const rect = containerRef.current?.getBoundingClientRect();
|
|
const cx = rect ? rect.left + rect.width / 2 : window.innerWidth / 2;
|
|
const cy = rect ? rect.top + rect.height / 2 : window.innerHeight / 2;
|
|
zoomBy(0.8, cx, cy);
|
|
} else if (e.key === "+" || e.key === "=") {
|
|
e.preventDefault();
|
|
const rect = containerRef.current?.getBoundingClientRect();
|
|
const cx = rect ? rect.left + rect.width / 2 : window.innerWidth / 2;
|
|
const cy = rect ? rect.top + rect.height / 2 : window.innerHeight / 2;
|
|
zoomBy(1.25, cx, cy);
|
|
} else if (e.key === "0") {
|
|
e.preventDefault();
|
|
setViewport({ x: 0, y: 0, scale: 1 });
|
|
}
|
|
}
|
|
function handleKeyUp(e: KeyboardEvent) {
|
|
if (e.code === "Space") setIsSpacePan(false);
|
|
if (e.key === "Shift") setIsShiftPressed(false);
|
|
}
|
|
function handleWheelCaptured(e: WheelEvent) {
|
|
if (!e.ctrlKey) return;
|
|
e.preventDefault();
|
|
}
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
window.addEventListener("keyup", handleKeyUp);
|
|
window.addEventListener("wheel", handleWheelCaptured, { capture: true, passive: false });
|
|
return () => {
|
|
window.removeEventListener("keydown", handleKeyDown);
|
|
window.removeEventListener("keyup", handleKeyUp);
|
|
window.removeEventListener("wheel", handleWheelCaptured, { capture: true });
|
|
};
|
|
}, [isSpacePan]);
|
|
|
|
function onContainerPointerDown(e: React.PointerEvent<HTMLDivElement>) {
|
|
if (e.button === 1) {
|
|
e.preventDefault();
|
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
isMiddleMousePan.current = true;
|
|
lastPos.current = { x: e.clientX, y: e.clientY };
|
|
return;
|
|
}
|
|
if (e.button !== 0) return;
|
|
const toolDef = getToolDefinition(tool);
|
|
if (toolDef?.interactionMode !== "select" && !isSpacePan) {
|
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
isPanning.current = true;
|
|
lastPos.current = { x: e.clientX, y: e.clientY };
|
|
return;
|
|
}
|
|
if (isSpacePan) {
|
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
isPanning.current = true;
|
|
lastPos.current = { x: e.clientX, y: e.clientY };
|
|
}
|
|
}
|
|
|
|
function onContainerPointerMove(e: React.PointerEvent<HTMLDivElement>) {
|
|
if (isMiddleMousePan.current || isPanning.current) {
|
|
const dx = e.clientX - lastPos.current.x;
|
|
const dy = e.clientY - lastPos.current.y;
|
|
lastPos.current = { x: e.clientX, y: e.clientY };
|
|
setViewport((vp) => ({ ...vp, x: vp.x + dx, y: vp.y + dy }));
|
|
return;
|
|
}
|
|
if (brushRef.current && onBrushStrokeMove) {
|
|
const { rect } = brushRef.current;
|
|
const localX = (e.clientX - rect.left) / viewport.scale;
|
|
const localY = (e.clientY - rect.top) / viewport.scale;
|
|
brushRef.current.lastX = localX;
|
|
brushRef.current.lastY = localY;
|
|
onBrushStrokeMove(brushRef.current.id, localX, localY);
|
|
return;
|
|
}
|
|
const toolDef = getToolDefinition(tool);
|
|
if (toolDef?.allowsLayerRotate && rotateRef.current && onRotateLayer) {
|
|
const { centerX, centerY, startAngle, startRotation, id } = rotateRef.current;
|
|
const currentAngle = Math.atan2(e.clientY - centerY, e.clientX - centerX);
|
|
const delta = currentAngle - startAngle;
|
|
const nextRotation = startRotation + (delta * 180) / Math.PI;
|
|
rotateRef.current.lastRotation = nextRotation;
|
|
onRotateLayer(id, nextRotation);
|
|
return;
|
|
}
|
|
if (toolDef?.allowsLayerResize && resizeRef.current && onResizeLayer) {
|
|
const dx = (e.clientX - resizeRef.current.startX) / viewport.scale;
|
|
const dy = (e.clientY - resizeRef.current.startY) / viewport.scale;
|
|
const corner = resizeRef.current.corner;
|
|
let nextWidth = resizeRef.current.startWidth;
|
|
let nextHeight = resizeRef.current.startHeight;
|
|
let offsetX = 0;
|
|
let offsetY = 0;
|
|
|
|
if (corner === "br") {
|
|
nextWidth = Math.max(8, resizeRef.current.startWidth + dx);
|
|
nextHeight = Math.max(8, resizeRef.current.startHeight + dy);
|
|
} else if (corner === "bl") {
|
|
nextWidth = Math.max(8, resizeRef.current.startWidth - dx);
|
|
nextHeight = Math.max(8, resizeRef.current.startHeight + dy);
|
|
} else if (corner === "tr") {
|
|
nextWidth = Math.max(8, resizeRef.current.startWidth + dx);
|
|
nextHeight = Math.max(8, resizeRef.current.startHeight - dy);
|
|
} else if (corner === "tl") {
|
|
nextWidth = Math.max(8, resizeRef.current.startWidth - dx);
|
|
nextHeight = Math.max(8, resizeRef.current.startHeight - dy);
|
|
}
|
|
|
|
if (!e.shiftKey && (corner === "br" || corner === "bl" || corner === "tr" || corner === "tl")) {
|
|
const aspect = resizeRef.current.aspect || 1;
|
|
if (Math.abs(dx) > Math.abs(dy)) {
|
|
nextHeight = Math.max(8, nextWidth / aspect);
|
|
} else {
|
|
nextWidth = Math.max(8, nextHeight * aspect);
|
|
}
|
|
}
|
|
|
|
if (corner === "bl" || corner === "tl") offsetX = resizeRef.current.startWidth - nextWidth;
|
|
if (corner === "tr" || corner === "tl") offsetY = resizeRef.current.startHeight - nextHeight;
|
|
|
|
resizeRef.current.lastWidth = nextWidth;
|
|
resizeRef.current.lastHeight = nextHeight;
|
|
resizeMovePendingRef.current = {
|
|
width: nextWidth,
|
|
height: nextHeight,
|
|
x: resizeRef.current.startLayerX + offsetX,
|
|
y: resizeRef.current.startLayerY + offsetY,
|
|
};
|
|
if (resizeMoveRafRef.current !== null) return;
|
|
resizeMoveRafRef.current = window.requestAnimationFrame(() => {
|
|
resizeMoveRafRef.current = null;
|
|
if (!resizeRef.current || !resizeMovePendingRef.current) return;
|
|
const pending = resizeMovePendingRef.current;
|
|
onResizeLayer(resizeRef.current.id, pending.width, pending.height);
|
|
if ((pending.x !== resizeRef.current.startLayerX || pending.y !== resizeRef.current.startLayerY) && onMoveLayer) {
|
|
onMoveLayer(resizeRef.current.id, pending.x, pending.y);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
if (toolDef?.allowsLayerDrag && dragRef.current) {
|
|
const dx = (e.clientX - dragRef.current.startEventX) / viewport.scale;
|
|
const dy = (e.clientY - dragRef.current.startEventY) / viewport.scale;
|
|
const nextX = dragRef.current.startLayerX + dx;
|
|
const nextY = dragRef.current.startLayerY + dy;
|
|
if (dragRef.current.lastX === nextX && dragRef.current.lastY === nextY) return;
|
|
dragRef.current.lastX = nextX;
|
|
dragRef.current.lastY = nextY;
|
|
if (dragMoveRafRef.current !== null) return;
|
|
dragMoveRafRef.current = window.requestAnimationFrame(() => {
|
|
dragMoveRafRef.current = null;
|
|
if (!dragRef.current) return;
|
|
onMoveLayer(dragRef.current.id, dragRef.current.lastX, dragRef.current.lastY);
|
|
});
|
|
}
|
|
}
|
|
|
|
function onContainerPointerUp() {
|
|
if (dragMoveRafRef.current !== null) {
|
|
window.cancelAnimationFrame(dragMoveRafRef.current);
|
|
dragMoveRafRef.current = null;
|
|
}
|
|
if (resizeMoveRafRef.current !== null) {
|
|
window.cancelAnimationFrame(resizeMoveRafRef.current);
|
|
resizeMoveRafRef.current = null;
|
|
if (resizeRef.current && resizeMovePendingRef.current && onResizeLayer) {
|
|
const pending = resizeMovePendingRef.current;
|
|
onResizeLayer(resizeRef.current.id, pending.width, pending.height);
|
|
if ((pending.x !== resizeRef.current.startLayerX || pending.y !== resizeRef.current.startLayerY) && onMoveLayer) {
|
|
onMoveLayer(resizeRef.current.id, pending.x, pending.y);
|
|
}
|
|
}
|
|
}
|
|
isPanning.current = false;
|
|
isMiddleMousePan.current = false;
|
|
if (dragRef.current && onMoveLayerEnd) {
|
|
const { id, lastX, lastY } = dragRef.current;
|
|
if (typeof lastX === "number" && typeof lastY === "number") onMoveLayerEnd(id, lastX, lastY);
|
|
}
|
|
if (resizeRef.current && onResizeLayerEnd) {
|
|
const { id, lastWidth, lastHeight } = resizeRef.current;
|
|
if (typeof lastWidth === "number" && typeof lastHeight === "number") onResizeLayerEnd(id, lastWidth, lastHeight);
|
|
}
|
|
if (rotateRef.current && onRotateLayerEnd) {
|
|
const { id, lastRotation } = rotateRef.current;
|
|
if (typeof lastRotation === "number") onRotateLayerEnd(id, lastRotation);
|
|
}
|
|
if (brushRef.current && onBrushStrokeEnd) {
|
|
onBrushStrokeEnd(brushRef.current.id);
|
|
brushRef.current = null;
|
|
}
|
|
dragRef.current = null;
|
|
resizeRef.current = null;
|
|
resizeMovePendingRef.current = null;
|
|
rotateRef.current = null;
|
|
endInteraction();
|
|
}
|
|
|
|
function onLayerPointerDown(e: React.PointerEvent<HTMLDivElement>, layer: Layer) {
|
|
if (isSpacePan) return;
|
|
e.stopPropagation();
|
|
const toolDef = getToolDefinition(tool);
|
|
if (toolDef?.interactionMode === "paint") {
|
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
const localX = (e.clientX - rect.left) / viewport.scale;
|
|
const localY = (e.clientY - rect.top) / viewport.scale;
|
|
onSelectLayer(layer.id);
|
|
if (tool === "brush" && onBrushStrokeStart) {
|
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
brushRef.current = { id: layer.id, lastX: localX, lastY: localY, rect: e.currentTarget.getBoundingClientRect() };
|
|
const lw = layer.width ?? Math.round(200 * layer.scale);
|
|
const lh = layer.height ?? Math.round(150 * layer.scale);
|
|
onBrushStrokeStart(layer.id, localX, localY, lw, lh);
|
|
beginInteraction();
|
|
} else if (onFillLayer) {
|
|
onFillLayer(layer.id, localX, localY);
|
|
}
|
|
return;
|
|
}
|
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
dragRef.current = {
|
|
id: layer.id,
|
|
startLayerX: layer.x,
|
|
startLayerY: layer.y,
|
|
startEventX: e.clientX,
|
|
startEventY: e.clientY,
|
|
lastX: layer.x,
|
|
lastY: layer.y,
|
|
};
|
|
beginInteraction();
|
|
onSelectLayer(layer.id);
|
|
}
|
|
|
|
function onResizeHandleDown(e: React.PointerEvent<HTMLButtonElement>, layer: Layer, corner: string) {
|
|
if (!getToolDefinition(tool)?.allowsLayerResize || !onResizeLayer) return;
|
|
e.stopPropagation();
|
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
rotateRef.current = null;
|
|
const width = layer.width ?? Math.round(200 * layer.scale);
|
|
const height = layer.height ?? Math.round(150 * layer.scale);
|
|
resizeRef.current = {
|
|
id: layer.id,
|
|
startWidth: width,
|
|
startHeight: height,
|
|
startX: e.clientX,
|
|
startY: e.clientY,
|
|
aspect: width > 0 && height > 0 ? width / height : 1,
|
|
lastWidth: width,
|
|
lastHeight: height,
|
|
corner,
|
|
startLayerX: layer.x,
|
|
startLayerY: layer.y,
|
|
};
|
|
beginInteraction();
|
|
onSelectLayer(layer.id);
|
|
}
|
|
|
|
function onRotateHandleDown(e: React.PointerEvent<HTMLButtonElement>, layer: Layer) {
|
|
if (!getToolDefinition(tool)?.allowsLayerRotate || !onRotateLayer) return;
|
|
e.stopPropagation();
|
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
resizeRef.current = null;
|
|
const rect = containerRef.current?.getBoundingClientRect();
|
|
const width = layer.width ?? Math.round(200 * layer.scale);
|
|
const height = layer.height ?? Math.round(150 * layer.scale);
|
|
const centerX = (rect?.left ?? 0) + viewport.x + layer.x * viewport.scale + (width * viewport.scale) / 2;
|
|
const centerY = (rect?.top ?? 0) + viewport.y + layer.y * viewport.scale + (height * viewport.scale) / 2;
|
|
const startAngle = Math.atan2(e.clientY - centerY, e.clientX - centerX);
|
|
rotateRef.current = {
|
|
id: layer.id,
|
|
centerX,
|
|
centerY,
|
|
startAngle,
|
|
startRotation: layer.rotation,
|
|
lastRotation: layer.rotation,
|
|
};
|
|
beginInteraction();
|
|
onSelectLayer(layer.id);
|
|
}
|
|
|
|
function onTouchStart(e: React.TouchEvent<HTMLDivElement>) {
|
|
if (e.touches.length === 2) {
|
|
const rect = containerRef.current?.getBoundingClientRect();
|
|
if (!rect) return;
|
|
const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2 - rect.left;
|
|
const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2 - rect.top;
|
|
pinchRef.current = {
|
|
active: true,
|
|
initialPinchPx: clientDist(e.touches[0], e.touches[1]),
|
|
initialScale: viewport.scale,
|
|
initialX: viewport.x,
|
|
initialY: viewport.y,
|
|
pivotX: midX,
|
|
pivotY: midY,
|
|
};
|
|
}
|
|
}
|
|
|
|
function onTouchMove(e: React.TouchEvent<HTMLDivElement>) {
|
|
if (pinchRef.current?.active && e.touches.length === 2) {
|
|
e.preventDefault();
|
|
const px = clientDist(e.touches[0], e.touches[1]);
|
|
const ratio = px / pinchRef.current.initialPinchPx;
|
|
const nextScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, pinchRef.current.initialScale * ratio));
|
|
const scaleChange = nextScale / pinchRef.current.initialScale;
|
|
const { pivotX, pivotY, initialX, initialY } = pinchRef.current;
|
|
setViewport({
|
|
x: pivotX - (pivotX - initialX) * scaleChange,
|
|
y: pivotY - (pivotY - initialY) * scaleChange,
|
|
scale: nextScale,
|
|
});
|
|
}
|
|
}
|
|
|
|
function onTouchEnd() {
|
|
if (pinchRef.current) pinchRef.current.active = false;
|
|
}
|
|
|
|
function onContextMenuOpen(e: React.MouseEvent<HTMLDivElement>) {
|
|
if (!onContextMenu) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
onContextMenu(e.clientX - rect.left, e.clientY - rect.top);
|
|
}
|
|
|
|
return {
|
|
containerRef,
|
|
viewport,
|
|
isSpacePan,
|
|
onContainerPointerDown,
|
|
onContainerPointerMove,
|
|
onContainerPointerUp,
|
|
onLayerPointerDown,
|
|
onResizeHandleDown,
|
|
onRotateHandleDown,
|
|
onTouchStart,
|
|
onTouchMove,
|
|
onTouchEnd,
|
|
onContextMenuOpen,
|
|
};
|
|
}
|