diff --git a/src/components/map/MapEditor.js b/src/components/map/MapEditor.js
new file mode 100644
index 0000000..74d977f
--- /dev/null
+++ b/src/components/map/MapEditor.js
@@ -0,0 +1,184 @@
+import React, { useState, useRef, useEffect } from "react";
+import { Box } from "theme-ui";
+import { Stage, Layer, Image } from "react-konva";
+import ReactResizeDetector from "react-resize-detector";
+import useImage from "use-image";
+import { useGesture } from "react-use-gesture";
+import normalizeWheel from "normalize-wheel";
+
+import useDataSource from "../../helpers/useDataSource";
+import usePreventOverscroll from "../../helpers/usePreventOverscroll";
+
+import { mapSources as defaultMapSources } from "../../maps";
+
+const wheelZoomSpeed = -0.001;
+const touchZoomSpeed = 0.005;
+const minZoom = 0.1;
+const maxZoom = 5;
+
+function MapEditor({ map }) {
+ 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 stageRatio = stageWidth / stageHeight;
+ const mapRatio = map ? map.width / map.height : 1;
+
+ let mapWidth;
+ let mapHeight;
+ if (stageRatio > mapRatio) {
+ mapWidth = map ? stageHeight / (map.height / map.width) : stageWidth;
+ mapHeight = stageHeight;
+ } else {
+ mapWidth = stageWidth;
+ mapHeight = map ? stageWidth * (map.height / map.width) : stageHeight;
+ }
+
+ const stageTranslateRef = useRef({ x: 0, y: 0 });
+ const isInteractingWithCanvas = useRef(false);
+ const pinchPreviousDistanceRef = useRef();
+ const pinchPreviousOriginRef = useRef();
+ const mapLayerRef = useRef();
+
+ function handleResize(width, height) {
+ setStageWidth(width);
+ setStageHeight(height);
+ }
+
+ useEffect(() => {
+ const layer = mapLayerRef.current;
+ const containerRect = containerRef.current.getBoundingClientRect();
+ if (map && layer) {
+ let newTranslate;
+ if (stageRatio > mapRatio) {
+ newTranslate = {
+ x: -(mapWidth - containerRect.width) / 2,
+ y: 0,
+ };
+ } else {
+ newTranslate = {
+ x: 0,
+ y: -(mapHeight - containerRect.height) / 2,
+ };
+ }
+
+ layer.x(newTranslate.x);
+ layer.y(newTranslate.y);
+ layer.draw();
+ stageTranslateRef.current = newTranslate;
+
+ setStageScale(1);
+ }
+ }, [map, mapWidth, mapHeight, stageRatio, mapRatio]);
+
+ const bind = useGesture({
+ onWheelStart: ({ event }) => {
+ isInteractingWithCanvas.current =
+ event.target === mapLayerRef.current.getCanvas()._canvas;
+ },
+ onWheel: ({ event }) => {
+ event.persist();
+ const { pixelY } = normalizeWheel(event);
+ if (!isInteractingWithCanvas.current) {
+ return;
+ }
+ const newScale = Math.min(
+ Math.max(stageScale + pixelY * wheelZoomSpeed, minZoom),
+ maxZoom
+ );
+ setStageScale(newScale);
+ },
+ onPinch: ({ da, origin, first }) => {
+ const [distance] = da;
+ const [originX, originY] = origin;
+ if (first) {
+ pinchPreviousDistanceRef.current = distance;
+ pinchPreviousOriginRef.current = { x: originX, y: originY };
+ }
+
+ // Apply scale
+ const distanceDelta = distance - pinchPreviousDistanceRef.current;
+ const originXDelta = originX - pinchPreviousOriginRef.current.x;
+ const originYDelta = originY - pinchPreviousOriginRef.current.y;
+ const newScale = Math.min(
+ Math.max(stageScale + distanceDelta * touchZoomSpeed, minZoom),
+ maxZoom
+ );
+ setStageScale(newScale);
+
+ // Apply translate
+ const stageTranslate = stageTranslateRef.current;
+ const layer = mapLayerRef.current;
+ const newTranslate = {
+ x: stageTranslate.x + originXDelta / newScale,
+ y: stageTranslate.y + originYDelta / newScale,
+ };
+ layer.x(newTranslate.x);
+ layer.y(newTranslate.y);
+ layer.draw();
+ stageTranslateRef.current = newTranslate;
+
+ pinchPreviousDistanceRef.current = distance;
+ pinchPreviousOriginRef.current = { x: originX, y: originY };
+ },
+ onDragStart: ({ event }) => {
+ isInteractingWithCanvas.current =
+ event.target === mapLayerRef.current.getCanvas()._canvas;
+ },
+ onDrag: ({ delta, pinching }) => {
+ if (pinching || !isInteractingWithCanvas.current) {
+ return;
+ }
+
+ const [dx, dy] = delta;
+ const stageTranslate = stageTranslateRef.current;
+ const layer = mapLayerRef.current;
+ const newTranslate = {
+ x: stageTranslate.x + dx / stageScale,
+ y: stageTranslate.y + dy / stageScale,
+ };
+ layer.x(newTranslate.x);
+ layer.y(newTranslate.y);
+ layer.draw();
+ stageTranslateRef.current = newTranslate;
+ },
+ });
+
+ const containerRef = useRef();
+ usePreventOverscroll(containerRef);
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default MapEditor;
diff --git a/src/components/map/MapSettings.js b/src/components/map/MapSettings.js
index f843716..7d0b376 100644
--- a/src/components/map/MapSettings.js
+++ b/src/components/map/MapSettings.js
@@ -81,18 +81,18 @@ function MapSettings({
/>
+
+
+ onSettingsChange("name", e.target.value)}
+ disabled={mapEmpty || map.type === "default"}
+ my={1}
+ />
+
{showMore && (
<>
-
-
- onSettingsChange("name", e.target.value)}
- disabled={mapEmpty || map.type === "default"}
- my={1}
- />
-
0 ||
- mapState.mapDrawActions.length > 0 ||
- mapState.fogDrawActions.length > 0);
-
- const expandButton = (
- {
- e.preventDefault();
- e.stopPropagation();
- setIsTileMenuOpen(true);
- }}
- bg="overlay"
- sx={{ borderRadius: "50%" }}
- m={2}
- >
-
-
- );
-
- function removeButton(map) {
- return (
- {
- e.preventDefault();
- e.stopPropagation();
- setIsTileMenuOpen(false);
- onMapRemove(map.id);
- }}
- bg="overlay"
- sx={{ borderRadius: "50%" }}
- m={2}
- >
-
-
- );
- }
-
- function resetButton(map) {
- return (
- {
- e.preventDefault();
- e.stopPropagation();
- setIsTileMenuOpen(false);
- onMapReset(map.id);
- }}
- bg="overlay"
- sx={{ borderRadius: "50%" }}
- m={2}
- >
-
-
- );
- }
-
return (
- {isDefault && hasMapState && resetButton(map)}
- {!isDefault && hasMapState && !isMapTileMenuOpen && expandButton}
- {!isDefault && !hasMapState && removeButton(map)}
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ onMapEdit(map.id);
+ }}
+ bg="overlay"
+ sx={{ borderRadius: "50%" }}
+ m={2}
+ >
+
+
)}
- {/* Tile menu for two actions */}
- {!isDefault && isMapTileMenuOpen && isSelected && (
- setIsTileMenuOpen(false)}
- >
- {!isDefault && removeButton(map)}
- {hasMapState && resetButton(map)}
-
- )}
);
}
diff --git a/src/components/map/MapTiles.js b/src/components/map/MapTiles.js
index 6bec907..76131fd 100644
--- a/src/components/map/MapTiles.js
+++ b/src/components/map/MapTiles.js
@@ -1,9 +1,11 @@
import React, { useContext } from "react";
-import { Flex, Box, Text } from "theme-ui";
+import { Flex, Box, Text, IconButton, Close } from "theme-ui";
import SimpleBar from "simplebar-react";
import { useMedia } from "react-media";
import AddIcon from "../../icons/AddIcon";
+import RemoveMapIcon from "../../icons/RemoveMapIcon";
+import ResetMapIcon from "../../icons/ResetMapIcon";
import MapTile from "./MapTile";
import Link from "../Link";
@@ -15,19 +17,27 @@ function MapTiles({
selectedMap,
selectedMapState,
onMapSelect,
- onMapAdd,
onMapRemove,
onMapReset,
+ onMapAdd,
+ onMapEdit,
onDone,
}) {
const { databaseStatus } = useContext(DatabaseContext);
const isSmallScreen = useMedia({ query: "(max-width: 500px)" });
+ const hasMapState =
+ selectedMapState &&
+ (Object.values(selectedMapState.tokens).length > 0 ||
+ selectedMapState.mapDrawActions.length > 0 ||
+ selectedMapState.fogDrawActions.length > 0);
+
return (
-
+
@@ -112,6 +118,41 @@ function MapTiles({
)}
+ {selectedMap && (
+
+ onMapSelect(null)}
+ />
+
+ onMapReset(selectedMap.id)}
+ disabled={!hasMapState}
+ >
+
+
+ onMapRemove(selectedMap.id)}
+ >
+
+
+
+
+ )}
);
}
diff --git a/src/icons/EditMapIcon.js b/src/icons/EditMapIcon.js
new file mode 100644
index 0000000..6e43c62
--- /dev/null
+++ b/src/icons/EditMapIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function EditMapIcon() {
+ return (
+
+ );
+}
+
+export default EditMapIcon;
diff --git a/src/modals/EditMapModal.js b/src/modals/EditMapModal.js
new file mode 100644
index 0000000..7341529
--- /dev/null
+++ b/src/modals/EditMapModal.js
@@ -0,0 +1,105 @@
+import React, { useState, useContext } from "react";
+import { Button, Flex, Label } from "theme-ui";
+
+import Modal from "../components/Modal";
+import MapSettings from "../components/map/MapSettings";
+import MapEditor from "../components/map/MapEditor";
+
+import MapDataContext from "../contexts/MapDataContext";
+
+import { isEmpty } from "../helpers/shared";
+
+function SelectMapModal({ isOpen, onDone, map, mapState }) {
+ const { updateMap, updateMapState } = useContext(MapDataContext);
+
+ function handleClose() {
+ onDone();
+ }
+
+ async function handleSave() {
+ await applyMapChanges();
+ onDone();
+ }
+
+ /**
+ * Map settings
+ */
+ // Local cache of map setting changes
+ // Applied when done is clicked or map selection is changed
+ const [mapSettingChanges, setMapSettingChanges] = useState({});
+ const [mapStateSettingChanges, setMapStateSettingChanges] = useState({});
+
+ function handleMapSettingsChange(key, value) {
+ setMapSettingChanges((prevChanges) => ({
+ ...prevChanges,
+ [key]: value,
+ lastModified: Date.now(),
+ }));
+ }
+
+ function handleMapStateSettingsChange(key, value) {
+ setMapStateSettingChanges((prevChanges) => ({
+ ...prevChanges,
+ [key]: value,
+ }));
+ }
+
+ async function applyMapChanges() {
+ if (!isEmpty(mapSettingChanges) || !isEmpty(mapStateSettingChanges)) {
+ // Ensure grid values are positive
+ let verifiedChanges = { ...mapSettingChanges };
+ if ("gridX" in verifiedChanges) {
+ verifiedChanges.gridX = verifiedChanges.gridX || 1;
+ }
+ if ("gridY" in verifiedChanges) {
+ verifiedChanges.gridY = verifiedChanges.gridY || 1;
+ }
+ await updateMap(map.id, verifiedChanges);
+ await updateMapState(map.id, mapStateSettingChanges);
+
+ setMapSettingChanges({});
+ setMapStateSettingChanges({});
+ }
+ }
+
+ const selectedMapWithChanges = map && {
+ ...map,
+ ...mapSettingChanges,
+ };
+ const selectedMapStateWithChanges = mapState && {
+ ...mapState,
+ ...mapStateSettingChanges,
+ };
+
+ const [showMoreSettings, setShowMoreSettings] = useState(false);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+export default SelectMapModal;
diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js
index f05d63a..9169a91 100644
--- a/src/modals/SelectMapModal.js
+++ b/src/modals/SelectMapModal.js
@@ -2,9 +2,10 @@ import React, { useRef, useState, useContext } from "react";
import { Button, Flex, Label } from "theme-ui";
import shortid from "shortid";
+import EditMapModal from "./EditMapModal";
+
import Modal from "../components/Modal";
import MapTiles from "../components/map/MapTiles";
-import MapSettings from "../components/map/MapSettings";
import ImageDrop from "../components/ImageDrop";
import LoadingOverlay from "../components/LoadingOverlay";
@@ -13,7 +14,6 @@ import blobToBuffer from "../helpers/blobToBuffer";
import MapDataContext from "../contexts/MapDataContext";
import AuthContext from "../contexts/AuthContext";
-import { isEmpty } from "../helpers/shared";
import { resizeImage } from "../helpers/image";
const defaultMapSize = 22;
@@ -49,7 +49,6 @@ function SelectMapModal({
removeMap,
resetMap,
updateMap,
- updateMapState,
} = useContext(MapDataContext);
const [imageLoading, setImageLoading] = useState(false);
@@ -62,6 +61,8 @@ function SelectMapModal({
(state) => state.mapId === selectedMapId
);
+ const [isEditModalOpen, setIsEditModalOpen] = useState(false);
+
const fileInputRef = useRef();
async function handleImagesUpload(files) {
@@ -175,8 +176,6 @@ function SelectMapModal({
async function handleMapRemove(id) {
await removeMap(id);
- setMapSettingChanges({});
- setMapStateSettingChanges({});
setSelectedMapId(null);
// Removed the map from the map screen if needed
if (currentMap && currentMap.id === selectedMapId) {
@@ -185,7 +184,6 @@ function SelectMapModal({
}
async function handleMapSelect(map) {
- await applyMapChanges();
if (map) {
setSelectedMapId(map.id);
} else {
@@ -202,9 +200,6 @@ function SelectMapModal({
}
async function handleClose() {
- if (selectedMapId) {
- await applyMapChanges();
- }
onDone();
}
@@ -213,74 +208,16 @@ function SelectMapModal({
return;
}
if (selectedMapId) {
- await applyMapChanges();
// Update last used for cache invalidation
const lastUsed = Date.now();
await updateMap(selectedMapId, { lastUsed });
- onMapChange(
- { ...selectedMapWithChanges, lastUsed },
- selectedMapStateWithChanges
- );
+ onMapChange({ ...selectedMap, lastUsed }, selectedMapState);
} else {
onMapChange(null, null);
}
onDone();
}
- /**
- * Map settings
- */
- const [showMoreSettings, setShowMoreSettings] = useState(false);
- // Local cache of map setting changes
- // Applied when done is clicked or map selection is changed
- const [mapSettingChanges, setMapSettingChanges] = useState({});
- const [mapStateSettingChanges, setMapStateSettingChanges] = useState({});
-
- function handleMapSettingsChange(key, value) {
- setMapSettingChanges((prevChanges) => ({
- ...prevChanges,
- [key]: value,
- lastModified: Date.now(),
- }));
- }
-
- function handleMapStateSettingsChange(key, value) {
- setMapStateSettingChanges((prevChanges) => ({
- ...prevChanges,
- [key]: value,
- }));
- }
-
- async function applyMapChanges() {
- if (
- selectedMapId &&
- (!isEmpty(mapSettingChanges) || !isEmpty(mapStateSettingChanges))
- ) {
- // Ensure grid values are positive
- let verifiedChanges = { ...mapSettingChanges };
- if ("gridX" in verifiedChanges) {
- verifiedChanges.gridX = verifiedChanges.gridX || 1;
- }
- if ("gridY" in verifiedChanges) {
- verifiedChanges.gridY = verifiedChanges.gridY || 1;
- }
- await updateMap(selectedMapId, verifiedChanges);
- await updateMapState(selectedMapId, mapStateSettingChanges);
-
- setMapSettingChanges({});
- setMapStateSettingChanges({});
- }
- }
-
- const selectedMapWithChanges = selectedMap && {
- ...selectedMap,
- ...mapSettingChanges,
- };
- const selectedMapStateWithChanges = selectedMapState && {
- ...selectedMapState,
- ...mapStateSettingChanges,
- };
-
return (
setIsEditModalOpen(true)}
onMapReset={handleMapReset}
+ onMapRemove={handleMapRemove}
+ selectedMap={selectedMap}
+ selectedMapState={selectedMapState}
+ onMapSelect={handleMapSelect}
onDone={handleDone}
/>
-
{imageLoading && }
+ setIsEditModalOpen(false)}
+ map={selectedMap}
+ mapState={selectedMapState}
+ />
);
}