From 2cf93ab77ff3bde58d049d463147976f54f2a118 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Mon, 27 Apr 2020 17:29:46 +1000 Subject: [PATCH] Added UI elements for the new drawing system Removed old gesture system Refactored map interaction into separate component --- package.json | 1 - src/components/map/Map.js | 253 ++++--------- src/components/map/MapControls.js | 338 ++++++------------ src/components/map/MapDrawing.js | 32 +- src/components/map/MapInteraction.js | 152 ++++++++ .../map/controls/AlphaBlendToggle.js | 19 + .../map/controls/BrushToolSettings.js | 51 +++ src/components/map/controls/ColorControl.js | 107 ++++++ src/components/map/controls/Divider.js | 24 ++ .../map/controls/EdgeSnappingToggle.js | 21 ++ .../map/controls/EraseToolSettings.js | 16 + .../map/controls/FogToolSettings.js | 55 +++ .../map/controls/GridSnappingToggle.js | 21 ++ .../map/controls/RadioIconButton.js | 22 ++ .../map/controls/ShapeToolSettings.js | 59 +++ src/helpers/gestures.js | 98 ----- src/icons/BrushFillIcon.js | 18 + src/icons/BrushStrokeIcon.js | 18 + src/icons/FogAddIcon.js | 18 + src/icons/FogRemoveIcon.js | 18 + src/icons/FogToggleIcon.js | 18 + src/icons/FogToolIcon.js | 18 + src/icons/ShapeCircleIcon.js | 18 + src/icons/ShapeRectangleIcon.js | 18 + src/icons/ShapeToolIcon.js | 20 ++ src/icons/ShapeTriangleIcon.js | 18 + src/icons/SnappingOffIcon.js | 18 + src/icons/SnappingOnIcon.js | 18 + yarn.lock | 5 - 29 files changed, 952 insertions(+), 540 deletions(-) create mode 100644 src/components/map/MapInteraction.js create mode 100644 src/components/map/controls/AlphaBlendToggle.js create mode 100644 src/components/map/controls/BrushToolSettings.js create mode 100644 src/components/map/controls/ColorControl.js create mode 100644 src/components/map/controls/Divider.js create mode 100644 src/components/map/controls/EdgeSnappingToggle.js create mode 100644 src/components/map/controls/EraseToolSettings.js create mode 100644 src/components/map/controls/FogToolSettings.js create mode 100644 src/components/map/controls/GridSnappingToggle.js create mode 100644 src/components/map/controls/RadioIconButton.js create mode 100644 src/components/map/controls/ShapeToolSettings.js delete mode 100644 src/helpers/gestures.js create mode 100644 src/icons/BrushFillIcon.js create mode 100644 src/icons/BrushStrokeIcon.js create mode 100644 src/icons/FogAddIcon.js create mode 100644 src/icons/FogRemoveIcon.js create mode 100644 src/icons/FogToggleIcon.js create mode 100644 src/icons/FogToolIcon.js create mode 100644 src/icons/ShapeCircleIcon.js create mode 100644 src/icons/ShapeRectangleIcon.js create mode 100644 src/icons/ShapeToolIcon.js create mode 100644 src/icons/ShapeTriangleIcon.js create mode 100644 src/icons/SnappingOffIcon.js create mode 100644 src/icons/SnappingOnIcon.js 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"