From f26cbf24d22c058afb2c3503c2533c75ecb3e956 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 22 Oct 2020 16:10:31 +1100 Subject: [PATCH] Refactored stage zooming to center on the mouse position --- src/components/map/MapEditor.js | 74 ++++++++------------------ src/components/map/MapInteraction.js | 57 +++++++------------- src/components/token/TokenPreview.js | 79 +++++++--------------------- src/helpers/useImageCenter.js | 71 +++++++++++++++++++++++++ src/helpers/useStageInteraction.js | 35 ++++++++---- 5 files changed, 157 insertions(+), 159 deletions(-) create mode 100644 src/helpers/useImageCenter.js diff --git a/src/components/map/MapEditor.js b/src/components/map/MapEditor.js index 9841020..d1e046a 100644 --- a/src/components/map/MapEditor.js +++ b/src/components/map/MapEditor.js @@ -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 ( {showGridControls && canEditGrid && ( - - )} - {showGridControls && canEditGrid && ( - + <> + + + )} diff --git a/src/components/map/MapInteraction.js b/src/components/map/MapInteraction.js index 45e156d..b750f6d 100644 --- a/src/components/map/MapInteraction.js +++ b/src/components/map/MapInteraction.js @@ -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} > diff --git a/src/components/token/TokenPreview.js b/src/components/token/TokenPreview.js index d659908..752a814 100644 --- a/src/components/token/TokenPreview.js +++ b/src/components/token/TokenPreview.js @@ -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} > - + 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; diff --git a/src/helpers/useStageInteraction.js b/src/helpers/useStageInteraction.js index 9ca556f..5d7b6a8 100644 --- a/src/helpers/useStageInteraction.js +++ b/src/helpers/useStageInteraction.js @@ -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);