diff --git a/package.json b/package.json
index 341f414..83a4a78 100644
--- a/package.json
+++ b/package.json
@@ -11,21 +11,26 @@
"dexie": "^2.0.4",
"fake-indexeddb": "^3.0.0",
"interactjs": "^1.9.7",
+ "konva": "^6.0.0",
"normalize-wheel": "^1.0.1",
"raw.macro": "^0.3.0",
"react": "^16.13.0",
"react-dom": "^16.13.0",
+ "react-konva": "^16.13.0-3",
"react-markdown": "^4.3.1",
"react-modal": "^3.11.2",
+ "react-resize-detector": "^4.2.3",
"react-router-dom": "^5.1.2",
"react-router-hash-link": "^1.2.2",
"react-scripts": "3.4.0",
+ "react-use-gesture": "^7.0.15",
"shortid": "^2.2.15",
"simple-peer": "^9.6.2",
"simplebar-react": "^2.1.0",
"simplify-js": "^1.2.4",
"socket.io-client": "^2.3.0",
"theme-ui": "^0.3.1",
+ "use-image": "^1.0.5",
"webrtc-adapter": "^7.5.1"
},
"scripts": {
diff --git a/src/components/map/Map.js b/src/components/map/Map.js
index f259128..c9182dc 100644
--- a/src/components/map/Map.js
+++ b/src/components/map/Map.js
@@ -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 = (
-
-
-
- );
-
- const mapTokens = (
-
- {mapState &&
- Object.values(mapState.tokens).map((tokenState) => (
- token.id === tokenState.tokenId)}
- tokenState={tokenState}
- tokenSizePercent={tokenSizePercent}
- className={`${mapTokenProxyClassName} ${mapTokenMenuClassName}`}
- />
- ))}
-
- );
-
- const mapDrawing = (
-
- );
-
- const mapFog = (
-
- );
-
const mapControls = (
);
+
+ 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) => (
+ token.id === tokenState.tokenId)}
+ tokenState={tokenState}
+ tokenSizePercent={tokenSizePercent}
+ onTokenStateChange={onMapTokenStateChange}
+ onTokenMenuOpen={handleTokenMenuOpen}
+ />
+ ));
+
+ const tokenMenu = isTokenMenuOpen && (
+ setIsTokenMenuOpen(false)}
+ onTokenChange={onMapTokenStateChange}
+ tokenState={mapState.tokens[tokenMenuOptions.tokenStateId]}
+ tokenImage={tokenMenuOptions.tokenImage}
+ />
+ );
+
return (
- <>
-
- {map && mapImage}
- {map && mapDrawing}
- {map && mapFog}
- {map && mapTokens}
-
-
-
- >
+
+ {mapControls}
+ {tokenMenu}
+ >
+ }
+ >
+ {mapTokens}
+
);
}
diff --git a/src/components/map/MapInteraction.js b/src/components/map/MapInteraction.js
index 48bfff8..142c393 100644
--- a/src/components/map/MapInteraction.js
+++ b/src/components/map/MapInteraction.js
@@ -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 (
-
-
-
-
+
+
+
-
- {children}
-
-
-
-
- {controls}
- {loading && }
+ {/* Forward auth context to konva elements */}
+
+
+ {children}
+
+
+
+
+
+
+ {controls}
+
);
}
diff --git a/src/components/map/MapMenu.js b/src/components/map/MapMenu.js
index 6ddfd75..909f141 100644
--- a/src/components/map/MapMenu.js
+++ b/src/components/map/MapMenu.js
@@ -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);
diff --git a/src/components/map/MapToken.js b/src/components/map/MapToken.js
index 79d8087..82b2f53 100644
--- a/src/components/map/MapToken.js
+++ b/src/components/map/MapToken.js
@@ -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 (
-
-
-
-
- {tokenState.statuses && }
- {tokenState.label && }
-
-
-
+ setPreventMapInteraction(true)}
+ onMouseUp={() => setPreventMapInteraction(false)}
+ onTouchStart={() => setPreventMapInteraction(true)}
+ onTouchEnd={() => setPreventMapInteraction(false)}
+ />
);
}
diff --git a/src/components/token/ProxyToken.js b/src/components/token/ProxyToken.js
index 90c4c32..a0dd52c 100644
--- a/src/components/token/ProxyToken.js
+++ b/src/components/token/ProxyToken.js
@@ -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 { Image, Box } from "theme-ui";
import interact from "interactjs";
@@ -8,6 +8,8 @@ import usePortal from "../../helpers/usePortal";
import TokenLabel from "./TokenLabel";
import TokenStatus from "./TokenStatus";
+import MapStageContext from "../../contexts/MapStageContext";
+
/**
* @callback onProxyDragEnd
* @param {boolean} isOnMap whether the token was dropped on the map
@@ -44,6 +46,7 @@ function ProxyToken({
}, [tokens, disabledTokens]);
const proxyOnMap = useRef(false);
+ const mapStageRef = useContext(MapStageContext);
useEffect(() => {
interact(`.${tokenClassName}`).draggable({
@@ -110,18 +113,27 @@ function ProxyToken({
}
let proxy = proxyRef.current;
if (proxy) {
- const mapImage = document.querySelector(".mapImage");
- if (onProxyDragEnd && mapImage) {
- const mapImageRect = mapImage.getBoundingClientRect();
+ const mapStage = mapStageRef.current;
+ if (onProxyDragEnd && mapStage) {
+ const mapImageRect = mapStage
+ .findOne("#mapImage")
+ .getClientRect();
+
+ const map = document.querySelector(".map");
+ const mapRect = map.getBoundingClientRect();
let x = parseFloat(proxy.getAttribute("data-x")) || 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
- x = x - mapImageRect.left;
- y = y - mapImageRect.top;
+ x = x - mapRect.left - mapImageRect.x;
+ y = y - mapRect.top - mapImageRect.y;
+
// Normalize to map width
- x = x / (mapImageRect.right - mapImageRect.left);
- y = y / (mapImageRect.bottom - mapImageRect.top);
+ x = x / mapImageRect.width;
+ y = y / mapImageRect.height;
// Get the token from the supplied tokens if it exists
const token = tokensRef.current[id] || {};
@@ -145,7 +157,7 @@ function ProxyToken({
},
},
});
- }, [onProxyDragEnd, tokenClassName, proxyContainer]);
+ }, [onProxyDragEnd, tokenClassName, proxyContainer, mapStageRef]);
if (!imageSource) {
return null;
diff --git a/src/components/token/TokenMenu.js b/src/components/token/TokenMenu.js
index b87ab78..07161fe 100644
--- a/src/components/token/TokenMenu.js
+++ b/src/components/token/TokenMenu.js
@@ -1,11 +1,12 @@
-import React, { useEffect, useState, useRef } from "react";
-import interact from "interactjs";
+import React, { useEffect, useState } from "react";
import { Box, Input, Slider, Flex, Text } from "theme-ui";
import MapMenu from "../map/MapMenu";
import colors, { colorOptions } from "../../helpers/colors";
+import usePrevious from "../../helpers/usePrevious";
+
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} disabledTokens An optional mapping of tokens that shouldn't allow interaction
*/
-function TokenMenu({ tokenClassName, onTokenChange, tokens, disabledTokens }) {
- const [isOpen, setIsOpen] = useState(false);
+function TokenMenu({
+ 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);
+ useEffect(() => {
+ if (isOpen && !wasOpen) {
+ setTokenMaxSize(Math.max(tokenState.size, defaultTokenMaxSize));
+ }
+ }, [isOpen, tokenState, wasOpen]);
function handleLabelChange(event) {
const label = event.target.value;
- setCurrentToken((prevToken) => ({
- ...prevToken,
- label: label,
- }));
-
- onTokenChange({ ...currentToken, label: label });
+ onTokenChange({ ...tokenState, 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) {
- const statuses = currentToken.statuses;
+ const statuses = tokenState.statuses;
let newStatuses = [];
if (statuses.includes(status)) {
newStatuses = statuses.filter((s) => s !== status);
} else {
newStatuses = [...statuses, status];
}
- setCurrentToken((prevToken) => ({
- ...prevToken,
- statuses: newStatuses,
- }));
- onTokenChange({ ...currentToken, statuses: newStatuses });
+ onTokenChange({ ...tokenState, statuses: newStatuses });
}
function handleSizeChange(event) {
const newSize = parseInt(event.target.value);
- setCurrentToken((prevToken) => ({
- ...prevToken,
- size: newSize,
- }));
- onTokenChange({ ...currentToken, size: newSize });
+ onTokenChange({ ...tokenState, 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) {
if (node) {
// Focus input
@@ -145,7 +100,7 @@ function TokenMenu({ tokenClassName, onTokenChange, tokens, disabledTokens }) {
return (
{
e.preventDefault();
- handleRequestClose();
+ onRequestClose();
}}
sx={{ alignItems: "center" }}
>
@@ -170,7 +125,7 @@ function TokenMenu({ tokenClassName, onTokenChange, tokens, disabledTokens }) {
handleStatusChange(color)}
aria-label={`Token label Color ${color}`}
>
- {currentToken.statuses && currentToken.statuses.includes(color) && (
+ {tokenState.statuses && tokenState.statuses.includes(color) && (
{},
+ mapWidth: 1,
+ mapHeight: 1,
});
export const MapInteractionProvider = MapInteractionContext.Provider;
diff --git a/src/contexts/MapStageContext.js b/src/contexts/MapStageContext.js
new file mode 100644
index 0000000..e8f61d8
--- /dev/null
+++ b/src/contexts/MapStageContext.js
@@ -0,0 +1,8 @@
+import React from "react";
+
+const MapStageContext = React.createContext({
+ mapStageRef: { current: null },
+});
+export const MapStageProvider = MapStageContext.Provider;
+
+export default MapStageContext;
diff --git a/src/helpers/usePreventOverscroll.js b/src/helpers/usePreventOverscroll.js
new file mode 100644
index 0000000..73cd602
--- /dev/null
+++ b/src/helpers/usePreventOverscroll.js
@@ -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;
diff --git a/src/routes/Game.js b/src/routes/Game.js
index edc5cb6..0bcad2d 100644
--- a/src/routes/Game.js
+++ b/src/routes/Game.js
@@ -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 { useParams } from "react-router-dom";
@@ -19,6 +25,7 @@ import AuthContext from "../contexts/AuthContext";
import DatabaseContext from "../contexts/DatabaseContext";
import TokenDataContext from "../contexts/TokenDataContext";
import MapDataContext from "../contexts/MapDataContext";
+import { MapStageProvider } from "../contexts/MapStageContext";
function Game() {
const { id: gameId } = useParams();
@@ -500,8 +507,12 @@ function Game() {
}
}, [stream, peers, handleStreamEnd]);
+ // A ref to the Konva stage
+ // the ref will be assigned in the MapInteraction component
+ const mapStageRef = useRef();
+
return (
- <>
+
{authenticationStatus === "unknown" && }
- >
+
);
}
diff --git a/yarn.lock b/yarn.lock
index 9cb4b13..b368756 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6868,6 +6868,11 @@ kleur@^3.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
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:
version "3.0.0"
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:
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:
version "3.0.0"
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"
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:
version "3.4.1"
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"
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:
version "3.0.4"
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"
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:
version "5.1.2"
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:
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:
version "16.13.0"
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"
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:
version "1.0.0"
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"
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:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"