Moved map and map tokens to Konva
This commit is contained in:
parent
542388a67f
commit
5b70f69fb7
@ -11,21 +11,26 @@
|
|||||||
"dexie": "^2.0.4",
|
"dexie": "^2.0.4",
|
||||||
"fake-indexeddb": "^3.0.0",
|
"fake-indexeddb": "^3.0.0",
|
||||||
"interactjs": "^1.9.7",
|
"interactjs": "^1.9.7",
|
||||||
|
"konva": "^6.0.0",
|
||||||
"normalize-wheel": "^1.0.1",
|
"normalize-wheel": "^1.0.1",
|
||||||
"raw.macro": "^0.3.0",
|
"raw.macro": "^0.3.0",
|
||||||
"react": "^16.13.0",
|
"react": "^16.13.0",
|
||||||
"react-dom": "^16.13.0",
|
"react-dom": "^16.13.0",
|
||||||
|
"react-konva": "^16.13.0-3",
|
||||||
"react-markdown": "^4.3.1",
|
"react-markdown": "^4.3.1",
|
||||||
"react-modal": "^3.11.2",
|
"react-modal": "^3.11.2",
|
||||||
|
"react-resize-detector": "^4.2.3",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-router-hash-link": "^1.2.2",
|
"react-router-hash-link": "^1.2.2",
|
||||||
"react-scripts": "3.4.0",
|
"react-scripts": "3.4.0",
|
||||||
|
"react-use-gesture": "^7.0.15",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"simple-peer": "^9.6.2",
|
"simple-peer": "^9.6.2",
|
||||||
"simplebar-react": "^2.1.0",
|
"simplebar-react": "^2.1.0",
|
||||||
"simplify-js": "^1.2.4",
|
"simplify-js": "^1.2.4",
|
||||||
"socket.io-client": "^2.3.0",
|
"socket.io-client": "^2.3.0",
|
||||||
"theme-ui": "^0.3.1",
|
"theme-ui": "^0.3.1",
|
||||||
|
"use-image": "^1.0.5",
|
||||||
"webrtc-adapter": "^7.5.1"
|
"webrtc-adapter": "^7.5.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -1,24 +1,12 @@
|
|||||||
import React, { useRef, useEffect, useState, useContext } from "react";
|
import React, { useState, useContext } from "react";
|
||||||
import { Box, Image } from "theme-ui";
|
|
||||||
|
|
||||||
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 MapControls from "./MapControls";
|
||||||
|
|
||||||
import { omit } from "../../helpers/shared";
|
|
||||||
import useDataSource from "../../helpers/useDataSource";
|
|
||||||
import MapInteraction from "./MapInteraction";
|
import MapInteraction from "./MapInteraction";
|
||||||
|
import MapToken from "./MapToken";
|
||||||
|
|
||||||
import AuthContext from "../../contexts/AuthContext";
|
|
||||||
import TokenDataContext from "../../contexts/TokenDataContext";
|
import TokenDataContext from "../../contexts/TokenDataContext";
|
||||||
|
import TokenMenu from "../token/TokenMenu";
|
||||||
import { mapSources as defaultMapSources } from "../../maps";
|
|
||||||
|
|
||||||
const mapTokenProxyClassName = "map-token__proxy";
|
|
||||||
const mapTokenMenuClassName = "map-token__menu";
|
|
||||||
|
|
||||||
function Map({
|
function Map({
|
||||||
map,
|
map,
|
||||||
@ -38,24 +26,12 @@ function Map({
|
|||||||
disabledTokens,
|
disabledTokens,
|
||||||
loading,
|
loading,
|
||||||
}) {
|
}) {
|
||||||
const { userId } = useContext(AuthContext);
|
|
||||||
const { tokens } = useContext(TokenDataContext);
|
const { tokens } = useContext(TokenDataContext);
|
||||||
|
|
||||||
const mapSource = useDataSource(map, defaultMapSources);
|
const gridX = map && map.gridX;
|
||||||
|
const gridY = map && map.gridY;
|
||||||
function handleProxyDragEnd(isOnMap, tokenState) {
|
const gridSizeNormalized = { x: 1 / gridX || 0, y: 1 / gridY || 0 };
|
||||||
if (isOnMap && onMapTokenStateChange) {
|
const tokenSizePercent = gridSizeNormalized.x;
|
||||||
onMapTokenStateChange({ ...tokenState, lastEditedBy: userId });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isOnMap && onMapTokenStateRemove) {
|
|
||||||
onMapTokenStateRemove({ ...tokenState, lastEditedBy: userId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map drawing
|
|
||||||
*/
|
|
||||||
|
|
||||||
const [selectedToolId, setSelectedToolId] = useState("pan");
|
const [selectedToolId, setSelectedToolId] = useState("pan");
|
||||||
const [toolSettings, setToolSettings] = useState({
|
const [toolSettings, setToolSettings] = useState({
|
||||||
@ -105,55 +81,7 @@ function Map({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [mapShapes, setMapShapes] = useState([]);
|
const [mapShapes, setMapShapes] = useState([]);
|
||||||
function handleMapShapeAdd(shape) {
|
|
||||||
onMapDraw({ type: "add", shapes: [shape] });
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMapShapeRemove(shapeId) {
|
|
||||||
onMapDraw({ type: "remove", shapeIds: [shapeId] });
|
|
||||||
}
|
|
||||||
|
|
||||||
const [fogShapes, setFogShapes] = useState([]);
|
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 = [];
|
const disabledControls = [];
|
||||||
if (!allowMapDrawing) {
|
if (!allowMapDrawing) {
|
||||||
@ -195,92 +123,6 @@ function Map({
|
|||||||
disabledSettings.fog.push("redo");
|
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 = (
|
const mapControls = (
|
||||||
<MapControls
|
<MapControls
|
||||||
onMapChange={onMapChange}
|
onMapChange={onMapChange}
|
||||||
@ -296,33 +138,49 @@ function Map({
|
|||||||
disabledSettings={disabledSettings}
|
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 (
|
return (
|
||||||
<>
|
<MapInteraction
|
||||||
<MapInteraction
|
map={map}
|
||||||
map={map}
|
controls={
|
||||||
aspectRatio={aspectRatio}
|
<>
|
||||||
isEnabled={selectedToolId === "pan"}
|
{mapControls}
|
||||||
controls={mapControls}
|
{tokenMenu}
|
||||||
loading={loading}
|
</>
|
||||||
>
|
}
|
||||||
{map && mapImage}
|
>
|
||||||
{map && mapDrawing}
|
{mapTokens}
|
||||||
{map && mapFog}
|
</MapInteraction>
|
||||||
{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}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,171 +1,129 @@
|
|||||||
import React, { useRef, useEffect } from "react";
|
import React, { useRef, useEffect, useState, useContext } from "react";
|
||||||
import { Box } from "theme-ui";
|
import { Box } from "theme-ui";
|
||||||
import interact from "interactjs";
|
import { useGesture } from "react-use-gesture";
|
||||||
import normalizeWheel from "normalize-wheel";
|
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 { MapInteractionProvider } from "../../contexts/MapInteractionContext";
|
||||||
|
import MapStageContext from "../../contexts/MapStageContext";
|
||||||
import LoadingOverlay from "../LoadingOverlay";
|
import AuthContext from "../../contexts/AuthContext";
|
||||||
|
|
||||||
const zoomSpeed = -0.001;
|
const zoomSpeed = -0.001;
|
||||||
const minZoom = 0.1;
|
const minZoom = 0.1;
|
||||||
const maxZoom = 5;
|
const maxZoom = 5;
|
||||||
|
|
||||||
function MapInteraction({
|
function MapInteraction({ map, children, controls }) {
|
||||||
map,
|
const mapSource = useDataSource(map, defaultMapSources);
|
||||||
aspectRatio,
|
const [mapSourceImage] = useImage(mapSource);
|
||||||
isEnabled,
|
|
||||||
children,
|
const [stageWidth, setStageWidth] = useState(1);
|
||||||
controls,
|
const [stageHeight, setStageHeight] = useState(1);
|
||||||
loading,
|
const [stageScale, setStageScale] = useState(1);
|
||||||
}) {
|
const [stageTranslate, setStageTranslate] = useState({ x: 0, y: 0 });
|
||||||
const mapContainerRef = useRef();
|
const [preventMapInteraction, setPreventMapInteraction] = useState(false);
|
||||||
const mapMoveContainerRef = useRef();
|
|
||||||
const mapScaleContainerRef = useRef();
|
const stageWidthRef = useRef(stageWidth);
|
||||||
const mapTranslateRef = useRef({ x: 0, y: 0 });
|
const stageHeightRef = useRef(stageHeight);
|
||||||
const mapScaleRef = useRef(1);
|
const stageScaleRef = useRef(stageScale);
|
||||||
function setTranslateAndScale(newTranslate, newScale) {
|
const stageTranslateRef = useRef(stageTranslate);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleMove(event, isGesture) {
|
if (map) {
|
||||||
const scale = mapScaleRef.current;
|
const mapHeight = stageWidthRef.current * (map.height / map.width);
|
||||||
const translate = mapTranslateRef.current;
|
setStageTranslate({ x: 0, y: -(mapHeight - stageHeightRef.current) / 2 });
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
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]);
|
}, [map]);
|
||||||
|
|
||||||
// Bind the wheel event of the map via a ref
|
const bind = useGesture({
|
||||||
// in order to support non-passive event listening
|
onWheel: ({ delta }) => {
|
||||||
// to allow the track pad zoom to be interrupted
|
const newScale = Math.min(
|
||||||
// see https://github.com/facebook/react/issues/14856
|
Math.max(stageScale - delta[1] * zoomSpeed, minZoom),
|
||||||
useEffect(() => {
|
maxZoom
|
||||||
const mapContainer = mapContainerRef.current;
|
);
|
||||||
|
setStageScale(newScale);
|
||||||
function handleZoom(event) {
|
stageScaleRef.current = newScale;
|
||||||
// Stop overscroll on chrome and safari
|
},
|
||||||
// also stop pinch to zoom on chrome
|
onDrag: ({ delta }) => {
|
||||||
event.preventDefault();
|
if (!preventMapInteraction) {
|
||||||
|
const newTranslate = {
|
||||||
// Try and normalize the wheel event to prevent OS differences for zoom speed
|
x: stageTranslate.x + delta[0] / stageScale,
|
||||||
const normalized = normalizeWheel(event);
|
y: stageTranslate.y + delta[1] / stageScale,
|
||||||
|
};
|
||||||
const scale = mapScaleRef.current;
|
setStageTranslate(newTranslate);
|
||||||
const translate = mapTranslateRef.current;
|
stageTranslateRef.current = newTranslate;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
}, []);
|
});
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
sx={{ flexGrow: 1, position: "relative" }}
|
||||||
|
ref={containerRef}
|
||||||
|
{...bind()}
|
||||||
className="map"
|
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
|
<ReactResizeDetector handleWidth handleHeight onResize={handleResize}>
|
||||||
sx={{
|
<Stage
|
||||||
position: "relative",
|
width={stageWidth}
|
||||||
top: "50%",
|
height={stageHeight}
|
||||||
left: "50%",
|
scale={{ x: stageScale, y: stageScale }}
|
||||||
transform: "translate(-50%, -50%)",
|
x={stageWidth / 2}
|
||||||
}}
|
y={stageHeight / 2}
|
||||||
>
|
offset={{ x: stageWidth / 2, y: stageHeight / 2 }}
|
||||||
<Box ref={mapScaleContainerRef}>
|
ref={mapStageRef}
|
||||||
<Box ref={mapMoveContainerRef}>
|
>
|
||||||
<Box
|
<Layer x={stageTranslate.x} y={stageTranslate.y}>
|
||||||
sx={{
|
<Image
|
||||||
width: "100%",
|
image={mapSourceImage}
|
||||||
height: 0,
|
width={mapWidth}
|
||||||
paddingBottom: `${(1 / aspectRatio) * 100}%`,
|
height={mapHeight}
|
||||||
}}
|
id="mapImage"
|
||||||
/>
|
/>
|
||||||
<MapInteractionProvider
|
{/* Forward auth context to konva elements */}
|
||||||
value={{
|
<AuthContext.Provider value={auth}>
|
||||||
translateRef: mapTranslateRef,
|
<MapInteractionProvider value={mapInteraction}>
|
||||||
scaleRef: mapScaleRef,
|
{children}
|
||||||
}}
|
</MapInteractionProvider>
|
||||||
>
|
</AuthContext.Provider>
|
||||||
{children}
|
</Layer>
|
||||||
</MapInteractionProvider>
|
</Stage>
|
||||||
</Box>
|
</ReactResizeDetector>
|
||||||
</Box>
|
<MapInteractionProvider value={mapInteraction}>
|
||||||
</Box>
|
{controls}
|
||||||
{controls}
|
</MapInteractionProvider>
|
||||||
{loading && <LoadingOverlay />}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState, useContext } from "react";
|
||||||
import Modal from "react-modal";
|
import Modal from "react-modal";
|
||||||
|
|
||||||
import { useThemeUI } from "theme-ui";
|
import { useThemeUI } from "theme-ui";
|
||||||
|
|
||||||
|
import MapInteractionContext from "../../contexts/MapInteractionContext";
|
||||||
|
|
||||||
function MapMenu({
|
function MapMenu({
|
||||||
isOpen,
|
isOpen,
|
||||||
onRequestClose,
|
onRequestClose,
|
||||||
@ -21,6 +22,8 @@ function MapMenu({
|
|||||||
// callback
|
// callback
|
||||||
const [modalContentNode, setModalContentNode] = useState(null);
|
const [modalContentNode, setModalContentNode] = useState(null);
|
||||||
|
|
||||||
|
const { setPreventMapInteraction } = useContext(MapInteractionContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Close modal if interacting with any other element
|
// Close modal if interacting with any other element
|
||||||
function handlePointerDown(event) {
|
function handlePointerDown(event) {
|
||||||
@ -29,17 +32,20 @@ function MapMenu({
|
|||||||
!path.includes(modalContentNode) &&
|
!path.includes(modalContentNode) &&
|
||||||
!(excludeNode && path.includes(excludeNode))
|
!(excludeNode && path.includes(excludeNode))
|
||||||
) {
|
) {
|
||||||
|
setPreventMapInteraction(false);
|
||||||
onRequestClose();
|
onRequestClose();
|
||||||
document.body.removeEventListener("pointerdown", handlePointerDown);
|
document.body.removeEventListener("pointerdown", handlePointerDown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modalContentNode) {
|
if (modalContentNode) {
|
||||||
|
setPreventMapInteraction(true);
|
||||||
document.body.addEventListener("pointerdown", handlePointerDown);
|
document.body.addEventListener("pointerdown", handlePointerDown);
|
||||||
// Check for wheel event to close modal as well
|
// Check for wheel event to close modal as well
|
||||||
document.body.addEventListener(
|
document.body.addEventListener(
|
||||||
"wheel",
|
"wheel",
|
||||||
() => {
|
() => {
|
||||||
|
setPreventMapInteraction(false);
|
||||||
onRequestClose();
|
onRequestClose();
|
||||||
},
|
},
|
||||||
{ once: true }
|
{ once: true }
|
||||||
@ -50,7 +56,7 @@ function MapMenu({
|
|||||||
document.body.removeEventListener("pointerdown", handlePointerDown);
|
document.body.removeEventListener("pointerdown", handlePointerDown);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [modalContentNode, excludeNode, onRequestClose]);
|
}, [modalContentNode, excludeNode, onRequestClose, setPreventMapInteraction]);
|
||||||
|
|
||||||
function handleModalContent(node) {
|
function handleModalContent(node) {
|
||||||
setModalContentNode(node);
|
setModalContentNode(node);
|
||||||
|
@ -1,76 +1,88 @@
|
|||||||
import React, { useRef, useContext } from "react";
|
import React, { useContext, useState, useEffect, useRef } from "react";
|
||||||
import { Box, Image } from "theme-ui";
|
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 useDataSource from "../../helpers/useDataSource";
|
||||||
|
import useDebounce from "../../helpers/useDebounce";
|
||||||
|
|
||||||
import AuthContext from "../../contexts/AuthContext";
|
import AuthContext from "../../contexts/AuthContext";
|
||||||
|
import MapInteractionContext from "../../contexts/MapInteractionContext";
|
||||||
|
|
||||||
import { tokenSources, unknownSource } from "../../tokens";
|
import { tokenSources, unknownSource } from "../../tokens";
|
||||||
|
|
||||||
function MapToken({ token, tokenState, tokenSizePercent, className }) {
|
function MapToken({
|
||||||
|
token,
|
||||||
|
tokenState,
|
||||||
|
tokenSizePercent,
|
||||||
|
onTokenStateChange,
|
||||||
|
onTokenMenuOpen,
|
||||||
|
}) {
|
||||||
const { userId } = useContext(AuthContext);
|
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();
|
const imageRef = useRef();
|
||||||
// Stop touch to prevent 3d touch gesutre on iOS
|
useEffect(() => {
|
||||||
usePreventTouch(imageRef);
|
const image = imageRef.current;
|
||||||
|
if (image) {
|
||||||
|
image.cache({
|
||||||
|
pixelRatio: debouncedStageScale,
|
||||||
|
});
|
||||||
|
image.drawHitFromCache();
|
||||||
|
// Force redraw
|
||||||
|
image.parent.draw();
|
||||||
|
}
|
||||||
|
}, [debouncedStageScale, tokenWidth, tokenHeight, tokenSourceStatus]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<KonvaImage
|
||||||
style={{
|
ref={imageRef}
|
||||||
transform: `translate(${tokenState.x * 100}%, ${tokenState.y * 100}%)`,
|
width={tokenWidth}
|
||||||
width: "100%",
|
height={tokenHeight}
|
||||||
height: "100%",
|
x={tokenState.x * mapWidth}
|
||||||
transition:
|
y={tokenState.y * mapHeight}
|
||||||
tokenState.lastEditedBy === userId
|
image={tokenSourceImage}
|
||||||
? "initial"
|
draggable
|
||||||
: "transform 0.5s ease",
|
onDragEnd={handleDragEnd}
|
||||||
}}
|
onClick={handleClick}
|
||||||
sx={{
|
onMouseDown={() => setPreventMapInteraction(true)}
|
||||||
position: "absolute",
|
onMouseUp={() => setPreventMapInteraction(false)}
|
||||||
pointerEvents: "none",
|
onTouchStart={() => setPreventMapInteraction(true)}
|
||||||
}}
|
onTouchEnd={() => setPreventMapInteraction(false)}
|
||||||
>
|
/>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState, useContext } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { Image, Box } from "theme-ui";
|
import { Image, Box } from "theme-ui";
|
||||||
import interact from "interactjs";
|
import interact from "interactjs";
|
||||||
@ -8,6 +8,8 @@ import usePortal from "../../helpers/usePortal";
|
|||||||
import TokenLabel from "./TokenLabel";
|
import TokenLabel from "./TokenLabel";
|
||||||
import TokenStatus from "./TokenStatus";
|
import TokenStatus from "./TokenStatus";
|
||||||
|
|
||||||
|
import MapStageContext from "../../contexts/MapStageContext";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @callback onProxyDragEnd
|
* @callback onProxyDragEnd
|
||||||
* @param {boolean} isOnMap whether the token was dropped on the map
|
* @param {boolean} isOnMap whether the token was dropped on the map
|
||||||
@ -44,6 +46,7 @@ function ProxyToken({
|
|||||||
}, [tokens, disabledTokens]);
|
}, [tokens, disabledTokens]);
|
||||||
|
|
||||||
const proxyOnMap = useRef(false);
|
const proxyOnMap = useRef(false);
|
||||||
|
const mapStageRef = useContext(MapStageContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
interact(`.${tokenClassName}`).draggable({
|
interact(`.${tokenClassName}`).draggable({
|
||||||
@ -110,18 +113,27 @@ function ProxyToken({
|
|||||||
}
|
}
|
||||||
let proxy = proxyRef.current;
|
let proxy = proxyRef.current;
|
||||||
if (proxy) {
|
if (proxy) {
|
||||||
const mapImage = document.querySelector(".mapImage");
|
const mapStage = mapStageRef.current;
|
||||||
if (onProxyDragEnd && mapImage) {
|
if (onProxyDragEnd && mapStage) {
|
||||||
const mapImageRect = mapImage.getBoundingClientRect();
|
const mapImageRect = mapStage
|
||||||
|
.findOne("#mapImage")
|
||||||
|
.getClientRect();
|
||||||
|
|
||||||
|
const map = document.querySelector(".map");
|
||||||
|
const mapRect = map.getBoundingClientRect();
|
||||||
|
|
||||||
let x = parseFloat(proxy.getAttribute("data-x")) || 0;
|
let x = parseFloat(proxy.getAttribute("data-x")) || 0;
|
||||||
let y = parseFloat(proxy.getAttribute("data-y")) || 0;
|
let y = parseFloat(proxy.getAttribute("data-y")) || 0;
|
||||||
|
|
||||||
|
// TODO: This seems to be wrong when map is zoomed
|
||||||
|
|
||||||
// Convert coordiantes to be relative to the map
|
// Convert coordiantes to be relative to the map
|
||||||
x = x - mapImageRect.left;
|
x = x - mapRect.left - mapImageRect.x;
|
||||||
y = y - mapImageRect.top;
|
y = y - mapRect.top - mapImageRect.y;
|
||||||
|
|
||||||
// Normalize to map width
|
// Normalize to map width
|
||||||
x = x / (mapImageRect.right - mapImageRect.left);
|
x = x / mapImageRect.width;
|
||||||
y = y / (mapImageRect.bottom - mapImageRect.top);
|
y = y / mapImageRect.height;
|
||||||
|
|
||||||
// Get the token from the supplied tokens if it exists
|
// Get the token from the supplied tokens if it exists
|
||||||
const token = tokensRef.current[id] || {};
|
const token = tokensRef.current[id] || {};
|
||||||
@ -145,7 +157,7 @@ function ProxyToken({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [onProxyDragEnd, tokenClassName, proxyContainer]);
|
}, [onProxyDragEnd, tokenClassName, proxyContainer, mapStageRef]);
|
||||||
|
|
||||||
if (!imageSource) {
|
if (!imageSource) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import React, { useEffect, useState, useRef } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import interact from "interactjs";
|
|
||||||
import { Box, Input, Slider, Flex, Text } from "theme-ui";
|
import { Box, Input, Slider, Flex, Text } from "theme-ui";
|
||||||
|
|
||||||
import MapMenu from "../map/MapMenu";
|
import MapMenu from "../map/MapMenu";
|
||||||
|
|
||||||
import colors, { colorOptions } from "../../helpers/colors";
|
import colors, { colorOptions } from "../../helpers/colors";
|
||||||
|
|
||||||
|
import usePrevious from "../../helpers/usePrevious";
|
||||||
|
|
||||||
const defaultTokenMaxSize = 6;
|
const defaultTokenMaxSize = 6;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,105 +21,59 @@ const defaultTokenMaxSize = 6;
|
|||||||
* @param {Object} tokens An mapping of tokens to use as a base when calling onTokenChange
|
* @param {Object} tokens An mapping of tokens to use as a base when calling onTokenChange
|
||||||
* @param {Object} disabledTokens An optional mapping of tokens that shouldn't allow interaction
|
* @param {Object} disabledTokens An optional mapping of tokens that shouldn't allow interaction
|
||||||
*/
|
*/
|
||||||
function TokenMenu({ tokenClassName, onTokenChange, tokens, disabledTokens }) {
|
function TokenMenu({
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
isOpen,
|
||||||
|
onRequestClose,
|
||||||
|
tokenState,
|
||||||
|
tokenImage,
|
||||||
|
onTokenChange,
|
||||||
|
}) {
|
||||||
|
const wasOpen = usePrevious(isOpen);
|
||||||
|
|
||||||
function handleRequestClose() {
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the tokens in a ref and access in the interactjs loop
|
|
||||||
// This is needed to stop interactjs from creating multiple listeners
|
|
||||||
const tokensRef = useRef(tokens);
|
|
||||||
const disabledTokensRef = useRef(disabledTokens);
|
|
||||||
useEffect(() => {
|
|
||||||
tokensRef.current = tokens;
|
|
||||||
disabledTokensRef.current = disabledTokens;
|
|
||||||
}, [tokens, disabledTokens]);
|
|
||||||
|
|
||||||
const [currentToken, setCurrentToken] = useState({});
|
|
||||||
const [menuLeft, setMenuLeft] = useState(0);
|
|
||||||
const [menuTop, setMenuTop] = useState(0);
|
|
||||||
const [tokenMaxSize, setTokenMaxSize] = useState(defaultTokenMaxSize);
|
const [tokenMaxSize, setTokenMaxSize] = useState(defaultTokenMaxSize);
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && !wasOpen) {
|
||||||
|
setTokenMaxSize(Math.max(tokenState.size, defaultTokenMaxSize));
|
||||||
|
}
|
||||||
|
}, [isOpen, tokenState, wasOpen]);
|
||||||
|
|
||||||
function handleLabelChange(event) {
|
function handleLabelChange(event) {
|
||||||
const label = event.target.value;
|
const label = event.target.value;
|
||||||
setCurrentToken((prevToken) => ({
|
onTokenChange({ ...tokenState, label: label });
|
||||||
...prevToken,
|
|
||||||
label: label,
|
|
||||||
}));
|
|
||||||
|
|
||||||
onTokenChange({ ...currentToken, label: label });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [menuLeft, setMenuLeft] = useState(0);
|
||||||
|
const [menuTop, setMenuTop] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tokenImage) {
|
||||||
|
const imageRect = tokenImage.getClientRect();
|
||||||
|
const map = document.querySelector(".map");
|
||||||
|
const mapRect = map.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Center X for the menu which is 156px wide
|
||||||
|
setMenuLeft(mapRect.left + imageRect.x + imageRect.width / 2 - 156 / 2);
|
||||||
|
// Y 12px from the bottom
|
||||||
|
setMenuTop(mapRect.top + imageRect.y + imageRect.height + 12);
|
||||||
|
}
|
||||||
|
}, [tokenImage]);
|
||||||
|
|
||||||
function handleStatusChange(status) {
|
function handleStatusChange(status) {
|
||||||
const statuses = currentToken.statuses;
|
const statuses = tokenState.statuses;
|
||||||
let newStatuses = [];
|
let newStatuses = [];
|
||||||
if (statuses.includes(status)) {
|
if (statuses.includes(status)) {
|
||||||
newStatuses = statuses.filter((s) => s !== status);
|
newStatuses = statuses.filter((s) => s !== status);
|
||||||
} else {
|
} else {
|
||||||
newStatuses = [...statuses, status];
|
newStatuses = [...statuses, status];
|
||||||
}
|
}
|
||||||
setCurrentToken((prevToken) => ({
|
onTokenChange({ ...tokenState, statuses: newStatuses });
|
||||||
...prevToken,
|
|
||||||
statuses: newStatuses,
|
|
||||||
}));
|
|
||||||
onTokenChange({ ...currentToken, statuses: newStatuses });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSizeChange(event) {
|
function handleSizeChange(event) {
|
||||||
const newSize = parseInt(event.target.value);
|
const newSize = parseInt(event.target.value);
|
||||||
setCurrentToken((prevToken) => ({
|
onTokenChange({ ...tokenState, size: newSize });
|
||||||
...prevToken,
|
|
||||||
size: newSize,
|
|
||||||
}));
|
|
||||||
onTokenChange({ ...currentToken, size: newSize });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function handleTokenMenuOpen(event) {
|
|
||||||
const target = event.target;
|
|
||||||
const id = target.getAttribute("data-id");
|
|
||||||
if (id in disabledTokensRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const token = tokensRef.current[id] || {};
|
|
||||||
setCurrentToken(token);
|
|
||||||
// Set token max size to be higher if needed
|
|
||||||
setTokenMaxSize(Math.max(token.size, defaultTokenMaxSize));
|
|
||||||
|
|
||||||
const targetRect = target.getBoundingClientRect();
|
|
||||||
setMenuLeft(targetRect.left);
|
|
||||||
setMenuTop(targetRect.bottom);
|
|
||||||
|
|
||||||
setIsOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add listener for tap gesture
|
|
||||||
const tokenInteract = interact(`.${tokenClassName}`).on(
|
|
||||||
"tap",
|
|
||||||
handleTokenMenuOpen
|
|
||||||
);
|
|
||||||
|
|
||||||
function handleMapContextMenu(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
if (event.target.classList.contains(tokenClassName)) {
|
|
||||||
handleTokenMenuOpen(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle context menu on the map level as handling
|
|
||||||
// on the token level lead to the default menu still
|
|
||||||
// being displayed
|
|
||||||
const map = document.querySelector(".map");
|
|
||||||
map.addEventListener("contextmenu", handleMapContextMenu);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
map.removeEventListener("contextmenu", handleMapContextMenu);
|
|
||||||
tokenInteract.unset();
|
|
||||||
};
|
|
||||||
}, [tokenClassName]);
|
|
||||||
|
|
||||||
function handleModalContent(node) {
|
function handleModalContent(node) {
|
||||||
if (node) {
|
if (node) {
|
||||||
// Focus input
|
// Focus input
|
||||||
@ -145,7 +100,7 @@ function TokenMenu({ tokenClassName, onTokenChange, tokens, disabledTokens }) {
|
|||||||
return (
|
return (
|
||||||
<MapMenu
|
<MapMenu
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onRequestClose={handleRequestClose}
|
onRequestClose={onRequestClose}
|
||||||
top={`${menuTop}px`}
|
top={`${menuTop}px`}
|
||||||
left={`${menuLeft}px`}
|
left={`${menuLeft}px`}
|
||||||
onModalContent={handleModalContent}
|
onModalContent={handleModalContent}
|
||||||
@ -155,7 +110,7 @@ function TokenMenu({ tokenClassName, onTokenChange, tokens, disabledTokens }) {
|
|||||||
as="form"
|
as="form"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleRequestClose();
|
onRequestClose();
|
||||||
}}
|
}}
|
||||||
sx={{ alignItems: "center" }}
|
sx={{ alignItems: "center" }}
|
||||||
>
|
>
|
||||||
@ -170,7 +125,7 @@ function TokenMenu({ tokenClassName, onTokenChange, tokens, disabledTokens }) {
|
|||||||
<Input
|
<Input
|
||||||
id="changeTokenLabel"
|
id="changeTokenLabel"
|
||||||
onChange={handleLabelChange}
|
onChange={handleLabelChange}
|
||||||
value={currentToken.label}
|
value={tokenState.label}
|
||||||
sx={{
|
sx={{
|
||||||
padding: "4px",
|
padding: "4px",
|
||||||
border: "none",
|
border: "none",
|
||||||
@ -202,7 +157,7 @@ function TokenMenu({ tokenClassName, onTokenChange, tokens, disabledTokens }) {
|
|||||||
onClick={() => handleStatusChange(color)}
|
onClick={() => handleStatusChange(color)}
|
||||||
aria-label={`Token label Color ${color}`}
|
aria-label={`Token label Color ${color}`}
|
||||||
>
|
>
|
||||||
{currentToken.statuses && currentToken.statuses.includes(color) && (
|
{tokenState.statuses && tokenState.statuses.includes(color) && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@ -227,7 +182,7 @@ function TokenMenu({ tokenClassName, onTokenChange, tokens, disabledTokens }) {
|
|||||||
Size:
|
Size:
|
||||||
</Text>
|
</Text>
|
||||||
<Slider
|
<Slider
|
||||||
value={currentToken.size || 1}
|
value={tokenState.size || 1}
|
||||||
onChange={handleSizeChange}
|
onChange={handleSizeChange}
|
||||||
step={1}
|
step={1}
|
||||||
min={1}
|
min={1}
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
const MapInteractionContext = React.createContext({
|
const MapInteractionContext = React.createContext({
|
||||||
translateRef: null,
|
stageTranslate: { x: 0, y: 0 },
|
||||||
scaleRef: null,
|
stageScale: 1,
|
||||||
|
stageWidth: 1,
|
||||||
|
stageHeight: 1,
|
||||||
|
setPreventMapInteraction: () => {},
|
||||||
|
mapWidth: 1,
|
||||||
|
mapHeight: 1,
|
||||||
});
|
});
|
||||||
export const MapInteractionProvider = MapInteractionContext.Provider;
|
export const MapInteractionProvider = MapInteractionContext.Provider;
|
||||||
|
|
||||||
|
8
src/contexts/MapStageContext.js
Normal file
8
src/contexts/MapStageContext.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const MapStageContext = React.createContext({
|
||||||
|
mapStageRef: { current: null },
|
||||||
|
});
|
||||||
|
export const MapStageProvider = MapStageContext.Provider;
|
||||||
|
|
||||||
|
export default MapStageContext;
|
25
src/helpers/usePreventOverscroll.js
Normal file
25
src/helpers/usePreventOverscroll.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
function usePreventOverscroll(elementRef) {
|
||||||
|
useEffect(() => {
|
||||||
|
// Stop overscroll on chrome and safari
|
||||||
|
// also stop pinch to zoom on chrome
|
||||||
|
function preventOverscroll(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
const element = elementRef.current;
|
||||||
|
if (element) {
|
||||||
|
element.addEventListener("wheel", preventOverscroll, {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (element) {
|
||||||
|
element.removeEventListener("wheel", preventOverscroll);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [elementRef]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default usePreventOverscroll;
|
@ -1,4 +1,10 @@
|
|||||||
import React, { useState, useEffect, useCallback, useContext } from "react";
|
import React, {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useRef,
|
||||||
|
} from "react";
|
||||||
import { Flex, Box, Text } from "theme-ui";
|
import { Flex, Box, Text } from "theme-ui";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
@ -19,6 +25,7 @@ import AuthContext from "../contexts/AuthContext";
|
|||||||
import DatabaseContext from "../contexts/DatabaseContext";
|
import DatabaseContext from "../contexts/DatabaseContext";
|
||||||
import TokenDataContext from "../contexts/TokenDataContext";
|
import TokenDataContext from "../contexts/TokenDataContext";
|
||||||
import MapDataContext from "../contexts/MapDataContext";
|
import MapDataContext from "../contexts/MapDataContext";
|
||||||
|
import { MapStageProvider } from "../contexts/MapStageContext";
|
||||||
|
|
||||||
function Game() {
|
function Game() {
|
||||||
const { id: gameId } = useParams();
|
const { id: gameId } = useParams();
|
||||||
@ -500,8 +507,12 @@ function Game() {
|
|||||||
}
|
}
|
||||||
}, [stream, peers, handleStreamEnd]);
|
}, [stream, peers, handleStreamEnd]);
|
||||||
|
|
||||||
|
// A ref to the Konva stage
|
||||||
|
// the ref will be assigned in the MapInteraction component
|
||||||
|
const mapStageRef = useRef();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<MapStageProvider value={mapStageRef}>
|
||||||
<Flex sx={{ flexDirection: "column", height: "100%" }}>
|
<Flex sx={{ flexDirection: "column", height: "100%" }}>
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
@ -551,7 +562,7 @@ function Game() {
|
|||||||
</Banner>
|
</Banner>
|
||||||
<AuthModal isOpen={authenticationStatus === "unauthenticated"} />
|
<AuthModal isOpen={authenticationStatus === "unauthenticated"} />
|
||||||
{authenticationStatus === "unknown" && <LoadingOverlay />}
|
{authenticationStatus === "unknown" && <LoadingOverlay />}
|
||||||
</>
|
</MapStageProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
62
yarn.lock
62
yarn.lock
@ -6868,6 +6868,11 @@ kleur@^3.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
|
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
|
||||||
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
|
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
|
||||||
|
|
||||||
|
konva@^6.0.0:
|
||||||
|
version "6.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/konva/-/konva-6.0.0.tgz#9b3d13a4622f353c4ce736fbf1fa4b6483240649"
|
||||||
|
integrity sha512-YTwmtz3KzbzdC0KDRHWLzuk0KXB4NUdaQqytrxacXE1C39V6wCk7Nnu0wgq+GdGbG6m8A1qiEU9TSJ19qdIzDw==
|
||||||
|
|
||||||
last-call-webpack-plugin@^3.0.0:
|
last-call-webpack-plugin@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555"
|
resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555"
|
||||||
@ -6988,6 +6993,11 @@ locate-path@^5.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-locate "^4.1.0"
|
p-locate "^4.1.0"
|
||||||
|
|
||||||
|
lodash-es@^4.17.15:
|
||||||
|
version "4.17.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
|
||||||
|
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
|
||||||
|
|
||||||
lodash._reinterpolate@^3.0.0:
|
lodash._reinterpolate@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
|
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
|
||||||
@ -9088,6 +9098,11 @@ queue-microtask@^1.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.1.2.tgz#139bf8186db0c545017ec66c2664ac646d5c571e"
|
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.1.2.tgz#139bf8186db0c545017ec66c2664ac646d5c571e"
|
||||||
integrity sha512-F9wwNePtXrzZenAB3ax0Y8TSKGvuB7Qw16J30hspEUTbfUM+H827XyN3rlpwhVmtm5wuZtbKIHjOnwDn7MUxWQ==
|
integrity sha512-F9wwNePtXrzZenAB3ax0Y8TSKGvuB7Qw16J30hspEUTbfUM+H827XyN3rlpwhVmtm5wuZtbKIHjOnwDn7MUxWQ==
|
||||||
|
|
||||||
|
raf-schd@^4.0.2:
|
||||||
|
version "4.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0"
|
||||||
|
integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==
|
||||||
|
|
||||||
raf@^3.4.1:
|
raf@^3.4.1:
|
||||||
version "3.4.1"
|
version "3.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
|
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
|
||||||
@ -9209,6 +9224,14 @@ react-is@^16.8.1, react-is@^16.8.4:
|
|||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
|
||||||
integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==
|
integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==
|
||||||
|
|
||||||
|
react-konva@^16.13.0-3:
|
||||||
|
version "16.13.0-3"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-konva/-/react-konva-16.13.0-3.tgz#9ef1e813c8b2dd61b54b26151ccbdeed52b89a80"
|
||||||
|
integrity sha512-U9az1RidQD4c64oZoHiiv6GU6h2ggHO30nZDqfQWuBTH+Bl2wij6Z0NgbUyVyN1IpKIgXRiEKMS9idlxhAzTXQ==
|
||||||
|
dependencies:
|
||||||
|
react-reconciler "^0.25.1"
|
||||||
|
scheduler "^0.19.1"
|
||||||
|
|
||||||
react-lifecycles-compat@^3.0.0:
|
react-lifecycles-compat@^3.0.0:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||||
@ -9238,6 +9261,27 @@ react-modal@^3.11.2:
|
|||||||
react-lifecycles-compat "^3.0.0"
|
react-lifecycles-compat "^3.0.0"
|
||||||
warning "^4.0.3"
|
warning "^4.0.3"
|
||||||
|
|
||||||
|
react-reconciler@^0.25.1:
|
||||||
|
version "0.25.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.25.1.tgz#f9814d59d115e1210762287ce987801529363aaa"
|
||||||
|
integrity sha512-R5UwsIvRcSs3w8n9k3tBoTtUHdVhu9u84EG7E5M0Jk9F5i6DA1pQzPfUZd6opYWGy56MJOtV3VADzy6DRwYDjw==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
object-assign "^4.1.1"
|
||||||
|
prop-types "^15.6.2"
|
||||||
|
scheduler "^0.19.1"
|
||||||
|
|
||||||
|
react-resize-detector@^4.2.3:
|
||||||
|
version "4.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-4.2.3.tgz#7df258668a30bdfd88e655bbdb27db7fd7b23127"
|
||||||
|
integrity sha512-4AeS6lxdz2KOgDZaOVt1duoDHrbYwSrUX32KeM9j6t9ISyRphoJbTRCMS1aPFxZHFqcCGLT1gMl3lEcSWZNW0A==
|
||||||
|
dependencies:
|
||||||
|
lodash "^4.17.15"
|
||||||
|
lodash-es "^4.17.15"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
raf-schd "^4.0.2"
|
||||||
|
resize-observer-polyfill "^1.5.1"
|
||||||
|
|
||||||
react-router-dom@^5.1.2:
|
react-router-dom@^5.1.2:
|
||||||
version "5.1.2"
|
version "5.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18"
|
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18"
|
||||||
@ -9334,6 +9378,11 @@ react-scripts@3.4.0:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "2.1.2"
|
fsevents "2.1.2"
|
||||||
|
|
||||||
|
react-use-gesture@^7.0.15:
|
||||||
|
version "7.0.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-use-gesture/-/react-use-gesture-7.0.15.tgz#93c651e916a31cfb12d079e7fa1543d5b0511e07"
|
||||||
|
integrity sha512-vHQkaa7oUbSDTAcFk9huQXa7E8KPrZH91erPuOMoqZT513qvtbb/SzTQ33lHc71/kOoJkMbzOkc4uoA4sT7Ogg==
|
||||||
|
|
||||||
react@^16.13.0:
|
react@^16.13.0:
|
||||||
version "16.13.0"
|
version "16.13.0"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-16.13.0.tgz#d046eabcdf64e457bbeed1e792e235e1b9934cf7"
|
resolved "https://registry.yarnpkg.com/react/-/react-16.13.0.tgz#d046eabcdf64e457bbeed1e792e235e1b9934cf7"
|
||||||
@ -9891,6 +9940,14 @@ scheduler@^0.19.0:
|
|||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
|
|
||||||
|
scheduler@^0.19.1:
|
||||||
|
version "0.19.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196"
|
||||||
|
integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
object-assign "^4.1.1"
|
||||||
|
|
||||||
schema-utils@^1.0.0:
|
schema-utils@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
|
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
|
||||||
@ -11171,6 +11228,11 @@ url@^0.11.0:
|
|||||||
punycode "1.3.2"
|
punycode "1.3.2"
|
||||||
querystring "0.2.0"
|
querystring "0.2.0"
|
||||||
|
|
||||||
|
use-image@^1.0.5:
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/use-image/-/use-image-1.0.5.tgz#51fa23fe705c3ad0d4ae3eca6cf636551c591693"
|
||||||
|
integrity sha512-tv1tHn1GRcbrifNzCPAN81Z1Fayfd3GXkUDFx0/dUkqqPmADNDRoCyT9MqrUX9GPcofsQl6SREPr9Zavm3dRTQ==
|
||||||
|
|
||||||
use@^3.1.0:
|
use@^3.1.0:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
||||||
|
Loading…
Reference in New Issue
Block a user