diff --git a/package.json b/package.json
index c904c22..3f72d07 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,6 @@
"react-modal": "^3.11.2",
"react-router-dom": "^5.1.2",
"react-scripts": "3.4.0",
- "shape-detector": "^0.2.1",
"shortid": "^2.2.15",
"simple-peer": "^9.6.2",
"simplebar-react": "^2.1.0",
diff --git a/src/components/map/Map.js b/src/components/map/Map.js
index edc1d01..b81d7cb 100644
--- a/src/components/map/Map.js
+++ b/src/components/map/Map.js
@@ -1,6 +1,5 @@
import React, { useRef, useEffect, useState } from "react";
import { Box, Image } from "theme-ui";
-import interact from "interactjs";
import ProxyToken from "../token/ProxyToken";
import TokenMenu from "../token/TokenMenu";
@@ -10,13 +9,12 @@ import MapControls from "./MapControls";
import { omit } from "../../helpers/shared";
import useDataSource from "../../helpers/useDataSource";
+import MapInteraction from "./MapInteraction";
+
import { mapSources as defaultMapSources } from "../../maps";
const mapTokenProxyClassName = "map-token__proxy";
const mapTokenMenuClassName = "map-token__menu";
-const zoomSpeed = -0.005;
-const minZoom = 0.1;
-const maxZoom = 5;
function Map({
map,
@@ -49,11 +47,31 @@ function Map({
* Map drawing
*/
- const [selectedTool, setSelectedTool] = useState("pan");
- const [brushColor, setBrushColor] = useState("black");
- const [useBrushGridSnapping, setUseBrushGridSnapping] = useState(false);
- const [useBrushBlending, setUseBrushBlending] = useState(false);
- const [useBrushGesture, setUseBrushGesture] = useState(false);
+ const [selectedToolId, setSelectedToolId] = useState("pan");
+ const [toolSettings, setToolSettings] = useState({
+ fog: { type: "add", useGridSnapping: false, useEdgeSnapping: true },
+ brush: {
+ color: "darkGray",
+ type: "stroke",
+ useBlending: false,
+ useGridSnapping: false,
+ },
+ shape: {
+ color: "red",
+ type: "rectangle",
+ useBlending: true,
+ useGridSnapping: true,
+ },
+ });
+ function handleToolSettingChange(tool, change) {
+ setToolSettings((prevSettings) => ({
+ ...prevSettings,
+ [tool]: {
+ ...prevSettings[tool],
+ ...change,
+ },
+ }));
+ }
const [drawnShapes, setDrawnShapes] = useState([]);
function handleShapeAdd(shape) {
@@ -88,121 +106,36 @@ function Map({
setDrawnShapes(Object.values(shapesById));
}, [mapState]);
- const disabledTools = [];
+ const disabledControls = [];
+ if (!allowMapChange) {
+ disabledControls.push("map");
+ }
+ if (!allowDrawing) {
+ disabledControls.push("drawing");
+ }
if (!map) {
- disabledTools.push("pan");
- disabledTools.push("brush");
+ disabledControls.push("pan");
+ disabledControls.push("brush");
}
if (drawnShapes.length === 0) {
- disabledTools.push("erase");
+ disabledControls.push("erase");
}
-
- /**
- * Map movement
- */
-
- const mapTranslateRef = useRef({ x: 0, y: 0 });
- const mapScaleRef = useRef(1);
- const mapMoveContainerRef = useRef();
- function setTranslateAndScale(newTranslate, newScale) {
- const moveContainer = mapMoveContainerRef.current;
- moveContainer.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px) scale(${newScale})`;
- mapScaleRef.current = newScale;
- mapTranslateRef.current = newTranslate;
+ if (!mapState || mapState.drawActionIndex < 0) {
+ disabledControls.push("undo");
+ }
+ if (
+ !mapState ||
+ mapState.drawActionIndex === mapState.drawActions.length - 1
+ ) {
+ disabledControls.push("redo");
}
-
- 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 (selectedTool === "pan" || isGesture) {
- newTranslate = {
- x: translate.x + event.dx,
- y: translate.y + event.dy,
- };
- }
- 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 selectedTool === "pan" && map ? "move" : "default";
- },
- })
- .on("doubletap", (event) => {
- event.preventDefault();
- if (selectedTool === "pan") {
- setTranslateAndScale({ x: 0, y: 0 }, 1);
- }
- });
-
- return () => {
- mapInteract.unset();
- };
- }, [selectedTool, 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();
-
- const scale = mapScaleRef.current;
- const translate = mapTranslateRef.current;
-
- const deltaY = event.deltaY * 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);
- }
- };
- }, []);
/**
* Member setup
*/
const mapRef = useRef(null);
- const mapContainerRef = useRef();
+
const gridX = map && map.gridX;
const gridY = map && map.gridY;
const gridSizeNormalized = { x: 1 / gridX || 0, y: 1 / gridY || 0 };
@@ -260,83 +193,43 @@ function Map({
);
+ const mapControls = (
+
+ );
return (
<>
-
-
-
-
- {map && mapImage}
- {map && mapDrawing}
- {map && mapTokens}
-
-
- {(allowMapChange || allowDrawing) && (
-
- )}
-
+ {map && mapImage}
+ {map && mapDrawing}
+ {map && mapTokens}
+
+
{allowTokenChange && (
<>
-
- {colorOptions.map((color) => (
- onBrushColorChange(color)}
- aria-label={`Brush Color ${color}`}
- >
- {brushColor === color && (
-
- )}
-
- ))}
-
-
- onBrushGridSnappingChange(!useBrushGridSnapping)}
- >
- {useBrushGridSnapping ? : }
-
- onBrushBlendingChange(!useBrushBlending)}
- >
- {useBrushBlending ? : }
-
- onBrushGestureChange(!useBrushGesture)}
- >
- {useBrushGesture ? : }
-
-
-
- ),
- erase: (
-
-
-
- ),
+ const toolsById = {
+ pan: {
+ id: "pan",
+ icon: ,
+ title: "Pan Tool",
+ },
+ fog: {
+ id: "fog",
+ icon: ,
+ title: "Fog Tool",
+ SettingsComponent: FogToolSettings,
+ },
+ brush: {
+ id: "brush",
+ icon: ,
+ title: "Brush Tool",
+ SettingsComponent: BrushToolSettings,
+ },
+ shape: {
+ id: "shape",
+ icon: ,
+ title: "Shape Tool",
+ SettingsComponent: ShapeToolSettings,
+ },
+ erase: {
+ id: "erase",
+ icon: ,
+ title: "Erase tool",
+ SettingsComponent: EraseToolSettings,
+ },
};
-
- const [currentSubmenu, setCurrentSubmenu] = useState(null);
- const [currentSubmenuOptions, setCurrentSubmenuOptions] = useState({});
-
- function handleToolClick(event, tool) {
- if (tool !== selectedTool) {
- onToolChange(tool);
- } else if (currentSubmenu) {
- setCurrentSubmenu(null);
- setCurrentSubmenuOptions({});
- } else if (subMenus[tool]) {
- const toolRect = event.target.getBoundingClientRect();
- setCurrentSubmenu(tool);
- setCurrentSubmenuOptions({
- // Align the right of the submenu to the left of the tool and center vertically
- left: `${toolRect.left - 16}px`,
- top: `${toolRect.bottom - toolRect.height / 2}px`,
- style: { transform: "translate(-100%, -50%)" },
- // Exclude this node from the sub menus auto close
- excludeNode: event.target,
- });
- }
- }
-
- // Detect when a tool becomes disabled and switch to to the pan tool
- useEffect(() => {
- if (disabledTools.includes(selectedTool)) {
- onToolChange("pan");
- }
- }, [selectedTool, disabledTools, onToolChange]);
-
- const divider = (
-
- );
-
- const expanedMenuRef = useRef();
+ const tools = ["pan", "fog", "brush", "shape", "erase"];
const sections = [];
- if (allowMapChange) {
+ if (!disabledControls.includes("map")) {
sections.push({
id: "map",
component: (
@@ -213,52 +80,34 @@ function MapControls({
),
});
}
- if (allowDrawing) {
+ if (!disabledControls.includes("drawing")) {
sections.push({
id: "drawing",
+ component: tools.map((tool) => (
+ onSelectedToolChange(tool)}
+ isSelected={selectedToolId === tool}
+ disabled={disabledControls.includes(tool)}
+ >
+ {toolsById[tool].icon}
+
+ )),
+ });
+ sections.push({
+ id: "history",
component: (
<>
handleToolClick(e, "pan")}
- sx={{ color: selectedTool === "pan" ? "primary" : "text" }}
- disabled={disabledTools.includes("pan")}
- >
-
-
- handleToolClick(e, "brush")}
- sx={{ color: selectedTool === "brush" ? "primary" : "text" }}
- disabled={disabledTools.includes("brush")}
- >
-
-
- handleToolClick(e, "erase")}
- sx={{ color: selectedTool === "erase" ? "primary" : "text" }}
- disabled={disabledTools.includes("erase")}
- >
-
-
- {divider}
- onUndo()}
- disabled={undoDisabled}
+ onClick={onUndo}
+ disabled={disabledControls.includes("undo")}
>
onRedo()}
- disabled={redoDisabled}
+ onClick={onRedo}
+ disabled={disabledControls.includes("redo")}
>
@@ -298,7 +147,6 @@ function MapControls({
>
-
{sections.map((section, index) => (
{section.component}
- {index !== sections.length - 1 && divider}
+ {index !== sections.length - 1 && }
))}
@@ -321,6 +168,34 @@ function MapControls({
);
}
+ function getToolSettings() {
+ const Settings = toolsById[selectedToolId].SettingsComponent;
+ if (Settings) {
+ return (
+
+
+ onToolSettingChange(selectedToolId, change)
+ }
+ />
+
+ );
+ } else {
+ return null;
+ }
+ }
+
return (
<>
{controls}
- {
- setCurrentSubmenu(null);
- setCurrentSubmenuOptions({});
- }}
- {...currentSubmenuOptions}
- >
- {currentSubmenu && subMenus[currentSubmenu]}
-
+ {getToolSettings()}
>
);
}
-export default MapControls;
+export default MapContols;
diff --git a/src/components/map/MapDrawing.js b/src/components/map/MapDrawing.js
index 3b26ee2..86be753 100644
--- a/src/components/map/MapDrawing.js
+++ b/src/components/map/MapDrawing.js
@@ -5,24 +5,23 @@ import shortid from "shortid";
import colors from "../../helpers/colors";
import { snapPositionToGrid } from "../../helpers/shared";
-import { pointsToGesture, gestureToData } from "../../helpers/gestures";
-
function MapDrawing({
width,
height,
selectedTool,
+ toolSettings,
shapes,
onShapeAdd,
onShapeRemove,
- brushColor,
- useGridSnapping,
gridSize,
- useBrushBlending,
- useBrushGesture,
}) {
const canvasRef = useRef();
const containerRef = useRef();
+ const toolColor = toolSettings && toolSettings.color;
+ const useToolBlending = toolSettings && toolSettings.useBlending;
+ const useGridSnapping = toolSettings && toolSettings.useGridSnapping;
+
const [brushPoints, setBrushPoints] = useState([]);
const [isDrawing, setIsDrawing] = useState(false);
const [pointerPosition, setPointerPosition] = useState({ x: -1, y: -1 });
@@ -91,22 +90,16 @@ function MapDrawing({
if (selectedTool === "brush") {
if (brushPoints.length > 1) {
const simplifiedPoints = simplify(brushPoints, 0.001);
- const type = useBrushGesture
- ? pointsToGesture(simplifiedPoints)
- : "path";
+ const type = "path";
if (type !== null) {
- const data =
- type === "path"
- ? { points: simplifiedPoints }
- : gestureToData(simplifiedPoints, type);
-
+ const data = { points: simplifiedPoints };
onShapeAdd({
type,
data,
id: shortid.generate(),
- color: brushColor,
- blend: useBrushBlending,
+ color: toolColor,
+ blend: useToolBlending,
});
}
@@ -188,7 +181,7 @@ function MapDrawing({
}
if (selectedTool === "brush" && brushPoints.length > 0) {
const path = pointsToPath(brushPoints);
- drawPath(path, colors[brushColor], useBrushBlending, context);
+ drawPath(path, colors[toolColor], useToolBlending, context);
}
if (hoveredShape) {
const path = shapeToPath(hoveredShape);
@@ -204,9 +197,8 @@ function MapDrawing({
isDrawing,
selectedTool,
brushPoints,
- brushColor,
- useBrushGesture,
- useBrushBlending,
+ toolColor,
+ useToolBlending,
]);
return (
diff --git a/src/components/map/MapInteraction.js b/src/components/map/MapInteraction.js
new file mode 100644
index 0000000..0ab1644
--- /dev/null
+++ b/src/components/map/MapInteraction.js
@@ -0,0 +1,152 @@
+import React, { useRef, useEffect } from "react";
+import { Box } from "theme-ui";
+import interact from "interactjs";
+
+const zoomSpeed = -0.005;
+const minZoom = 0.1;
+const maxZoom = 5;
+
+function MapInteraction({
+ map,
+ aspectRatio,
+ selectedTool,
+ selectedToolSettings,
+ children,
+ controls,
+}) {
+ const mapContainerRef = useRef();
+ const mapMoveContainerRef = useRef();
+ const mapTranslateRef = useRef({ x: 0, y: 0 });
+ const mapScaleRef = useRef(1);
+ function setTranslateAndScale(newTranslate, newScale) {
+ const moveContainer = mapMoveContainerRef.current;
+ moveContainer.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px) scale(${newScale})`;
+ mapScaleRef.current = newScale;
+ mapTranslateRef.current = newTranslate;
+ }
+
+ 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 (selectedTool === "pan" || isGesture) {
+ newTranslate = {
+ x: translate.x + event.dx,
+ y: translate.y + event.dy,
+ };
+ }
+ 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 selectedTool === "pan" && map ? "move" : "default";
+ },
+ })
+ .on("doubletap", (event) => {
+ event.preventDefault();
+ if (selectedTool === "pan") {
+ setTranslateAndScale({ x: 0, y: 0 }, 1);
+ }
+ });
+
+ return () => {
+ mapInteract.unset();
+ };
+ }, [selectedTool, 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();
+
+ const scale = mapScaleRef.current;
+ const translate = mapTranslateRef.current;
+
+ const deltaY = event.deltaY * 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);
+ }
+ };
+ }, []);
+
+ return (
+
+
+
+
+ {children}
+
+
+ {controls}
+
+ );
+}
+
+export default MapInteraction;
diff --git a/src/components/map/controls/AlphaBlendToggle.js b/src/components/map/controls/AlphaBlendToggle.js
new file mode 100644
index 0000000..ccc2065
--- /dev/null
+++ b/src/components/map/controls/AlphaBlendToggle.js
@@ -0,0 +1,19 @@
+import React from "react";
+import { IconButton } from "theme-ui";
+
+import BlendOnIcon from "../../../icons/BlendOnIcon";
+import BlendOffIcon from "../../../icons/BlendOffIcon";
+
+function AlphaBlendToggle({ useBlending, onBlendingChange }) {
+ return (
+ onBlendingChange(!useBlending)}
+ >
+ {useBlending ? : }
+
+ );
+}
+
+export default AlphaBlendToggle;
diff --git a/src/components/map/controls/BrushToolSettings.js b/src/components/map/controls/BrushToolSettings.js
new file mode 100644
index 0000000..95996f4
--- /dev/null
+++ b/src/components/map/controls/BrushToolSettings.js
@@ -0,0 +1,51 @@
+import React from "react";
+import { Flex } from "theme-ui";
+
+import ColorControl from "./ColorControl";
+import AlphaBlendToggle from "./AlphaBlendToggle";
+import GridSnappingToggle from "./GridSnappingToggle";
+import RadioIconButton from "./RadioIconButton";
+
+import BrushStrokeIcon from "../../../icons/BrushStrokeIcon";
+import BrushFillIcon from "../../../icons/BrushFillIcon";
+
+import Divider from "./Divider";
+
+function BrushToolSettings({ settings, onSettingChange }) {
+ return (
+
+ onSettingChange({ color })}
+ />
+
+ onSettingChange({ type: "stroke" })}
+ isSelected={settings.type === "stroke"}
+ >
+
+
+ onSettingChange({ type: "fill" })}
+ isSelected={settings.type === "fill"}
+ >
+
+
+
+ onSettingChange({ useBlending })}
+ />
+
+ onSettingChange({ useGridSnapping })
+ }
+ />
+
+ );
+}
+
+export default BrushToolSettings;
diff --git a/src/components/map/controls/ColorControl.js b/src/components/map/controls/ColorControl.js
new file mode 100644
index 0000000..d9264f1
--- /dev/null
+++ b/src/components/map/controls/ColorControl.js
@@ -0,0 +1,107 @@
+import React, { useState } from "react";
+import { Box } from "theme-ui";
+
+import colors, { colorOptions } from "../../../helpers/colors";
+import MapMenu from "../MapMenu";
+
+function ColorCircle({ color, selected, onClick, sx }) {
+ return (
+
+ {selected && (
+
+ )}
+
+ );
+}
+
+function ColorControl({ color, onColorChange }) {
+ const [showColorMenu, setShowColorMenu] = useState(false);
+ const [colorMenuOptions, setColorMenuOptions] = useState({});
+
+ function handleControlClick(event) {
+ if (showColorMenu) {
+ setShowColorMenu(false);
+ setColorMenuOptions({});
+ } else {
+ setShowColorMenu(true);
+ const rect = event.target.getBoundingClientRect();
+ setColorMenuOptions({
+ // Align the right of the submenu to the left of the tool and center vertically
+ left: `${rect.left + rect.width / 2}px`,
+ top: `${rect.bottom + 16}px`,
+ style: { transform: "translateX(-50%)" },
+ // Exclude this node from the sub menus auto close
+ excludeNode: event.target,
+ });
+ }
+ }
+
+ const colorMenu = (
+ {
+ setShowColorMenu(false);
+ setColorMenuOptions({});
+ }}
+ {...colorMenuOptions}
+ >
+
+ {colorOptions.map((c) => (
+ {
+ onColorChange(c);
+ setShowColorMenu(false);
+ setColorMenuOptions({});
+ }}
+ sx={{ width: "25%", paddingTop: "25%" }}
+ />
+ ))}
+
+
+ );
+
+ return (
+ <>
+
+ {colorMenu}
+ >
+ );
+}
+
+export default ColorControl;
diff --git a/src/components/map/controls/Divider.js b/src/components/map/controls/Divider.js
new file mode 100644
index 0000000..485e2d6
--- /dev/null
+++ b/src/components/map/controls/Divider.js
@@ -0,0 +1,24 @@
+import React from "react";
+import { Divider } from "theme-ui";
+
+function StyledDivider({ vertical }) {
+ return (
+
+ );
+}
+
+StyledDivider.defaultProps = {
+ vertical: false,
+};
+
+export default StyledDivider;
diff --git a/src/components/map/controls/EdgeSnappingToggle.js b/src/components/map/controls/EdgeSnappingToggle.js
new file mode 100644
index 0000000..addd219
--- /dev/null
+++ b/src/components/map/controls/EdgeSnappingToggle.js
@@ -0,0 +1,21 @@
+import React from "react";
+import { IconButton } from "theme-ui";
+
+import SnappingOnIcon from "../../../icons/SnappingOnIcon";
+import SnappingOffIcon from "../../../icons/SnappingOffIcon";
+
+function EdgeSnappingToggle({ useEdgeSnapping, onEdgeSnappingChange }) {
+ return (
+ onEdgeSnappingChange(!useEdgeSnapping)}
+ >
+ {useEdgeSnapping ? : }
+
+ );
+}
+
+export default EdgeSnappingToggle;
diff --git a/src/components/map/controls/EraseToolSettings.js b/src/components/map/controls/EraseToolSettings.js
new file mode 100644
index 0000000..f418bed
--- /dev/null
+++ b/src/components/map/controls/EraseToolSettings.js
@@ -0,0 +1,16 @@
+import React from "react";
+import { Flex, IconButton } from "theme-ui";
+
+import EraseAllIcon from "../../../icons/EraseAllIcon";
+
+function EraseToolSettings({ onEraseAll }) {
+ return (
+
+
+
+
+
+ );
+}
+
+export default EraseToolSettings;
diff --git a/src/components/map/controls/FogToolSettings.js b/src/components/map/controls/FogToolSettings.js
new file mode 100644
index 0000000..ecb6a79
--- /dev/null
+++ b/src/components/map/controls/FogToolSettings.js
@@ -0,0 +1,55 @@
+import React from "react";
+import { Flex } from "theme-ui";
+
+import GridSnappingToggle from "./GridSnappingToggle";
+import EdgeSnappingToggle from "./EdgeSnappingToggle";
+import RadioIconButton from "./RadioIconButton";
+
+import FogAddIcon from "../../../icons/FogAddIcon";
+import FogRemoveIcon from "../../../icons/FogRemoveIcon";
+import FogToggleIcon from "../../../icons/FogToggleIcon";
+
+import Divider from "./Divider";
+
+function BrushToolSettings({ settings, onSettingChange }) {
+ return (
+
+ onSettingChange({ type: "add" })}
+ isSelected={settings.type === "add"}
+ >
+
+
+ onSettingChange({ type: "remove" })}
+ isSelected={settings.type === "remove"}
+ >
+
+
+ onSettingChange({ type: "toggle" })}
+ isSelected={settings.type === "toggle"}
+ >
+
+
+
+
+ onSettingChange({ useGridSnapping })
+ }
+ />
+
+ onSettingChange({ useEdgeSnapping })
+ }
+ />
+
+ );
+}
+
+export default BrushToolSettings;
diff --git a/src/components/map/controls/GridSnappingToggle.js b/src/components/map/controls/GridSnappingToggle.js
new file mode 100644
index 0000000..b09232a
--- /dev/null
+++ b/src/components/map/controls/GridSnappingToggle.js
@@ -0,0 +1,21 @@
+import React from "react";
+import { IconButton } from "theme-ui";
+
+import GridOnIcon from "../../../icons/GridOnIcon";
+import GridOffIcon from "../../../icons/GridOffIcon";
+
+function GridSnappingToggle({ useGridSnapping, onGridSnappingChange }) {
+ return (
+ onGridSnappingChange(!useGridSnapping)}
+ >
+ {useGridSnapping ? : }
+
+ );
+}
+
+export default GridSnappingToggle;
diff --git a/src/components/map/controls/RadioIconButton.js b/src/components/map/controls/RadioIconButton.js
new file mode 100644
index 0000000..14579bc
--- /dev/null
+++ b/src/components/map/controls/RadioIconButton.js
@@ -0,0 +1,22 @@
+import React from "react";
+import { IconButton } from "theme-ui";
+
+function RadioIconButton({ title, onClick, isSelected, disabled, children }) {
+ return (
+
+ {children}
+
+ );
+}
+
+RadioIconButton.defaultProps = {
+ disabled: false,
+};
+
+export default RadioIconButton;
diff --git a/src/components/map/controls/ShapeToolSettings.js b/src/components/map/controls/ShapeToolSettings.js
new file mode 100644
index 0000000..386331c
--- /dev/null
+++ b/src/components/map/controls/ShapeToolSettings.js
@@ -0,0 +1,59 @@
+import React from "react";
+import { Flex } from "theme-ui";
+
+import ColorControl from "./ColorControl";
+import AlphaBlendToggle from "./AlphaBlendToggle";
+import GridSnappingToggle from "./GridSnappingToggle";
+import RadioIconButton from "./RadioIconButton";
+
+import ShapeRectangleIcon from "../../../icons/ShapeRectangleIcon";
+import ShapeCircleIcon from "../../../icons/ShapeCircleIcon";
+import ShapeTriangleIcon from "../../../icons/ShapeTriangleIcon";
+
+import Divider from "./Divider";
+
+function ShapeToolSettings({ settings, onSettingChange }) {
+ return (
+
+ onSettingChange({ color })}
+ />
+
+ onSettingChange({ type: "rectangle" })}
+ isSelected={settings.type === "rectangle"}
+ >
+
+
+ onSettingChange({ type: "circle" })}
+ isSelected={settings.type === "cricle"}
+ >
+
+
+ onSettingChange({ type: "triangle" })}
+ isSelected={settings.type === "triangle"}
+ >
+
+
+
+ onSettingChange({ useBlending })}
+ />
+
+ onSettingChange({ useGridSnapping })
+ }
+ />
+
+ );
+}
+
+export default ShapeToolSettings;
diff --git a/src/helpers/gestures.js b/src/helpers/gestures.js
deleted file mode 100644
index 9efe6fb..0000000
--- a/src/helpers/gestures.js
+++ /dev/null
@@ -1,98 +0,0 @@
-import ShapeDetector from "shape-detector";
-import simplify from "simplify-js";
-import { normalize, subtract, dot, length } from "./vector2";
-
-import gestures from "./gesturesData";
-
-const detector = new ShapeDetector(gestures);
-
-export function pointsToGesture(points) {
- return detector.spot(points).pattern;
-}
-
-export function getBounds(points) {
- let minX = Number.MAX_VALUE;
- let maxX = Number.MIN_VALUE;
- let minY = Number.MAX_VALUE;
- let maxY = Number.MIN_VALUE;
- for (let point of points) {
- minX = point.x < minX ? point.x : minX;
- maxX = point.x > maxX ? point.x : maxX;
- minY = point.y < minY ? point.y : minY;
- maxY = point.y > maxY ? point.y : maxY;
- }
- return { minX, maxX, minY, maxY };
-}
-
-function getTrianglePoints(points) {
- if (points.length < 3) {
- return points;
- }
-
- // Simplify edges up to the average distance between points
- let perimeterDistance = 0;
- for (let i = 0; i < points.length - 1; i++) {
- perimeterDistance += length(subtract(points[i + 1], points[i]));
- }
- const averagePointDistance = perimeterDistance / points.length;
- const simplifiedPoints = simplify(points, averagePointDistance);
-
- const edges = [];
- // Find edges of the simplified points that have the highest angular change
- for (let i = 0; i < simplifiedPoints.length; i++) {
- // Ensure index loops over to start and end of erray
- const prevIndex = i - 1 < 0 ? simplifiedPoints.length - i - 1 : i - 1;
- const nextIndex = (i + 1) % simplifiedPoints.length;
-
- const prev = normalize(
- subtract(simplifiedPoints[i], simplifiedPoints[prevIndex])
- );
- const next = normalize(
- subtract(simplifiedPoints[nextIndex], simplifiedPoints[i])
- );
-
- const similarity = dot(prev, next);
- if (similarity < 0.25) {
- edges.push({ similarity, point: simplifiedPoints[i] });
- }
- }
-
- edges.sort((a, b) => a.similarity - b.similarity);
- const trianglePoints = edges.slice(0, 3).map((edge) => edge.point);
- // Return the points with the highest angular change or fallback to a heuristic
- if (trianglePoints.length === 3) {
- return trianglePoints;
- } else {
- return [
- { x: points[0].x, y: points[0].y },
- {
- x: points[Math.floor(points.length / 2)].x,
- y: points[Math.floor(points.length / 2)].y,
- },
- { x: points[points.length - 1].x, y: points[points.length - 1].y },
- ];
- }
-}
-
-export function gestureToData(points, gesture) {
- const bounds = getBounds(points);
- const width = bounds.maxX - bounds.minX;
- const height = bounds.maxY - bounds.minY;
- const maxSide = width > height ? width : height;
- switch (gesture) {
- case "rectangle":
- return { x: bounds.minX, y: bounds.minY, width, height };
- case "triangle":
- return {
- points: getTrianglePoints(points),
- };
- case "circle":
- return {
- x: bounds.minX + width / 2,
- y: bounds.minY + height / 2,
- radius: maxSide / 2,
- };
- default:
- throw Error("Gesture not implemented");
- }
-}
diff --git a/src/icons/BrushFillIcon.js b/src/icons/BrushFillIcon.js
new file mode 100644
index 0000000..b0776df
--- /dev/null
+++ b/src/icons/BrushFillIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function BrushFillIcon() {
+ return (
+
+ );
+}
+
+export default BrushFillIcon;
diff --git a/src/icons/BrushStrokeIcon.js b/src/icons/BrushStrokeIcon.js
new file mode 100644
index 0000000..8b1d9ff
--- /dev/null
+++ b/src/icons/BrushStrokeIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function BrushStrokeIcon() {
+ return (
+
+ );
+}
+
+export default BrushStrokeIcon;
diff --git a/src/icons/FogAddIcon.js b/src/icons/FogAddIcon.js
new file mode 100644
index 0000000..aa21d79
--- /dev/null
+++ b/src/icons/FogAddIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function FogAddIcon() {
+ return (
+
+ );
+}
+
+export default FogAddIcon;
diff --git a/src/icons/FogRemoveIcon.js b/src/icons/FogRemoveIcon.js
new file mode 100644
index 0000000..af6c2e6
--- /dev/null
+++ b/src/icons/FogRemoveIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function FogRemoveIcon() {
+ return (
+
+ );
+}
+
+export default FogRemoveIcon;
diff --git a/src/icons/FogToggleIcon.js b/src/icons/FogToggleIcon.js
new file mode 100644
index 0000000..569288b
--- /dev/null
+++ b/src/icons/FogToggleIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function FogToggleIcon() {
+ return (
+
+ );
+}
+
+export default FogToggleIcon;
diff --git a/src/icons/FogToolIcon.js b/src/icons/FogToolIcon.js
new file mode 100644
index 0000000..d81bd89
--- /dev/null
+++ b/src/icons/FogToolIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function FogToolIcon() {
+ return (
+
+ );
+}
+
+export default FogToolIcon;
diff --git a/src/icons/ShapeCircleIcon.js b/src/icons/ShapeCircleIcon.js
new file mode 100644
index 0000000..c393782
--- /dev/null
+++ b/src/icons/ShapeCircleIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function ShapeCircleIcon() {
+ return (
+
+ );
+}
+
+export default ShapeCircleIcon;
diff --git a/src/icons/ShapeRectangleIcon.js b/src/icons/ShapeRectangleIcon.js
new file mode 100644
index 0000000..55d00ee
--- /dev/null
+++ b/src/icons/ShapeRectangleIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function ShapeRectangleIcon() {
+ return (
+
+ );
+}
+
+export default ShapeRectangleIcon;
diff --git a/src/icons/ShapeToolIcon.js b/src/icons/ShapeToolIcon.js
new file mode 100644
index 0000000..aa4398f
--- /dev/null
+++ b/src/icons/ShapeToolIcon.js
@@ -0,0 +1,20 @@
+import React from "react";
+
+function ShapeToolIcon() {
+ return (
+
+ );
+}
+
+export default ShapeToolIcon;
diff --git a/src/icons/ShapeTriangleIcon.js b/src/icons/ShapeTriangleIcon.js
new file mode 100644
index 0000000..86a29d2
--- /dev/null
+++ b/src/icons/ShapeTriangleIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function ShapeTriangleIcon() {
+ return (
+
+ );
+}
+
+export default ShapeTriangleIcon;
diff --git a/src/icons/SnappingOffIcon.js b/src/icons/SnappingOffIcon.js
new file mode 100644
index 0000000..a51c22b
--- /dev/null
+++ b/src/icons/SnappingOffIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function SnappingOffIcon() {
+ return (
+
+ );
+}
+
+export default SnappingOffIcon;
diff --git a/src/icons/SnappingOnIcon.js b/src/icons/SnappingOnIcon.js
new file mode 100644
index 0000000..355831c
--- /dev/null
+++ b/src/icons/SnappingOnIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function SnappingOnIcon() {
+ return (
+
+ );
+}
+
+export default SnappingOnIcon;
diff --git a/yarn.lock b/yarn.lock
index 4e843e1..2a3a068 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9837,11 +9837,6 @@ shallow-clone@^3.0.0:
dependencies:
kind-of "^6.0.2"
-shape-detector@^0.2.1:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/shape-detector/-/shape-detector-0.2.1.tgz#d69acf8a5f595100fee08b2d69d6b5c74d887e1e"
- integrity sha1-1prPil9ZUQD+4Istada1x02Ifh4=
-
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"