Refactored konva map interaction and map image source
This commit is contained in:
parent
438e0c0bb3
commit
f783a9bb70
@ -2,23 +2,13 @@ import React, { useState, useRef, useEffect } from "react";
|
|||||||
import { Box } from "theme-ui";
|
import { Box } from "theme-ui";
|
||||||
import { Stage, Layer, Image } from "react-konva";
|
import { Stage, Layer, Image } from "react-konva";
|
||||||
import ReactResizeDetector from "react-resize-detector";
|
import ReactResizeDetector from "react-resize-detector";
|
||||||
import useImage from "use-image";
|
|
||||||
import { useGesture } from "react-use-gesture";
|
|
||||||
import normalizeWheel from "normalize-wheel";
|
|
||||||
|
|
||||||
import useDataSource from "../../helpers/useDataSource";
|
import useMapImage from "../../helpers/useMapImage";
|
||||||
import usePreventOverscroll from "../../helpers/usePreventOverscroll";
|
import usePreventOverscroll from "../../helpers/usePreventOverscroll";
|
||||||
|
import useStageInteraction from "../../helpers/useStageInteraction";
|
||||||
import { mapSources as defaultMapSources } from "../../maps";
|
|
||||||
|
|
||||||
const wheelZoomSpeed = -0.001;
|
|
||||||
const touchZoomSpeed = 0.005;
|
|
||||||
const minZoom = 0.1;
|
|
||||||
const maxZoom = 5;
|
|
||||||
|
|
||||||
function MapEditor({ map }) {
|
function MapEditor({ map }) {
|
||||||
const mapSource = useDataSource(map, defaultMapSources);
|
const [mapImageSource] = useMapImage(map);
|
||||||
const [mapSourceImage] = useImage(mapSource);
|
|
||||||
|
|
||||||
const [stageWidth, setStageWidth] = useState(1);
|
const [stageWidth, setStageWidth] = useState(1);
|
||||||
const [stageHeight, setStageHeight] = useState(1);
|
const [stageHeight, setStageHeight] = useState(1);
|
||||||
@ -38,9 +28,6 @@ function MapEditor({ map }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const stageTranslateRef = useRef({ x: 0, y: 0 });
|
const stageTranslateRef = useRef({ x: 0, y: 0 });
|
||||||
const isInteractingWithCanvas = useRef(false);
|
|
||||||
const pinchPreviousDistanceRef = useRef();
|
|
||||||
const pinchPreviousOriginRef = useRef();
|
|
||||||
const mapLayerRef = useRef();
|
const mapLayerRef = useRef();
|
||||||
|
|
||||||
function handleResize(width, height) {
|
function handleResize(width, height) {
|
||||||
@ -48,10 +35,11 @@ function MapEditor({ map }) {
|
|||||||
setStageHeight(height);
|
setStageHeight(height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset map translate and scale
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const layer = mapLayerRef.current;
|
const layer = mapLayerRef.current;
|
||||||
const containerRect = containerRef.current.getBoundingClientRect();
|
const containerRect = containerRef.current.getBoundingClientRect();
|
||||||
if (map && layer) {
|
if (layer) {
|
||||||
let newTranslate;
|
let newTranslate;
|
||||||
if (stageRatio > mapRatio) {
|
if (stageRatio > mapRatio) {
|
||||||
newTranslate = {
|
newTranslate = {
|
||||||
@ -72,80 +60,14 @@ function MapEditor({ map }) {
|
|||||||
|
|
||||||
setStageScale(1);
|
setStageScale(1);
|
||||||
}
|
}
|
||||||
}, [map, mapWidth, mapHeight, stageRatio, mapRatio]);
|
}, [map.id, mapWidth, mapHeight, stageRatio, mapRatio]);
|
||||||
|
|
||||||
const bind = useGesture({
|
const bind = useStageInteraction(
|
||||||
onWheelStart: ({ event }) => {
|
mapLayerRef.current,
|
||||||
isInteractingWithCanvas.current =
|
stageScale,
|
||||||
event.target === mapLayerRef.current.getCanvas()._canvas;
|
setStageScale,
|
||||||
},
|
stageTranslateRef
|
||||||
onWheel: ({ event }) => {
|
);
|
||||||
event.persist();
|
|
||||||
const { pixelY } = normalizeWheel(event);
|
|
||||||
if (!isInteractingWithCanvas.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newScale = Math.min(
|
|
||||||
Math.max(stageScale + pixelY * wheelZoomSpeed, minZoom),
|
|
||||||
maxZoom
|
|
||||||
);
|
|
||||||
setStageScale(newScale);
|
|
||||||
},
|
|
||||||
onPinch: ({ da, origin, first }) => {
|
|
||||||
const [distance] = da;
|
|
||||||
const [originX, originY] = origin;
|
|
||||||
if (first) {
|
|
||||||
pinchPreviousDistanceRef.current = distance;
|
|
||||||
pinchPreviousOriginRef.current = { x: originX, y: originY };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply scale
|
|
||||||
const distanceDelta = distance - pinchPreviousDistanceRef.current;
|
|
||||||
const originXDelta = originX - pinchPreviousOriginRef.current.x;
|
|
||||||
const originYDelta = originY - pinchPreviousOriginRef.current.y;
|
|
||||||
const newScale = Math.min(
|
|
||||||
Math.max(stageScale + distanceDelta * touchZoomSpeed, minZoom),
|
|
||||||
maxZoom
|
|
||||||
);
|
|
||||||
setStageScale(newScale);
|
|
||||||
|
|
||||||
// Apply translate
|
|
||||||
const stageTranslate = stageTranslateRef.current;
|
|
||||||
const layer = mapLayerRef.current;
|
|
||||||
const newTranslate = {
|
|
||||||
x: stageTranslate.x + originXDelta / newScale,
|
|
||||||
y: stageTranslate.y + originYDelta / newScale,
|
|
||||||
};
|
|
||||||
layer.x(newTranslate.x);
|
|
||||||
layer.y(newTranslate.y);
|
|
||||||
layer.draw();
|
|
||||||
stageTranslateRef.current = newTranslate;
|
|
||||||
|
|
||||||
pinchPreviousDistanceRef.current = distance;
|
|
||||||
pinchPreviousOriginRef.current = { x: originX, y: originY };
|
|
||||||
},
|
|
||||||
onDragStart: ({ event }) => {
|
|
||||||
isInteractingWithCanvas.current =
|
|
||||||
event.target === mapLayerRef.current.getCanvas()._canvas;
|
|
||||||
},
|
|
||||||
onDrag: ({ delta, pinching }) => {
|
|
||||||
if (pinching || !isInteractingWithCanvas.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [dx, dy] = delta;
|
|
||||||
const stageTranslate = stageTranslateRef.current;
|
|
||||||
const layer = mapLayerRef.current;
|
|
||||||
const newTranslate = {
|
|
||||||
x: stageTranslate.x + dx / stageScale,
|
|
||||||
y: stageTranslate.y + dy / stageScale,
|
|
||||||
};
|
|
||||||
layer.x(newTranslate.x);
|
|
||||||
layer.y(newTranslate.y);
|
|
||||||
layer.draw();
|
|
||||||
stageTranslateRef.current = newTranslate;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const containerRef = useRef();
|
const containerRef = useRef();
|
||||||
usePreventOverscroll(containerRef);
|
usePreventOverscroll(containerRef);
|
||||||
@ -155,7 +77,7 @@ function MapEditor({ map }) {
|
|||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "300px",
|
height: "300px",
|
||||||
cursor: "pointer",
|
cursor: "move",
|
||||||
touchAction: "none",
|
touchAction: "none",
|
||||||
outline: "none",
|
outline: "none",
|
||||||
}}
|
}}
|
||||||
@ -173,7 +95,7 @@ function MapEditor({ map }) {
|
|||||||
offset={{ x: stageWidth / 2, y: stageHeight / 2 }}
|
offset={{ x: stageWidth / 2, y: stageHeight / 2 }}
|
||||||
>
|
>
|
||||||
<Layer ref={mapLayerRef}>
|
<Layer ref={mapLayerRef}>
|
||||||
<Image image={mapSourceImage} width={mapWidth} height={mapHeight} />
|
<Image image={mapImageSource} width={mapWidth} height={mapHeight} />
|
||||||
</Layer>
|
</Layer>
|
||||||
</Stage>
|
</Stage>
|
||||||
</ReactResizeDetector>
|
</ReactResizeDetector>
|
||||||
|
@ -1,17 +1,13 @@
|
|||||||
import React, { useRef, useEffect, useState, useContext } from "react";
|
import React, { useRef, useEffect, useState, useContext } from "react";
|
||||||
import { Box } from "theme-ui";
|
import { Box } from "theme-ui";
|
||||||
import { useGesture } from "react-use-gesture";
|
|
||||||
import ReactResizeDetector from "react-resize-detector";
|
import ReactResizeDetector from "react-resize-detector";
|
||||||
import useImage from "use-image";
|
|
||||||
import { Stage, Layer, Image } from "react-konva";
|
import { Stage, Layer, Image } from "react-konva";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import normalizeWheel from "normalize-wheel";
|
|
||||||
|
|
||||||
|
import useMapImage from "../../helpers/useMapImage";
|
||||||
import usePreventOverscroll from "../../helpers/usePreventOverscroll";
|
import usePreventOverscroll from "../../helpers/usePreventOverscroll";
|
||||||
import useDataSource from "../../helpers/useDataSource";
|
|
||||||
import useKeyboard from "../../helpers/useKeyboard";
|
import useKeyboard from "../../helpers/useKeyboard";
|
||||||
|
import useStageInteraction from "../../helpers/useStageInteraction";
|
||||||
import { mapSources as defaultMapSources } from "../../maps";
|
|
||||||
|
|
||||||
import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
|
import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
|
||||||
import MapStageContext, {
|
import MapStageContext, {
|
||||||
@ -21,11 +17,6 @@ import AuthContext from "../../contexts/AuthContext";
|
|||||||
import SettingsContext from "../../contexts/SettingsContext";
|
import SettingsContext from "../../contexts/SettingsContext";
|
||||||
import KeyboardContext from "../../contexts/KeyboardContext";
|
import KeyboardContext from "../../contexts/KeyboardContext";
|
||||||
|
|
||||||
const wheelZoomSpeed = -0.001;
|
|
||||||
const touchZoomSpeed = 0.005;
|
|
||||||
const minZoom = 0.1;
|
|
||||||
const maxZoom = 5;
|
|
||||||
|
|
||||||
function MapInteraction({
|
function MapInteraction({
|
||||||
map,
|
map,
|
||||||
children,
|
children,
|
||||||
@ -34,29 +25,7 @@ function MapInteraction({
|
|||||||
onSelectedToolChange,
|
onSelectedToolChange,
|
||||||
disabledControls,
|
disabledControls,
|
||||||
}) {
|
}) {
|
||||||
let mapSourceMap = map;
|
const [mapImageSource, mapImageSourceStatus] = useMapImage(map);
|
||||||
if (map && map.type === "file" && map.resolutions) {
|
|
||||||
// Set to the quality if available
|
|
||||||
if (map.quality !== "original" && map.resolutions[map.quality]) {
|
|
||||||
mapSourceMap = map.resolutions[map.quality];
|
|
||||||
} else if (!map.file) {
|
|
||||||
// If no file fallback to the highest resolution
|
|
||||||
for (let resolution in map.resolutions) {
|
|
||||||
mapSourceMap = map.resolutions[resolution];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapSource = useDataSource(mapSourceMap, defaultMapSources);
|
|
||||||
const [mapSourceImage, mapSourceImageStatus] = useImage(mapSource);
|
|
||||||
|
|
||||||
// Create a map source that only updates when the image is fully loaded
|
|
||||||
const [loadedMapSourceImage, setLoadedMapSourceImage] = useState();
|
|
||||||
useEffect(() => {
|
|
||||||
if (mapSourceImageStatus === "loaded") {
|
|
||||||
setLoadedMapSourceImage(mapSourceImage);
|
|
||||||
}
|
|
||||||
}, [mapSourceImage, mapSourceImageStatus]);
|
|
||||||
|
|
||||||
// Map loaded taking in to account different resolutions
|
// Map loaded taking in to account different resolutions
|
||||||
const [mapLoaded, setMapLoaded] = useState(false);
|
const [mapLoaded, setMapLoaded] = useState(false);
|
||||||
@ -64,10 +33,10 @@ function MapInteraction({
|
|||||||
if (map === null) {
|
if (map === null) {
|
||||||
setMapLoaded(false);
|
setMapLoaded(false);
|
||||||
}
|
}
|
||||||
if (mapSourceImageStatus === "loaded") {
|
if (mapImageSourceStatus === "loaded") {
|
||||||
setMapLoaded(true);
|
setMapLoaded(true);
|
||||||
}
|
}
|
||||||
}, [mapSourceImageStatus, map]);
|
}, [mapImageSourceStatus, map]);
|
||||||
|
|
||||||
const [stageWidth, setStageWidth] = useState(1);
|
const [stageWidth, setStageWidth] = useState(1);
|
||||||
const [stageHeight, setStageHeight] = useState(1);
|
const [stageHeight, setStageHeight] = useState(1);
|
||||||
@ -100,107 +69,6 @@ function MapInteraction({
|
|||||||
previousMapIdRef.current = map && map.id;
|
previousMapIdRef.current = map && map.id;
|
||||||
}, [map]);
|
}, [map]);
|
||||||
|
|
||||||
const pinchPreviousDistanceRef = useRef();
|
|
||||||
const pinchPreviousOriginRef = useRef();
|
|
||||||
const isInteractingWithCanvas = useRef(false);
|
|
||||||
const previousSelectedToolRef = useRef(selectedToolId);
|
|
||||||
|
|
||||||
const [interactionEmitter] = useState(new EventEmitter());
|
|
||||||
|
|
||||||
const bind = useGesture({
|
|
||||||
onWheelStart: ({ event }) => {
|
|
||||||
isInteractingWithCanvas.current =
|
|
||||||
event.target === mapLayerRef.current.getCanvas()._canvas;
|
|
||||||
},
|
|
||||||
onWheel: ({ event }) => {
|
|
||||||
event.persist();
|
|
||||||
const { pixelY } = normalizeWheel(event);
|
|
||||||
if (preventMapInteraction || !isInteractingWithCanvas.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newScale = Math.min(
|
|
||||||
Math.max(stageScale + pixelY * wheelZoomSpeed, minZoom),
|
|
||||||
maxZoom
|
|
||||||
);
|
|
||||||
setStageScale(newScale);
|
|
||||||
},
|
|
||||||
onPinchStart: () => {
|
|
||||||
// Change to pan tool when pinching and zooming
|
|
||||||
previousSelectedToolRef.current = selectedToolId;
|
|
||||||
onSelectedToolChange("pan");
|
|
||||||
},
|
|
||||||
onPinch: ({ da, origin, first }) => {
|
|
||||||
const [distance] = da;
|
|
||||||
const [originX, originY] = origin;
|
|
||||||
if (first) {
|
|
||||||
pinchPreviousDistanceRef.current = distance;
|
|
||||||
pinchPreviousOriginRef.current = { x: originX, y: originY };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply scale
|
|
||||||
const distanceDelta = distance - pinchPreviousDistanceRef.current;
|
|
||||||
const originXDelta = originX - pinchPreviousOriginRef.current.x;
|
|
||||||
const originYDelta = originY - pinchPreviousOriginRef.current.y;
|
|
||||||
const newScale = Math.min(
|
|
||||||
Math.max(stageScale + distanceDelta * touchZoomSpeed, minZoom),
|
|
||||||
maxZoom
|
|
||||||
);
|
|
||||||
setStageScale(newScale);
|
|
||||||
|
|
||||||
// Apply translate
|
|
||||||
const stageTranslate = stageTranslateRef.current;
|
|
||||||
const layer = mapLayerRef.current;
|
|
||||||
const newTranslate = {
|
|
||||||
x: stageTranslate.x + originXDelta / newScale,
|
|
||||||
y: stageTranslate.y + originYDelta / newScale,
|
|
||||||
};
|
|
||||||
layer.x(newTranslate.x);
|
|
||||||
layer.y(newTranslate.y);
|
|
||||||
layer.draw();
|
|
||||||
stageTranslateRef.current = newTranslate;
|
|
||||||
|
|
||||||
pinchPreviousDistanceRef.current = distance;
|
|
||||||
pinchPreviousOriginRef.current = { x: originX, y: originY };
|
|
||||||
},
|
|
||||||
onPinchEnd: () => {
|
|
||||||
onSelectedToolChange(previousSelectedToolRef.current);
|
|
||||||
},
|
|
||||||
onDragStart: ({ event }) => {
|
|
||||||
isInteractingWithCanvas.current =
|
|
||||||
event.target === mapLayerRef.current.getCanvas()._canvas;
|
|
||||||
},
|
|
||||||
onDrag: ({ delta, first, last, pinching }) => {
|
|
||||||
if (
|
|
||||||
preventMapInteraction ||
|
|
||||||
pinching ||
|
|
||||||
!isInteractingWithCanvas.current
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [dx, dy] = delta;
|
|
||||||
const stageTranslate = stageTranslateRef.current;
|
|
||||||
const layer = mapLayerRef.current;
|
|
||||||
if (selectedToolId === "pan") {
|
|
||||||
const newTranslate = {
|
|
||||||
x: stageTranslate.x + dx / stageScale,
|
|
||||||
y: stageTranslate.y + dy / stageScale,
|
|
||||||
};
|
|
||||||
layer.x(newTranslate.x);
|
|
||||||
layer.y(newTranslate.y);
|
|
||||||
layer.draw();
|
|
||||||
stageTranslateRef.current = newTranslate;
|
|
||||||
}
|
|
||||||
if (first) {
|
|
||||||
interactionEmitter.emit("dragStart");
|
|
||||||
} else if (last) {
|
|
||||||
interactionEmitter.emit("dragEnd");
|
|
||||||
} else {
|
|
||||||
interactionEmitter.emit("drag");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleResize(width, height) {
|
function handleResize(width, height) {
|
||||||
setStageWidth(width);
|
setStageWidth(width);
|
||||||
setStageHeight(height);
|
setStageHeight(height);
|
||||||
@ -208,6 +76,41 @@ function MapInteraction({
|
|||||||
stageHeightRef.current = height;
|
stageHeightRef.current = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mapStageRef = useContext(MapStageContext);
|
||||||
|
const mapLayerRef = useRef();
|
||||||
|
const mapImageRef = useRef();
|
||||||
|
|
||||||
|
const previousSelectedToolRef = useRef(selectedToolId);
|
||||||
|
|
||||||
|
const [interactionEmitter] = useState(new EventEmitter());
|
||||||
|
|
||||||
|
const bind = useStageInteraction(
|
||||||
|
mapLayerRef.current,
|
||||||
|
stageScale,
|
||||||
|
setStageScale,
|
||||||
|
stageTranslateRef,
|
||||||
|
preventMapInteraction,
|
||||||
|
{
|
||||||
|
onPinchStart: () => {
|
||||||
|
// Change to pan tool when pinching and zooming
|
||||||
|
previousSelectedToolRef.current = selectedToolId;
|
||||||
|
onSelectedToolChange("pan");
|
||||||
|
},
|
||||||
|
onPinchEnd: () => {
|
||||||
|
onSelectedToolChange(previousSelectedToolRef.current);
|
||||||
|
},
|
||||||
|
onDrag: ({ first, last }) => {
|
||||||
|
if (first) {
|
||||||
|
interactionEmitter.emit("dragStart");
|
||||||
|
} else if (last) {
|
||||||
|
interactionEmitter.emit("dragEnd");
|
||||||
|
} else {
|
||||||
|
interactionEmitter.emit("drag");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function handleKeyDown(event) {
|
function handleKeyDown(event) {
|
||||||
// Change to pan tool when pressing space
|
// Change to pan tool when pressing space
|
||||||
if (event.key === " " && selectedToolId === "pan") {
|
if (event.key === " " && selectedToolId === "pan") {
|
||||||
@ -272,10 +175,6 @@ function MapInteraction({
|
|||||||
const mapWidth = stageWidth;
|
const mapWidth = stageWidth;
|
||||||
const mapHeight = map ? stageWidth * (map.height / map.width) : stageHeight;
|
const mapHeight = map ? stageWidth * (map.height / map.width) : stageHeight;
|
||||||
|
|
||||||
const mapStageRef = useContext(MapStageContext);
|
|
||||||
const mapLayerRef = useRef();
|
|
||||||
const mapImageRef = useRef();
|
|
||||||
|
|
||||||
const auth = useContext(AuthContext);
|
const auth = useContext(AuthContext);
|
||||||
const settings = useContext(SettingsContext);
|
const settings = useContext(SettingsContext);
|
||||||
|
|
||||||
@ -314,7 +213,7 @@ function MapInteraction({
|
|||||||
>
|
>
|
||||||
<Layer ref={mapLayerRef}>
|
<Layer ref={mapLayerRef}>
|
||||||
<Image
|
<Image
|
||||||
image={mapLoaded && loadedMapSourceImage}
|
image={mapLoaded && mapImageSource}
|
||||||
width={mapWidth}
|
width={mapWidth}
|
||||||
height={mapHeight}
|
height={mapHeight}
|
||||||
id="mapImage"
|
id="mapImage"
|
||||||
|
58
src/helpers/useMapImage.js
Normal file
58
src/helpers/useMapImage.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import useImage from "use-image";
|
||||||
|
|
||||||
|
import useDataSource from "./useDataSource";
|
||||||
|
|
||||||
|
import { mapSources as defaultMapSources } from "../maps";
|
||||||
|
|
||||||
|
function useMapImage(map) {
|
||||||
|
const [mapSourceMap, setMapSourceMap] = useState({});
|
||||||
|
// Update source map data when either the map or map quality changes
|
||||||
|
useEffect(() => {
|
||||||
|
function updateMapSource() {
|
||||||
|
if (map && map.type === "file" && map.resolutions) {
|
||||||
|
// If quality is set and the quality is available
|
||||||
|
if (map.quality !== "original" && map.resolutions[map.quality]) {
|
||||||
|
setMapSourceMap({
|
||||||
|
...map.resolutions[map.quality],
|
||||||
|
id: map.id,
|
||||||
|
quality: map.quality,
|
||||||
|
});
|
||||||
|
} else if (!map.file) {
|
||||||
|
// If no file fallback to the highest resolution
|
||||||
|
const resolutionArray = Object.keys(map.resolutions);
|
||||||
|
setMapSourceMap({
|
||||||
|
...map.resolutions[resolutionArray[resolutionArray.length - 1]],
|
||||||
|
id: map.id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setMapSourceMap(map);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setMapSourceMap(map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (map && map.id !== mapSourceMap.id) {
|
||||||
|
updateMapSource();
|
||||||
|
} else if (map && map.type === "file") {
|
||||||
|
if (map.file && map.quality !== mapSourceMap.quality) {
|
||||||
|
updateMapSource();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [map, mapSourceMap]);
|
||||||
|
|
||||||
|
const mapSource = useDataSource(mapSourceMap, defaultMapSources);
|
||||||
|
const [mapSourceImage, mapSourceImageStatus] = useImage(mapSource);
|
||||||
|
|
||||||
|
// Create a map source that only updates when the image is fully loaded
|
||||||
|
const [loadedMapSourceImage, setLoadedMapSourceImage] = useState();
|
||||||
|
useEffect(() => {
|
||||||
|
if (mapSourceImageStatus === "loaded") {
|
||||||
|
setLoadedMapSourceImage(mapSourceImage);
|
||||||
|
}
|
||||||
|
}, [mapSourceImage, mapSourceImageStatus]);
|
||||||
|
|
||||||
|
return [loadedMapSourceImage, mapSourceImageStatus];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useMapImage;
|
107
src/helpers/useStageInteraction.js
Normal file
107
src/helpers/useStageInteraction.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import { useGesture } from "react-use-gesture";
|
||||||
|
import normalizeWheel from "normalize-wheel";
|
||||||
|
|
||||||
|
const wheelZoomSpeed = -0.001;
|
||||||
|
const touchZoomSpeed = 0.005;
|
||||||
|
const minZoom = 0.1;
|
||||||
|
const maxZoom = 5;
|
||||||
|
|
||||||
|
function useStageInteraction(
|
||||||
|
layer,
|
||||||
|
stageScale,
|
||||||
|
onStageScaleChange,
|
||||||
|
stageTranslateRef,
|
||||||
|
preventInteraction = false,
|
||||||
|
gesture = {}
|
||||||
|
) {
|
||||||
|
const isInteractingWithCanvas = useRef(false);
|
||||||
|
const pinchPreviousDistanceRef = useRef();
|
||||||
|
const pinchPreviousOriginRef = useRef();
|
||||||
|
|
||||||
|
const bind = useGesture({
|
||||||
|
...gesture,
|
||||||
|
onWheelStart: (props) => {
|
||||||
|
const { event } = props;
|
||||||
|
isInteractingWithCanvas.current =
|
||||||
|
event.target === layer.getCanvas()._canvas;
|
||||||
|
gesture.onWheelStart && gesture.onWheelStart(props);
|
||||||
|
},
|
||||||
|
onWheel: (props) => {
|
||||||
|
const { event } = props;
|
||||||
|
event.persist();
|
||||||
|
const { pixelY } = normalizeWheel(event);
|
||||||
|
if (preventInteraction || !isInteractingWithCanvas.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newScale = Math.min(
|
||||||
|
Math.max(stageScale + pixelY * wheelZoomSpeed, minZoom),
|
||||||
|
maxZoom
|
||||||
|
);
|
||||||
|
onStageScaleChange(newScale);
|
||||||
|
gesture.onWheel && gesture.onWheel(props);
|
||||||
|
},
|
||||||
|
onPinch: (props) => {
|
||||||
|
const { da, origin, first } = props;
|
||||||
|
const [distance] = da;
|
||||||
|
const [originX, originY] = origin;
|
||||||
|
if (first) {
|
||||||
|
pinchPreviousDistanceRef.current = distance;
|
||||||
|
pinchPreviousOriginRef.current = { x: originX, y: originY };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply scale
|
||||||
|
const distanceDelta = distance - pinchPreviousDistanceRef.current;
|
||||||
|
const originXDelta = originX - pinchPreviousOriginRef.current.x;
|
||||||
|
const originYDelta = originY - pinchPreviousOriginRef.current.y;
|
||||||
|
const newScale = Math.min(
|
||||||
|
Math.max(stageScale + distanceDelta * touchZoomSpeed, minZoom),
|
||||||
|
maxZoom
|
||||||
|
);
|
||||||
|
onStageScaleChange(newScale);
|
||||||
|
|
||||||
|
// Apply translate
|
||||||
|
const stageTranslate = stageTranslateRef.current;
|
||||||
|
const newTranslate = {
|
||||||
|
x: stageTranslate.x + originXDelta / newScale,
|
||||||
|
y: stageTranslate.y + originYDelta / newScale,
|
||||||
|
};
|
||||||
|
layer.x(newTranslate.x);
|
||||||
|
layer.y(newTranslate.y);
|
||||||
|
layer.draw();
|
||||||
|
stageTranslateRef.current = newTranslate;
|
||||||
|
|
||||||
|
pinchPreviousDistanceRef.current = distance;
|
||||||
|
pinchPreviousOriginRef.current = { x: originX, y: originY };
|
||||||
|
gesture.onPinch && gesture.onPinch(props);
|
||||||
|
},
|
||||||
|
onDragStart: (props) => {
|
||||||
|
const { event } = props;
|
||||||
|
isInteractingWithCanvas.current =
|
||||||
|
event.target === layer.getCanvas()._canvas;
|
||||||
|
gesture.onDragStart && gesture.onDragStart(props);
|
||||||
|
},
|
||||||
|
onDrag: (props) => {
|
||||||
|
const { delta, pinching } = props;
|
||||||
|
if (preventInteraction || pinching || !isInteractingWithCanvas.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [dx, dy] = delta;
|
||||||
|
const stageTranslate = stageTranslateRef.current;
|
||||||
|
const newTranslate = {
|
||||||
|
x: stageTranslate.x + dx / stageScale,
|
||||||
|
y: stageTranslate.y + dy / stageScale,
|
||||||
|
};
|
||||||
|
layer.x(newTranslate.x);
|
||||||
|
layer.y(newTranslate.y);
|
||||||
|
layer.draw();
|
||||||
|
stageTranslateRef.current = newTranslate;
|
||||||
|
gesture.onDrag && gesture.onDrag(props);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return bind;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useStageInteraction;
|
Loading…
Reference in New Issue
Block a user