Refactored stage zooming to center on the mouse position
This commit is contained in:
parent
dddfb3d498
commit
f26cbf24d2
@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef, useEffect, useContext } from "react";
|
||||
import React, { useState, useRef, useContext } from "react";
|
||||
import { Box, IconButton } from "theme-ui";
|
||||
import { Stage, Layer, Image } from "react-konva";
|
||||
import ReactResizeDetector from "react-resize-detector";
|
||||
@ -6,6 +6,7 @@ import ReactResizeDetector from "react-resize-detector";
|
||||
import useMapImage from "../../helpers/useMapImage";
|
||||
import usePreventOverscroll from "../../helpers/usePreventOverscroll";
|
||||
import useStageInteraction from "../../helpers/useStageInteraction";
|
||||
import useImageCenter from "../../helpers/useImageCenter";
|
||||
import { getMapDefaultInset, getMapMaxZoom } from "../../helpers/map";
|
||||
|
||||
import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
|
||||
@ -25,19 +26,6 @@ function MapEditor({ map, onSettingsChange }) {
|
||||
const [stageHeight, setStageHeight] = useState(1);
|
||||
const [stageScale, setStageScale] = useState(1);
|
||||
|
||||
const stageRatio = stageWidth / stageHeight;
|
||||
const mapRatio = map ? map.width / map.height : 1;
|
||||
|
||||
let mapWidth;
|
||||
let mapHeight;
|
||||
if (stageRatio > mapRatio) {
|
||||
mapWidth = map ? stageHeight / (map.height / map.width) : stageWidth;
|
||||
mapHeight = stageHeight;
|
||||
} else {
|
||||
mapWidth = stageWidth;
|
||||
mapHeight = map ? stageWidth * (map.height / map.width) : stageHeight;
|
||||
}
|
||||
|
||||
const defaultInset = getMapDefaultInset(
|
||||
map.width,
|
||||
map.height,
|
||||
@ -46,6 +34,7 @@ function MapEditor({ map, onSettingsChange }) {
|
||||
);
|
||||
|
||||
const stageTranslateRef = useRef({ x: 0, y: 0 });
|
||||
const mapStageRef = useRef();
|
||||
const mapLayerRef = useRef();
|
||||
const [preventMapInteraction, setPreventMapInteraction] = useState(false);
|
||||
|
||||
@ -54,46 +43,32 @@ function MapEditor({ map, onSettingsChange }) {
|
||||
setStageHeight(height);
|
||||
}
|
||||
|
||||
// Reset map translate and scale
|
||||
useEffect(() => {
|
||||
const layer = mapLayerRef.current;
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
if (layer) {
|
||||
let newTranslate;
|
||||
if (stageRatio > mapRatio) {
|
||||
newTranslate = {
|
||||
x: -(mapWidth - containerRect.width) / 2,
|
||||
y: 0,
|
||||
};
|
||||
} else {
|
||||
newTranslate = {
|
||||
x: 0,
|
||||
y: -(mapHeight - containerRect.height) / 2,
|
||||
};
|
||||
}
|
||||
const containerRef = useRef();
|
||||
usePreventOverscroll(containerRef);
|
||||
|
||||
layer.x(newTranslate.x);
|
||||
layer.y(newTranslate.y);
|
||||
layer.draw();
|
||||
stageTranslateRef.current = newTranslate;
|
||||
|
||||
setStageScale(1);
|
||||
}
|
||||
}, [map.id, mapWidth, mapHeight, stageRatio, mapRatio]);
|
||||
const [mapWidth, mapHeight] = useImageCenter(
|
||||
map,
|
||||
mapStageRef,
|
||||
stageWidth,
|
||||
stageHeight,
|
||||
stageTranslateRef,
|
||||
setStageScale,
|
||||
mapLayerRef,
|
||||
containerRef,
|
||||
true
|
||||
);
|
||||
|
||||
const bind = useStageInteraction(
|
||||
mapLayerRef.current,
|
||||
mapStageRef.current,
|
||||
stageScale,
|
||||
setStageScale,
|
||||
stageTranslateRef,
|
||||
mapLayerRef.current,
|
||||
getMapMaxZoom(map),
|
||||
"pan",
|
||||
preventMapInteraction
|
||||
);
|
||||
|
||||
const containerRef = useRef();
|
||||
usePreventOverscroll(containerRef);
|
||||
|
||||
function handleGridChange(inset) {
|
||||
onSettingsChange("grid", {
|
||||
...map.grid,
|
||||
@ -129,7 +104,6 @@ function MapEditor({ map, onSettingsChange }) {
|
||||
map.grid.inset.topLeft.y !== defaultInset.topLeft.y ||
|
||||
map.grid.inset.bottomRight.x !== defaultInset.bottomRight.x ||
|
||||
map.grid.inset.bottomRight.y !== defaultInset.bottomRight.y;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -149,19 +123,17 @@ function MapEditor({ map, onSettingsChange }) {
|
||||
width={stageWidth}
|
||||
height={stageHeight}
|
||||
scale={{ x: stageScale, y: stageScale }}
|
||||
x={stageWidth / 2}
|
||||
y={stageHeight / 2}
|
||||
offset={{ x: stageWidth / 2, y: stageHeight / 2 }}
|
||||
ref={mapStageRef}
|
||||
>
|
||||
<Layer ref={mapLayerRef}>
|
||||
<Image image={mapImageSource} width={mapWidth} height={mapHeight} />
|
||||
<KeyboardContext.Provider value={keyboardValue}>
|
||||
<MapInteractionProvider value={mapInteraction}>
|
||||
{showGridControls && canEditGrid && (
|
||||
<>
|
||||
<MapGrid map={map} strokeWidth={0.5} />
|
||||
)}
|
||||
{showGridControls && canEditGrid && (
|
||||
<MapGridEditor map={map} onGridChange={handleGridChange} />
|
||||
</>
|
||||
)}
|
||||
</MapInteractionProvider>
|
||||
</KeyboardContext.Provider>
|
||||
|
@ -8,6 +8,7 @@ import useMapImage from "../../helpers/useMapImage";
|
||||
import usePreventOverscroll from "../../helpers/usePreventOverscroll";
|
||||
import useKeyboard from "../../helpers/useKeyboard";
|
||||
import useStageInteraction from "../../helpers/useStageInteraction";
|
||||
import useImageCenter from "../../helpers/useImageCenter";
|
||||
import { getMapMaxZoom } from "../../helpers/map";
|
||||
|
||||
import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
|
||||
@ -43,52 +44,41 @@ function MapInteraction({
|
||||
const [stageScale, setStageScale] = useState(1);
|
||||
const [preventMapInteraction, setPreventMapInteraction] = useState(false);
|
||||
|
||||
const stageWidthRef = useRef(stageWidth);
|
||||
const stageHeightRef = useRef(stageHeight);
|
||||
// Avoid state udpates when panning the map by using a ref and updating the konva element directly
|
||||
const stageTranslateRef = useRef({ x: 0, y: 0 });
|
||||
|
||||
// Reset transform when map changes
|
||||
const previousMapIdRef = useRef();
|
||||
useEffect(() => {
|
||||
const layer = mapLayerRef.current;
|
||||
const previousMapId = previousMapIdRef.current;
|
||||
if (map && layer && previousMapId !== map.id) {
|
||||
const mapHeight = stageWidthRef.current * (map.height / map.width);
|
||||
const newTranslate = {
|
||||
x: 0,
|
||||
y: -(mapHeight - stageHeightRef.current) / 2,
|
||||
};
|
||||
layer.x(newTranslate.x);
|
||||
layer.y(newTranslate.y);
|
||||
layer.draw();
|
||||
stageTranslateRef.current = newTranslate;
|
||||
|
||||
setStageScale(1);
|
||||
}
|
||||
previousMapIdRef.current = map && map.id;
|
||||
}, [map]);
|
||||
const mapStageRef = useContext(MapStageContext);
|
||||
const mapLayerRef = useRef();
|
||||
const mapImageRef = useRef();
|
||||
|
||||
function handleResize(width, height) {
|
||||
setStageWidth(width);
|
||||
setStageHeight(height);
|
||||
stageWidthRef.current = width;
|
||||
stageHeightRef.current = height;
|
||||
}
|
||||
|
||||
const mapStageRef = useContext(MapStageContext);
|
||||
const mapLayerRef = useRef();
|
||||
const mapImageRef = useRef();
|
||||
const containerRef = useRef();
|
||||
usePreventOverscroll(containerRef);
|
||||
|
||||
const [mapWidth, mapHeight] = useImageCenter(
|
||||
map,
|
||||
mapStageRef,
|
||||
stageWidth,
|
||||
stageHeight,
|
||||
stageTranslateRef,
|
||||
setStageScale,
|
||||
mapLayerRef,
|
||||
containerRef
|
||||
);
|
||||
|
||||
const previousSelectedToolRef = useRef(selectedToolId);
|
||||
|
||||
const [interactionEmitter] = useState(new EventEmitter());
|
||||
|
||||
const bind = useStageInteraction(
|
||||
mapLayerRef.current,
|
||||
mapStageRef.current,
|
||||
stageScale,
|
||||
setStageScale,
|
||||
stageTranslateRef,
|
||||
mapLayerRef.current,
|
||||
getMapMaxZoom(map),
|
||||
selectedToolId,
|
||||
preventMapInteraction,
|
||||
@ -171,12 +161,6 @@ function MapInteraction({
|
||||
}
|
||||
}
|
||||
|
||||
const containerRef = useRef();
|
||||
usePreventOverscroll(containerRef);
|
||||
|
||||
const mapWidth = stageWidth;
|
||||
const mapHeight = map ? stageWidth * (map.height / map.width) : stageHeight;
|
||||
|
||||
const auth = useContext(AuthContext);
|
||||
const settings = useContext(SettingsContext);
|
||||
|
||||
@ -208,9 +192,6 @@ function MapInteraction({
|
||||
width={stageWidth}
|
||||
height={stageHeight}
|
||||
scale={{ x: stageScale, y: stageScale }}
|
||||
x={stageWidth / 2}
|
||||
y={stageHeight / 2}
|
||||
offset={{ x: stageWidth / 2, y: stageHeight / 2 }}
|
||||
ref={mapStageRef}
|
||||
>
|
||||
<Layer ref={mapLayerRef}>
|
||||
|
@ -7,6 +7,7 @@ import useImage from "use-image";
|
||||
import usePreventOverscroll from "../../helpers/usePreventOverscroll";
|
||||
import useStageInteraction from "../../helpers/useStageInteraction";
|
||||
import useDataSource from "../../helpers/useDataSource";
|
||||
import useImageCenter from "../../helpers/useImageCenter";
|
||||
|
||||
import GridOnIcon from "../../icons/GridOnIcon";
|
||||
import GridOffIcon from "../../icons/GridOffIcon";
|
||||
@ -29,81 +30,43 @@ function TokenPreview({ token }) {
|
||||
unknownSource
|
||||
);
|
||||
const [tokenSourceImage] = useImage(tokenSource);
|
||||
const [tokenRatio, setTokenRatio] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
if (tokenSourceImage) {
|
||||
setTokenRatio(tokenSourceImage.width / tokenSourceImage.height);
|
||||
}
|
||||
}, [tokenSourceImage]);
|
||||
|
||||
const [stageWidth, setStageWidth] = useState(1);
|
||||
const [stageHeight, setStageHeight] = useState(1);
|
||||
const [stageScale, setStageScale] = useState(1);
|
||||
|
||||
const stageRatio = stageWidth / stageHeight;
|
||||
|
||||
let tokenWidth;
|
||||
let tokenHeight;
|
||||
if (stageRatio > tokenRatio) {
|
||||
tokenWidth = tokenSourceImage
|
||||
? stageHeight / (tokenSourceImage.height / tokenSourceImage.width)
|
||||
: stageWidth;
|
||||
tokenHeight = stageHeight;
|
||||
} else {
|
||||
tokenWidth = stageWidth;
|
||||
tokenHeight = tokenSourceImage
|
||||
? stageWidth * (tokenSourceImage.height / tokenSourceImage.width)
|
||||
: stageHeight;
|
||||
}
|
||||
|
||||
const stageTranslateRef = useRef({ x: 0, y: 0 });
|
||||
const mapLayerRef = useRef();
|
||||
const tokenStageRef = useRef();
|
||||
const tokenLayerRef = useRef();
|
||||
|
||||
function handleResize(width, height) {
|
||||
setStageWidth(width);
|
||||
setStageHeight(height);
|
||||
}
|
||||
|
||||
// Reset map translate and scale
|
||||
useEffect(() => {
|
||||
const layer = mapLayerRef.current;
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
if (layer) {
|
||||
let newTranslate;
|
||||
if (stageRatio > tokenRatio) {
|
||||
newTranslate = {
|
||||
x: -(tokenWidth - containerRect.width) / 2,
|
||||
y: 0,
|
||||
};
|
||||
} else {
|
||||
newTranslate = {
|
||||
x: 0,
|
||||
y: -(tokenHeight - containerRect.height) / 2,
|
||||
};
|
||||
}
|
||||
const containerRef = useRef();
|
||||
usePreventOverscroll(containerRef);
|
||||
|
||||
layer.x(newTranslate.x);
|
||||
layer.y(newTranslate.y);
|
||||
layer.draw();
|
||||
stageTranslateRef.current = newTranslate;
|
||||
|
||||
setStageScale(1);
|
||||
}
|
||||
}, [token.id, tokenWidth, tokenHeight, stageRatio, tokenRatio]);
|
||||
const [tokenWidth, tokenHeight] = useImageCenter(
|
||||
token,
|
||||
tokenStageRef,
|
||||
stageWidth,
|
||||
stageHeight,
|
||||
stageTranslateRef,
|
||||
setStageScale,
|
||||
tokenLayerRef,
|
||||
containerRef,
|
||||
true
|
||||
);
|
||||
|
||||
const bind = useStageInteraction(
|
||||
mapLayerRef.current,
|
||||
tokenStageRef.current,
|
||||
stageScale,
|
||||
setStageScale,
|
||||
stageTranslateRef,
|
||||
10,
|
||||
"pan"
|
||||
tokenLayerRef.current
|
||||
);
|
||||
|
||||
const containerRef = useRef();
|
||||
usePreventOverscroll(containerRef);
|
||||
|
||||
const [showGridPreview, setShowGridPreview] = useState(true);
|
||||
const gridWidth = tokenWidth;
|
||||
const gridX = token.defaultSize;
|
||||
@ -134,11 +97,9 @@ function TokenPreview({ token }) {
|
||||
width={stageWidth}
|
||||
height={stageHeight}
|
||||
scale={{ x: stageScale, y: stageScale }}
|
||||
x={stageWidth / 2}
|
||||
y={stageHeight / 2}
|
||||
offset={{ x: stageWidth / 2, y: stageHeight / 2 }}
|
||||
ref={tokenStageRef}
|
||||
>
|
||||
<Layer ref={mapLayerRef}>
|
||||
<Layer ref={tokenLayerRef}>
|
||||
<Image
|
||||
image={tokenSourceImage}
|
||||
width={tokenWidth}
|
||||
|
71
src/helpers/useImageCenter.js
Normal file
71
src/helpers/useImageCenter.js
Normal file
@ -0,0 +1,71 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
function useImageCenter(
|
||||
data,
|
||||
stageRef,
|
||||
stageWidth,
|
||||
stageHeight,
|
||||
stageTranslateRef,
|
||||
setStageScale,
|
||||
imageLayerRef,
|
||||
containerRef,
|
||||
responsive = false
|
||||
) {
|
||||
const stageRatio = stageWidth / stageHeight;
|
||||
const imageRatio = data ? data.width / data.height : 1;
|
||||
|
||||
let imageWidth;
|
||||
let imageHeight;
|
||||
if (stageRatio > imageRatio) {
|
||||
imageWidth = data ? stageHeight / (data.height / data.width) : stageWidth;
|
||||
imageHeight = stageHeight;
|
||||
} else {
|
||||
imageWidth = stageWidth;
|
||||
imageHeight = data ? stageWidth * (data.height / data.width) : stageHeight;
|
||||
}
|
||||
|
||||
// Reset data translate and scale
|
||||
const previousDataIdRef = useRef();
|
||||
const previousStageRatioRef = useRef(stageRatio);
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const layer = imageLayerRef.current;
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const previousDataId = previousDataIdRef.current;
|
||||
const previousStageRatio = previousStageRatioRef.current;
|
||||
|
||||
// Update when the id has changed and if responsive update when the stage changes
|
||||
const shouldUpdate = responsive
|
||||
? previousDataId !== data.id || previousStageRatio !== stageRatio
|
||||
: previousDataId !== data.id;
|
||||
|
||||
if (layer && shouldUpdate) {
|
||||
let newTranslate;
|
||||
if (stageRatio > imageRatio) {
|
||||
newTranslate = {
|
||||
x: -(imageWidth - containerRect.width) / 2,
|
||||
y: 0,
|
||||
};
|
||||
} else {
|
||||
newTranslate = {
|
||||
x: 0,
|
||||
y: -(imageHeight - containerRect.height) / 2,
|
||||
};
|
||||
}
|
||||
layer.position(newTranslate);
|
||||
stageRef.current.position({ x: 0, y: 0 });
|
||||
stageTranslateRef.current = { x: 0, y: 0 };
|
||||
|
||||
setStageScale(1);
|
||||
}
|
||||
previousDataIdRef.current = data.id;
|
||||
previousStageRatioRef.current = stageRatio;
|
||||
});
|
||||
|
||||
return [imageWidth, imageHeight];
|
||||
}
|
||||
|
||||
export default useImageCenter;
|
@ -7,10 +7,11 @@ const touchZoomSpeed = 0.005;
|
||||
const minZoom = 0.1;
|
||||
|
||||
function useStageInteraction(
|
||||
layer,
|
||||
stage,
|
||||
stageScale,
|
||||
onStageScaleChange,
|
||||
stageTranslateRef,
|
||||
layer,
|
||||
maxZoom = 10,
|
||||
tool = "pan",
|
||||
preventInteraction = false,
|
||||
@ -43,6 +44,20 @@ function useStageInteraction(
|
||||
),
|
||||
maxZoom
|
||||
);
|
||||
|
||||
const pointer = stage.getPointerPosition();
|
||||
const pointerChange = {
|
||||
x: (pointer.x - stage.x()) / stageScale,
|
||||
y: (pointer.y - stage.y()) / stageScale,
|
||||
};
|
||||
|
||||
const newTranslate = {
|
||||
x: pointer.x - pointerChange.x * newScale,
|
||||
y: pointer.y - pointerChange.y * newScale,
|
||||
};
|
||||
stage.position(newTranslate);
|
||||
stageTranslateRef.current = newTranslate;
|
||||
|
||||
onStageScaleChange(newScale);
|
||||
gesture.onWheel && gesture.onWheel(props);
|
||||
},
|
||||
@ -68,12 +83,11 @@ function useStageInteraction(
|
||||
// Apply translate
|
||||
const stageTranslate = stageTranslateRef.current;
|
||||
const newTranslate = {
|
||||
x: stageTranslate.x + originXDelta / newScale,
|
||||
y: stageTranslate.y + originYDelta / newScale,
|
||||
x: stageTranslate.x + originXDelta,
|
||||
y: stageTranslate.y + originYDelta,
|
||||
};
|
||||
layer.x(newTranslate.x);
|
||||
layer.y(newTranslate.y);
|
||||
layer.draw();
|
||||
stage.position(newTranslate);
|
||||
stage.draw();
|
||||
stageTranslateRef.current = newTranslate;
|
||||
|
||||
pinchPreviousDistanceRef.current = distance;
|
||||
@ -96,12 +110,11 @@ function useStageInteraction(
|
||||
const stageTranslate = stageTranslateRef.current;
|
||||
if (tool === "pan") {
|
||||
const newTranslate = {
|
||||
x: stageTranslate.x + dx / stageScale,
|
||||
y: stageTranslate.y + dy / stageScale,
|
||||
x: stageTranslate.x + dx,
|
||||
y: stageTranslate.y + dy,
|
||||
};
|
||||
layer.x(newTranslate.x);
|
||||
layer.y(newTranslate.y);
|
||||
layer.draw();
|
||||
stage.position(newTranslate);
|
||||
stage.draw();
|
||||
stageTranslateRef.current = newTranslate;
|
||||
}
|
||||
gesture.onDrag && gesture.onDrag(props);
|
||||
|
Loading…
Reference in New Issue
Block a user