Moved map and map tokens to Konva
This commit is contained in:
+49
-191
@@ -1,24 +1,12 @@
|
||||
import React, { useRef, useEffect, useState, useContext } from "react";
|
||||
import { Box, Image } from "theme-ui";
|
||||
import React, { useState, useContext } from "react";
|
||||
|
||||
import ProxyToken from "../token/ProxyToken";
|
||||
import TokenMenu from "../token/TokenMenu";
|
||||
import MapToken from "./MapToken";
|
||||
import MapDrawing from "./MapDrawing";
|
||||
import MapFog from "./MapFog";
|
||||
import MapControls from "./MapControls";
|
||||
|
||||
import { omit } from "../../helpers/shared";
|
||||
import useDataSource from "../../helpers/useDataSource";
|
||||
import MapInteraction from "./MapInteraction";
|
||||
import MapToken from "./MapToken";
|
||||
|
||||
import AuthContext from "../../contexts/AuthContext";
|
||||
import TokenDataContext from "../../contexts/TokenDataContext";
|
||||
|
||||
import { mapSources as defaultMapSources } from "../../maps";
|
||||
|
||||
const mapTokenProxyClassName = "map-token__proxy";
|
||||
const mapTokenMenuClassName = "map-token__menu";
|
||||
import TokenMenu from "../token/TokenMenu";
|
||||
|
||||
function Map({
|
||||
map,
|
||||
@@ -38,24 +26,12 @@ function Map({
|
||||
disabledTokens,
|
||||
loading,
|
||||
}) {
|
||||
const { userId } = useContext(AuthContext);
|
||||
const { tokens } = useContext(TokenDataContext);
|
||||
|
||||
const mapSource = useDataSource(map, defaultMapSources);
|
||||
|
||||
function handleProxyDragEnd(isOnMap, tokenState) {
|
||||
if (isOnMap && onMapTokenStateChange) {
|
||||
onMapTokenStateChange({ ...tokenState, lastEditedBy: userId });
|
||||
}
|
||||
|
||||
if (!isOnMap && onMapTokenStateRemove) {
|
||||
onMapTokenStateRemove({ ...tokenState, lastEditedBy: userId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map drawing
|
||||
*/
|
||||
const gridX = map && map.gridX;
|
||||
const gridY = map && map.gridY;
|
||||
const gridSizeNormalized = { x: 1 / gridX || 0, y: 1 / gridY || 0 };
|
||||
const tokenSizePercent = gridSizeNormalized.x;
|
||||
|
||||
const [selectedToolId, setSelectedToolId] = useState("pan");
|
||||
const [toolSettings, setToolSettings] = useState({
|
||||
@@ -105,55 +81,7 @@ function Map({
|
||||
}
|
||||
|
||||
const [mapShapes, setMapShapes] = useState([]);
|
||||
function handleMapShapeAdd(shape) {
|
||||
onMapDraw({ type: "add", shapes: [shape] });
|
||||
}
|
||||
|
||||
function handleMapShapeRemove(shapeId) {
|
||||
onMapDraw({ type: "remove", shapeIds: [shapeId] });
|
||||
}
|
||||
|
||||
const [fogShapes, setFogShapes] = useState([]);
|
||||
function handleFogShapeAdd(shape) {
|
||||
onFogDraw({ type: "add", shapes: [shape] });
|
||||
}
|
||||
|
||||
function handleFogShapeRemove(shapeId) {
|
||||
onFogDraw({ type: "remove", shapeIds: [shapeId] });
|
||||
}
|
||||
|
||||
function handleFogShapeEdit(shape) {
|
||||
onFogDraw({ type: "edit", shapes: [shape] });
|
||||
}
|
||||
|
||||
// Replay the draw actions and convert them to shapes for the map drawing
|
||||
useEffect(() => {
|
||||
if (!mapState) {
|
||||
return;
|
||||
}
|
||||
function actionsToShapes(actions, actionIndex) {
|
||||
let shapesById = {};
|
||||
for (let i = 0; i <= actionIndex; i++) {
|
||||
const action = actions[i];
|
||||
if (action.type === "add" || action.type === "edit") {
|
||||
for (let shape of action.shapes) {
|
||||
shapesById[shape.id] = shape;
|
||||
}
|
||||
}
|
||||
if (action.type === "remove") {
|
||||
shapesById = omit(shapesById, action.shapeIds);
|
||||
}
|
||||
}
|
||||
return Object.values(shapesById);
|
||||
}
|
||||
|
||||
setMapShapes(
|
||||
actionsToShapes(mapState.mapDrawActions, mapState.mapDrawActionIndex)
|
||||
);
|
||||
setFogShapes(
|
||||
actionsToShapes(mapState.fogDrawActions, mapState.fogDrawActionIndex)
|
||||
);
|
||||
}, [mapState]);
|
||||
|
||||
const disabledControls = [];
|
||||
if (!allowMapDrawing) {
|
||||
@@ -195,92 +123,6 @@ function Map({
|
||||
disabledSettings.fog.push("redo");
|
||||
}
|
||||
|
||||
/**
|
||||
* Member setup
|
||||
*/
|
||||
|
||||
const mapRef = useRef(null);
|
||||
|
||||
const gridX = map && map.gridX;
|
||||
const gridY = map && map.gridY;
|
||||
const gridSizeNormalized = { x: 1 / gridX || 0, y: 1 / gridY || 0 };
|
||||
const tokenSizePercent = gridSizeNormalized.x * 100;
|
||||
const aspectRatio = (map && map.width / map.height) || 1;
|
||||
|
||||
const mapImage = (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
ref={mapRef}
|
||||
className="mapImage"
|
||||
sx={{
|
||||
width: "100%",
|
||||
userSelect: "none",
|
||||
touchAction: "none",
|
||||
}}
|
||||
src={mapSource}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const mapTokens = (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
{mapState &&
|
||||
Object.values(mapState.tokens).map((tokenState) => (
|
||||
<MapToken
|
||||
key={tokenState.id}
|
||||
token={tokens.find((token) => token.id === tokenState.tokenId)}
|
||||
tokenState={tokenState}
|
||||
tokenSizePercent={tokenSizePercent}
|
||||
className={`${mapTokenProxyClassName} ${mapTokenMenuClassName}`}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const mapDrawing = (
|
||||
<MapDrawing
|
||||
width={map ? map.width : 0}
|
||||
height={map ? map.height : 0}
|
||||
selectedTool={selectedToolId !== "fog" ? selectedToolId : "none"}
|
||||
toolSettings={toolSettings[selectedToolId]}
|
||||
shapes={mapShapes}
|
||||
onShapeAdd={handleMapShapeAdd}
|
||||
onShapeRemove={handleMapShapeRemove}
|
||||
gridSize={gridSizeNormalized}
|
||||
/>
|
||||
);
|
||||
|
||||
const mapFog = (
|
||||
<MapFog
|
||||
width={map ? map.width : 0}
|
||||
height={map ? map.height : 0}
|
||||
isEditing={selectedToolId === "fog"}
|
||||
toolSettings={toolSettings["fog"]}
|
||||
shapes={fogShapes}
|
||||
onShapeAdd={handleFogShapeAdd}
|
||||
onShapeRemove={handleFogShapeRemove}
|
||||
onShapeEdit={handleFogShapeEdit}
|
||||
gridSize={gridSizeNormalized}
|
||||
/>
|
||||
);
|
||||
|
||||
const mapControls = (
|
||||
<MapControls
|
||||
onMapChange={onMapChange}
|
||||
@@ -296,33 +138,49 @@ function Map({
|
||||
disabledSettings={disabledSettings}
|
||||
/>
|
||||
);
|
||||
|
||||
const [isTokenMenuOpen, setIsTokenMenuOpen] = useState(false);
|
||||
const [tokenMenuOptions, setTokenMenuOptions] = useState({});
|
||||
function handleTokenMenuOpen(tokenStateId, tokenImage) {
|
||||
setTokenMenuOptions({ tokenStateId, tokenImage });
|
||||
setIsTokenMenuOpen(true);
|
||||
}
|
||||
|
||||
const mapTokens =
|
||||
mapState &&
|
||||
Object.values(mapState.tokens).map((tokenState) => (
|
||||
<MapToken
|
||||
key={tokenState.id}
|
||||
token={tokens.find((token) => token.id === tokenState.tokenId)}
|
||||
tokenState={tokenState}
|
||||
tokenSizePercent={tokenSizePercent}
|
||||
onTokenStateChange={onMapTokenStateChange}
|
||||
onTokenMenuOpen={handleTokenMenuOpen}
|
||||
/>
|
||||
));
|
||||
|
||||
const tokenMenu = isTokenMenuOpen && (
|
||||
<TokenMenu
|
||||
isOpen={isTokenMenuOpen}
|
||||
onRequestClose={() => setIsTokenMenuOpen(false)}
|
||||
onTokenChange={onMapTokenStateChange}
|
||||
tokenState={mapState.tokens[tokenMenuOptions.tokenStateId]}
|
||||
tokenImage={tokenMenuOptions.tokenImage}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MapInteraction
|
||||
map={map}
|
||||
aspectRatio={aspectRatio}
|
||||
isEnabled={selectedToolId === "pan"}
|
||||
controls={mapControls}
|
||||
loading={loading}
|
||||
>
|
||||
{map && mapImage}
|
||||
{map && mapDrawing}
|
||||
{map && mapFog}
|
||||
{map && mapTokens}
|
||||
</MapInteraction>
|
||||
<ProxyToken
|
||||
tokenClassName={mapTokenProxyClassName}
|
||||
onProxyDragEnd={handleProxyDragEnd}
|
||||
tokens={mapState && mapState.tokens}
|
||||
disabledTokens={disabledTokens}
|
||||
/>
|
||||
<TokenMenu
|
||||
tokenClassName={mapTokenMenuClassName}
|
||||
onTokenChange={onMapTokenStateChange}
|
||||
tokens={mapState && mapState.tokens}
|
||||
disabledTokens={disabledTokens}
|
||||
/>
|
||||
</>
|
||||
<MapInteraction
|
||||
map={map}
|
||||
controls={
|
||||
<>
|
||||
{mapControls}
|
||||
{tokenMenu}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{mapTokens}
|
||||
</MapInteraction>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,171 +1,129 @@
|
||||
import React, { useRef, useEffect } from "react";
|
||||
import React, { useRef, useEffect, useState, useContext } from "react";
|
||||
import { Box } from "theme-ui";
|
||||
import interact from "interactjs";
|
||||
import normalizeWheel from "normalize-wheel";
|
||||
import { useGesture } from "react-use-gesture";
|
||||
import ReactResizeDetector from "react-resize-detector";
|
||||
import useImage from "use-image";
|
||||
import { Stage, Layer, Image } from "react-konva";
|
||||
|
||||
import usePreventOverscroll from "../../helpers/usePreventOverscroll";
|
||||
import useDataSource from "../../helpers/useDataSource";
|
||||
|
||||
import { mapSources as defaultMapSources } from "../../maps";
|
||||
|
||||
import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
|
||||
|
||||
import LoadingOverlay from "../LoadingOverlay";
|
||||
import MapStageContext from "../../contexts/MapStageContext";
|
||||
import AuthContext from "../../contexts/AuthContext";
|
||||
|
||||
const zoomSpeed = -0.001;
|
||||
const minZoom = 0.1;
|
||||
const maxZoom = 5;
|
||||
|
||||
function MapInteraction({
|
||||
map,
|
||||
aspectRatio,
|
||||
isEnabled,
|
||||
children,
|
||||
controls,
|
||||
loading,
|
||||
}) {
|
||||
const mapContainerRef = useRef();
|
||||
const mapMoveContainerRef = useRef();
|
||||
const mapScaleContainerRef = useRef();
|
||||
const mapTranslateRef = useRef({ x: 0, y: 0 });
|
||||
const mapScaleRef = useRef(1);
|
||||
function setTranslateAndScale(newTranslate, newScale) {
|
||||
const moveContainer = mapMoveContainerRef.current;
|
||||
const scaleContainer = mapScaleContainerRef.current;
|
||||
moveContainer.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)`;
|
||||
scaleContainer.style.transform = ` scale(${newScale})`;
|
||||
mapScaleRef.current = newScale;
|
||||
mapTranslateRef.current = newTranslate;
|
||||
}
|
||||
function MapInteraction({ map, children, controls }) {
|
||||
const mapSource = useDataSource(map, defaultMapSources);
|
||||
const [mapSourceImage] = useImage(mapSource);
|
||||
|
||||
const [stageWidth, setStageWidth] = useState(1);
|
||||
const [stageHeight, setStageHeight] = useState(1);
|
||||
const [stageScale, setStageScale] = useState(1);
|
||||
const [stageTranslate, setStageTranslate] = useState({ x: 0, y: 0 });
|
||||
const [preventMapInteraction, setPreventMapInteraction] = useState(false);
|
||||
|
||||
const stageWidthRef = useRef(stageWidth);
|
||||
const stageHeightRef = useRef(stageHeight);
|
||||
const stageScaleRef = useRef(stageScale);
|
||||
const stageTranslateRef = useRef(stageTranslate);
|
||||
|
||||
useEffect(() => {
|
||||
function handleMove(event, isGesture) {
|
||||
const scale = mapScaleRef.current;
|
||||
const translate = mapTranslateRef.current;
|
||||
|
||||
let newScale = scale;
|
||||
let newTranslate = translate;
|
||||
|
||||
if (isGesture) {
|
||||
newScale = Math.max(Math.min(scale + event.ds, maxZoom), minZoom);
|
||||
}
|
||||
|
||||
if (isEnabled || isGesture) {
|
||||
newTranslate = {
|
||||
x: translate.x + event.dx / newScale,
|
||||
y: translate.y + event.dy / newScale,
|
||||
};
|
||||
}
|
||||
setTranslateAndScale(newTranslate, newScale);
|
||||
if (map) {
|
||||
const mapHeight = stageWidthRef.current * (map.height / map.width);
|
||||
setStageTranslate({ x: 0, y: -(mapHeight - stageHeightRef.current) / 2 });
|
||||
}
|
||||
const mapInteract = interact(".map")
|
||||
.gesturable({
|
||||
listeners: {
|
||||
move: (e) => handleMove(e, true),
|
||||
},
|
||||
})
|
||||
.draggable({
|
||||
inertia: true,
|
||||
listeners: {
|
||||
move: (e) => handleMove(e, false),
|
||||
},
|
||||
cursorChecker: () => {
|
||||
return isEnabled && map ? "move" : "default";
|
||||
},
|
||||
})
|
||||
.on("doubletap", (event) => {
|
||||
event.preventDefault();
|
||||
if (isEnabled) {
|
||||
setTranslateAndScale({ x: 0, y: 0 }, 1);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
mapInteract.unset();
|
||||
};
|
||||
}, [isEnabled, map]);
|
||||
|
||||
// Reset map transform when map changes
|
||||
useEffect(() => {
|
||||
setTranslateAndScale({ x: 0, y: 0 }, 1);
|
||||
}, [map]);
|
||||
|
||||
// Bind the wheel event of the map via a ref
|
||||
// in order to support non-passive event listening
|
||||
// to allow the track pad zoom to be interrupted
|
||||
// see https://github.com/facebook/react/issues/14856
|
||||
useEffect(() => {
|
||||
const mapContainer = mapContainerRef.current;
|
||||
|
||||
function handleZoom(event) {
|
||||
// Stop overscroll on chrome and safari
|
||||
// also stop pinch to zoom on chrome
|
||||
event.preventDefault();
|
||||
|
||||
// Try and normalize the wheel event to prevent OS differences for zoom speed
|
||||
const normalized = normalizeWheel(event);
|
||||
|
||||
const scale = mapScaleRef.current;
|
||||
const translate = mapTranslateRef.current;
|
||||
|
||||
const deltaY = normalized.pixelY * zoomSpeed;
|
||||
const newScale = Math.max(Math.min(scale + deltaY, maxZoom), minZoom);
|
||||
|
||||
setTranslateAndScale(translate, newScale);
|
||||
}
|
||||
|
||||
if (mapContainer) {
|
||||
mapContainer.addEventListener("wheel", handleZoom, {
|
||||
passive: false,
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (mapContainer) {
|
||||
mapContainer.removeEventListener("wheel", handleZoom);
|
||||
const bind = useGesture({
|
||||
onWheel: ({ delta }) => {
|
||||
const newScale = Math.min(
|
||||
Math.max(stageScale - delta[1] * zoomSpeed, minZoom),
|
||||
maxZoom
|
||||
);
|
||||
setStageScale(newScale);
|
||||
stageScaleRef.current = newScale;
|
||||
},
|
||||
onDrag: ({ delta }) => {
|
||||
if (!preventMapInteraction) {
|
||||
const newTranslate = {
|
||||
x: stageTranslate.x + delta[0] / stageScale,
|
||||
y: stageTranslate.y + delta[1] / stageScale,
|
||||
};
|
||||
setStageTranslate(newTranslate);
|
||||
stageTranslateRef.current = newTranslate;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
},
|
||||
});
|
||||
|
||||
function handleResize(width, height) {
|
||||
setStageWidth(width);
|
||||
setStageHeight(height);
|
||||
stageWidthRef.current = width;
|
||||
stageHeightRef.current = height;
|
||||
}
|
||||
|
||||
const containerRef = useRef();
|
||||
usePreventOverscroll(containerRef);
|
||||
|
||||
const mapWidth = stageWidth;
|
||||
const mapHeight = map ? stageWidth * (map.height / map.width) : stageHeight;
|
||||
|
||||
const mapStageRef = useContext(MapStageContext);
|
||||
|
||||
const auth = useContext(AuthContext);
|
||||
|
||||
const mapInteraction = {
|
||||
stageTranslate,
|
||||
stageScale,
|
||||
stageWidth,
|
||||
stageHeight,
|
||||
setPreventMapInteraction,
|
||||
mapWidth,
|
||||
mapHeight,
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{ flexGrow: 1, position: "relative" }}
|
||||
ref={containerRef}
|
||||
{...bind()}
|
||||
className="map"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.1)",
|
||||
userSelect: "none",
|
||||
touchAction: "none",
|
||||
}}
|
||||
bg="background"
|
||||
ref={mapContainerRef}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
>
|
||||
<Box ref={mapScaleContainerRef}>
|
||||
<Box ref={mapMoveContainerRef}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 0,
|
||||
paddingBottom: `${(1 / aspectRatio) * 100}%`,
|
||||
}}
|
||||
<ReactResizeDetector handleWidth handleHeight onResize={handleResize}>
|
||||
<Stage
|
||||
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 x={stageTranslate.x} y={stageTranslate.y}>
|
||||
<Image
|
||||
image={mapSourceImage}
|
||||
width={mapWidth}
|
||||
height={mapHeight}
|
||||
id="mapImage"
|
||||
/>
|
||||
<MapInteractionProvider
|
||||
value={{
|
||||
translateRef: mapTranslateRef,
|
||||
scaleRef: mapScaleRef,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MapInteractionProvider>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
{controls}
|
||||
{loading && <LoadingOverlay />}
|
||||
{/* Forward auth context to konva elements */}
|
||||
<AuthContext.Provider value={auth}>
|
||||
<MapInteractionProvider value={mapInteraction}>
|
||||
{children}
|
||||
</MapInteractionProvider>
|
||||
</AuthContext.Provider>
|
||||
</Layer>
|
||||
</Stage>
|
||||
</ReactResizeDetector>
|
||||
<MapInteractionProvider value={mapInteraction}>
|
||||
{controls}
|
||||
</MapInteractionProvider>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState, useContext } from "react";
|
||||
import Modal from "react-modal";
|
||||
|
||||
import { useThemeUI } from "theme-ui";
|
||||
|
||||
import MapInteractionContext from "../../contexts/MapInteractionContext";
|
||||
|
||||
function MapMenu({
|
||||
isOpen,
|
||||
onRequestClose,
|
||||
@@ -21,6 +22,8 @@ function MapMenu({
|
||||
// callback
|
||||
const [modalContentNode, setModalContentNode] = useState(null);
|
||||
|
||||
const { setPreventMapInteraction } = useContext(MapInteractionContext);
|
||||
|
||||
useEffect(() => {
|
||||
// Close modal if interacting with any other element
|
||||
function handlePointerDown(event) {
|
||||
@@ -29,17 +32,20 @@ function MapMenu({
|
||||
!path.includes(modalContentNode) &&
|
||||
!(excludeNode && path.includes(excludeNode))
|
||||
) {
|
||||
setPreventMapInteraction(false);
|
||||
onRequestClose();
|
||||
document.body.removeEventListener("pointerdown", handlePointerDown);
|
||||
}
|
||||
}
|
||||
|
||||
if (modalContentNode) {
|
||||
setPreventMapInteraction(true);
|
||||
document.body.addEventListener("pointerdown", handlePointerDown);
|
||||
// Check for wheel event to close modal as well
|
||||
document.body.addEventListener(
|
||||
"wheel",
|
||||
() => {
|
||||
setPreventMapInteraction(false);
|
||||
onRequestClose();
|
||||
},
|
||||
{ once: true }
|
||||
@@ -50,7 +56,7 @@ function MapMenu({
|
||||
document.body.removeEventListener("pointerdown", handlePointerDown);
|
||||
}
|
||||
};
|
||||
}, [modalContentNode, excludeNode, onRequestClose]);
|
||||
}, [modalContentNode, excludeNode, onRequestClose, setPreventMapInteraction]);
|
||||
|
||||
function handleModalContent(node) {
|
||||
setModalContentNode(node);
|
||||
|
||||
@@ -1,76 +1,88 @@
|
||||
import React, { useRef, useContext } from "react";
|
||||
import { Box, Image } from "theme-ui";
|
||||
import React, { useContext, useState, useEffect, useRef } from "react";
|
||||
import { Image as KonvaImage } from "react-konva";
|
||||
import useImage from "use-image";
|
||||
|
||||
import TokenLabel from "../token/TokenLabel";
|
||||
import TokenStatus from "../token/TokenStatus";
|
||||
|
||||
import usePreventTouch from "../../helpers/usePreventTouch";
|
||||
import useDataSource from "../../helpers/useDataSource";
|
||||
import useDebounce from "../../helpers/useDebounce";
|
||||
|
||||
import AuthContext from "../../contexts/AuthContext";
|
||||
import MapInteractionContext from "../../contexts/MapInteractionContext";
|
||||
|
||||
import { tokenSources, unknownSource } from "../../tokens";
|
||||
|
||||
function MapToken({ token, tokenState, tokenSizePercent, className }) {
|
||||
function MapToken({
|
||||
token,
|
||||
tokenState,
|
||||
tokenSizePercent,
|
||||
onTokenStateChange,
|
||||
onTokenMenuOpen,
|
||||
}) {
|
||||
const { userId } = useContext(AuthContext);
|
||||
const imageSource = useDataSource(token, tokenSources, unknownSource);
|
||||
const {
|
||||
setPreventMapInteraction,
|
||||
mapWidth,
|
||||
mapHeight,
|
||||
stageScale,
|
||||
} = useContext(MapInteractionContext);
|
||||
|
||||
const tokenSource = useDataSource(token, tokenSources, unknownSource);
|
||||
const [tokenSourceImage, tokenSourceStatus] = useImage(tokenSource);
|
||||
const [tokenAspectRatio, setTokenAspectRatio] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
if (tokenSourceImage) {
|
||||
setTokenAspectRatio(tokenSourceImage.width / tokenSourceImage.height);
|
||||
}
|
||||
}, [tokenSourceImage]);
|
||||
|
||||
function handleDragEnd(event) {
|
||||
onTokenStateChange({
|
||||
...tokenState,
|
||||
x: event.target.x() / mapWidth,
|
||||
y: event.target.y() / mapHeight,
|
||||
lastEditedBy: userId,
|
||||
});
|
||||
}
|
||||
|
||||
function handleClick(event) {
|
||||
const tokenImage = event.target;
|
||||
onTokenMenuOpen(tokenState.id, tokenImage);
|
||||
}
|
||||
|
||||
const tokenWidth = tokenSizePercent * mapWidth * tokenState.size;
|
||||
const tokenHeight =
|
||||
tokenSizePercent * (mapWidth / tokenAspectRatio) * tokenState.size;
|
||||
|
||||
const debouncedStageScale = useDebounce(stageScale, 50);
|
||||
const imageRef = useRef();
|
||||
// Stop touch to prevent 3d touch gesutre on iOS
|
||||
usePreventTouch(imageRef);
|
||||
useEffect(() => {
|
||||
const image = imageRef.current;
|
||||
if (image) {
|
||||
image.cache({
|
||||
pixelRatio: debouncedStageScale,
|
||||
});
|
||||
image.drawHitFromCache();
|
||||
// Force redraw
|
||||
image.parent.draw();
|
||||
}
|
||||
}, [debouncedStageScale, tokenWidth, tokenHeight, tokenSourceStatus]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
transform: `translate(${tokenState.x * 100}%, ${tokenState.y * 100}%)`,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
transition:
|
||||
tokenState.lastEditedBy === userId
|
||||
? "initial"
|
||||
: "transform 0.5s ease",
|
||||
}}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
width: `${tokenSizePercent * (tokenState.size || 1)}%`,
|
||||
}}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
pointerEvents: "all",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
display: "flex", // Set display to flex to fix height being calculated wrong
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
className={className}
|
||||
sx={{
|
||||
userSelect: "none",
|
||||
touchAction: "none",
|
||||
width: "100%",
|
||||
// Fix image from being clipped when transitioning
|
||||
willChange: "transform",
|
||||
}}
|
||||
src={imageSource}
|
||||
// pass id into the dom element which is then used by the ProxyToken
|
||||
data-id={tokenState.id}
|
||||
ref={imageRef}
|
||||
/>
|
||||
{tokenState.statuses && <TokenStatus token={tokenState} />}
|
||||
{tokenState.label && <TokenLabel token={tokenState} />}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<KonvaImage
|
||||
ref={imageRef}
|
||||
width={tokenWidth}
|
||||
height={tokenHeight}
|
||||
x={tokenState.x * mapWidth}
|
||||
y={tokenState.y * mapHeight}
|
||||
image={tokenSourceImage}
|
||||
draggable
|
||||
onDragEnd={handleDragEnd}
|
||||
onClick={handleClick}
|
||||
onMouseDown={() => setPreventMapInteraction(true)}
|
||||
onMouseUp={() => setPreventMapInteraction(false)}
|
||||
onTouchStart={() => setPreventMapInteraction(true)}
|
||||
onTouchEnd={() => setPreventMapInteraction(false)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user