259 lines
6.7 KiB
TypeScript
259 lines
6.7 KiB
TypeScript
import {
|
|
type RefObject,
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
} from 'react';
|
|
|
|
import {
|
|
DEFAULT_CANVAS_SIZE,
|
|
} from './ImageCanvasEditorModel';
|
|
import type {
|
|
CanvasLayer,
|
|
CanvasViewport,
|
|
DragState,
|
|
} from './ImageCanvasEditorTypes';
|
|
import {
|
|
createMinimapModel,
|
|
fitViewportToLayers,
|
|
getCanvasDropPoint as resolveCanvasDropPoint,
|
|
getCanvasPointFromClient as resolveCanvasPointFromClient,
|
|
getWorldPointFromClient,
|
|
moveViewportFromMinimapDrag as resolveViewportFromMinimapDrag,
|
|
moveViewportFromMinimapPointer as resolveViewportFromMinimapPointer,
|
|
scaleViewportFromScreenPoint,
|
|
scrollViewportVertically,
|
|
zoomViewportFromWheel,
|
|
} from './ImageCanvasInteractionModel';
|
|
|
|
export const DEFAULT_IMAGE_CANVAS_VIEWPORT: CanvasViewport = {
|
|
x: -260,
|
|
y: 70,
|
|
scale: 0.82,
|
|
};
|
|
|
|
type UseImageCanvasViewportControlsOptions = {
|
|
canvasViewportRef: RefObject<HTMLDivElement | null>;
|
|
layers: CanvasLayer[];
|
|
captureCanvasHistory: () => void;
|
|
};
|
|
|
|
export function useImageCanvasViewportControls({
|
|
canvasViewportRef,
|
|
layers,
|
|
captureCanvasHistory,
|
|
}: UseImageCanvasViewportControlsOptions) {
|
|
const [viewport, setViewport] = useState<CanvasViewport>(
|
|
DEFAULT_IMAGE_CANVAS_VIEWPORT,
|
|
);
|
|
const [canvasSize, setCanvasSize] = useState(DEFAULT_CANVAS_SIZE);
|
|
|
|
const minimapModel = useMemo(
|
|
() => createMinimapModel({ layers, viewport, canvasSize }),
|
|
[canvasSize, layers, viewport],
|
|
);
|
|
|
|
useEffect(() => {
|
|
const viewportElement = canvasViewportRef.current;
|
|
if (!viewportElement) {
|
|
return undefined;
|
|
}
|
|
|
|
const updateCanvasSize = () => {
|
|
setCanvasSize({
|
|
width: viewportElement.clientWidth || DEFAULT_CANVAS_SIZE.width,
|
|
height: viewportElement.clientHeight || DEFAULT_CANVAS_SIZE.height,
|
|
});
|
|
};
|
|
|
|
updateCanvasSize();
|
|
|
|
if (typeof ResizeObserver === 'undefined') {
|
|
window.addEventListener('resize', updateCanvasSize);
|
|
return () => window.removeEventListener('resize', updateCanvasSize);
|
|
}
|
|
|
|
const observer = new ResizeObserver(updateCanvasSize);
|
|
observer.observe(viewportElement);
|
|
return () => observer.disconnect();
|
|
}, [canvasViewportRef]);
|
|
|
|
const updateScaleFromCenter = useCallback(
|
|
(nextScale: number) => {
|
|
const viewportElement = canvasViewportRef.current;
|
|
if (!viewportElement) {
|
|
captureCanvasHistory();
|
|
setViewport((currentViewport) =>
|
|
scaleViewportFromScreenPoint({
|
|
viewport: currentViewport,
|
|
nextScale,
|
|
screenPoint: null,
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
|
|
const rect = viewportElement.getBoundingClientRect();
|
|
const centerX = rect.width > 0 ? rect.width / 2 : canvasSize.width / 2;
|
|
const centerY = rect.height > 0 ? rect.height / 2 : canvasSize.height / 2;
|
|
captureCanvasHistory();
|
|
setViewport((currentViewport) =>
|
|
scaleViewportFromScreenPoint({
|
|
viewport: currentViewport,
|
|
nextScale,
|
|
screenPoint: { x: centerX, y: centerY },
|
|
}),
|
|
);
|
|
},
|
|
[canvasSize.height, canvasSize.width, canvasViewportRef, captureCanvasHistory],
|
|
);
|
|
|
|
const fitLayers = useCallback(
|
|
(targetLayers: CanvasLayer[] = layers) => {
|
|
const nextViewport = fitViewportToLayers({
|
|
layers: targetLayers,
|
|
canvasSize,
|
|
});
|
|
if (!nextViewport) {
|
|
return;
|
|
}
|
|
|
|
captureCanvasHistory();
|
|
setViewport(nextViewport);
|
|
},
|
|
[captureCanvasHistory, canvasSize, layers],
|
|
);
|
|
|
|
const resolveCanvasPoint = useCallback(
|
|
(clientX: number, clientY: number) => {
|
|
const rect = canvasViewportRef.current?.getBoundingClientRect() ?? null;
|
|
return resolveCanvasPointFromClient({ clientX, clientY, rect });
|
|
},
|
|
[canvasViewportRef],
|
|
);
|
|
|
|
const getCanvasDropPoint = useCallback(
|
|
(clientX: number, clientY: number) =>
|
|
resolveCanvasDropPoint({
|
|
clientX,
|
|
clientY,
|
|
rect: canvasViewportRef.current?.getBoundingClientRect() ?? null,
|
|
canvasSize,
|
|
}),
|
|
[canvasSize, canvasViewportRef],
|
|
);
|
|
|
|
const getCanvasPointFromClient = useCallback(
|
|
(clientX: number, clientY: number) =>
|
|
getWorldPointFromClient({
|
|
clientX,
|
|
clientY,
|
|
rect: canvasViewportRef.current?.getBoundingClientRect() ?? null,
|
|
viewport,
|
|
}),
|
|
[canvasViewportRef, viewport],
|
|
);
|
|
|
|
const moveViewportFromMinimapPointer = useCallback(
|
|
(clientX: number, clientY: number) => {
|
|
if (!minimapModel) {
|
|
return;
|
|
}
|
|
const minimapElement = document.querySelector(
|
|
'.image-canvas-editor__minimap',
|
|
) as HTMLElement | null;
|
|
const rect = minimapElement?.getBoundingClientRect();
|
|
if (!rect) {
|
|
return;
|
|
}
|
|
setViewport((currentViewport) =>
|
|
resolveViewportFromMinimapPointer({
|
|
viewport: currentViewport,
|
|
canvasSize,
|
|
minimapModel,
|
|
pointer: {
|
|
x: clientX - rect.left,
|
|
y: clientY - rect.top,
|
|
},
|
|
}),
|
|
);
|
|
},
|
|
[canvasSize, minimapModel],
|
|
);
|
|
|
|
const updateViewportFromMinimapDrag = useCallback(
|
|
(
|
|
dragState: Extract<DragState, { kind: 'minimap' }>,
|
|
clientX: number,
|
|
clientY: number,
|
|
) => {
|
|
setViewport(
|
|
resolveViewportFromMinimapDrag(dragState, {
|
|
x: clientX,
|
|
y: clientY,
|
|
}),
|
|
);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const handleNativeWheel = useCallback(
|
|
(event: WheelEvent) => {
|
|
event.preventDefault();
|
|
const viewportElement = canvasViewportRef.current;
|
|
if (!viewportElement) {
|
|
return;
|
|
}
|
|
|
|
if (!event.ctrlKey && !event.metaKey) {
|
|
setViewport((currentViewport) =>
|
|
scrollViewportVertically(currentViewport, event.deltaY),
|
|
);
|
|
return;
|
|
}
|
|
|
|
const rect = viewportElement.getBoundingClientRect();
|
|
const screenPoint = {
|
|
x: event.clientX - rect.left,
|
|
y: event.clientY - rect.top,
|
|
};
|
|
setViewport((currentViewport) =>
|
|
zoomViewportFromWheel({
|
|
viewport: currentViewport,
|
|
deltaY: event.deltaY,
|
|
screenPoint,
|
|
}),
|
|
);
|
|
},
|
|
[canvasViewportRef],
|
|
);
|
|
|
|
useEffect(() => {
|
|
const viewportElement = canvasViewportRef.current;
|
|
if (!viewportElement) {
|
|
return undefined;
|
|
}
|
|
viewportElement.addEventListener('wheel', handleNativeWheel, {
|
|
passive: false,
|
|
});
|
|
return () => {
|
|
viewportElement.removeEventListener('wheel', handleNativeWheel);
|
|
};
|
|
}, [canvasViewportRef, handleNativeWheel]);
|
|
|
|
return {
|
|
viewport,
|
|
setViewport,
|
|
canvasSize,
|
|
minimapModel,
|
|
updateScaleFromCenter,
|
|
fitLayers,
|
|
resolveCanvasPoint,
|
|
getCanvasDropPoint,
|
|
getCanvasPointFromClient,
|
|
moveViewportFromMinimapPointer,
|
|
updateViewportFromMinimapDrag,
|
|
};
|
|
}
|