Added UI elements for the new drawing system
Removed old gesture system Refactored map interaction into separate component
This commit is contained in:
@@ -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({
|
||||
<MapDrawing
|
||||
width={map ? map.width : 0}
|
||||
height={map ? map.height : 0}
|
||||
selectedTool={selectedTool}
|
||||
selectedTool={selectedToolId}
|
||||
toolSettings={toolSettings[selectedToolId]}
|
||||
shapes={drawnShapes}
|
||||
onShapeAdd={handleShapeAdd}
|
||||
onShapeRemove={handleShapeRemove}
|
||||
brushColor={brushColor}
|
||||
useGridSnapping={useBrushGridSnapping}
|
||||
gridSize={gridSizeNormalized}
|
||||
useBrushBlending={useBrushBlending}
|
||||
useBrushGesture={useBrushGesture}
|
||||
/>
|
||||
);
|
||||
|
||||
const mapControls = (
|
||||
<MapControls
|
||||
onMapChange={onMapChange}
|
||||
onMapStateChange={onMapStateChange}
|
||||
currentMap={map}
|
||||
onSelectedToolChange={setSelectedToolId}
|
||||
selectedToolId={selectedToolId}
|
||||
toolSettings={toolSettings}
|
||||
onToolSettingChange={handleToolSettingChange}
|
||||
disabledControls={disabledControls}
|
||||
onUndo={onMapDrawUndo}
|
||||
onRedo={onMapDrawRedo}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
className="map"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.1)",
|
||||
userSelect: "none",
|
||||
touchAction: "none",
|
||||
}}
|
||||
bg="background"
|
||||
ref={mapContainerRef}
|
||||
<MapInteraction
|
||||
map={map}
|
||||
aspectRatio={aspectRatio}
|
||||
selectedTool={selectedToolId}
|
||||
toolSettings={toolSettings[selectedToolId]}
|
||||
controls={(allowMapChange || allowDrawing) && mapControls}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
>
|
||||
<Box ref={mapMoveContainerRef}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 0,
|
||||
paddingBottom: `${(1 / aspectRatio) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
{map && mapImage}
|
||||
{map && mapDrawing}
|
||||
{map && mapTokens}
|
||||
</Box>
|
||||
</Box>
|
||||
{(allowMapChange || allowDrawing) && (
|
||||
<MapControls
|
||||
onMapChange={onMapChange}
|
||||
onMapStateChange={onMapStateChange}
|
||||
currentMap={map}
|
||||
onToolChange={setSelectedTool}
|
||||
selectedTool={selectedTool}
|
||||
disabledTools={disabledTools}
|
||||
onUndo={onMapDrawUndo}
|
||||
onRedo={onMapDrawRedo}
|
||||
undoDisabled={!mapState || mapState.drawActionIndex < 0}
|
||||
redoDisabled={
|
||||
!mapState ||
|
||||
mapState.drawActionIndex === mapState.drawActions.length - 1
|
||||
}
|
||||
brushColor={brushColor}
|
||||
onBrushColorChange={setBrushColor}
|
||||
onEraseAll={handleShapeRemoveAll}
|
||||
useBrushGridSnapping={useBrushGridSnapping}
|
||||
onBrushGridSnappingChange={setUseBrushGridSnapping}
|
||||
useBrushBlending={useBrushBlending}
|
||||
onBrushBlendingChange={setUseBrushBlending}
|
||||
useBrushGesture={useBrushGesture}
|
||||
onBrushGestureChange={setUseBrushGesture}
|
||||
allowDrawing={allowDrawing}
|
||||
allowMapChange={allowMapChange}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{map && mapImage}
|
||||
{map && mapDrawing}
|
||||
{map && mapTokens}
|
||||
</MapInteraction>
|
||||
|
||||
{allowTokenChange && (
|
||||
<>
|
||||
<ProxyToken
|
||||
|
||||
@@ -1,207 +1,74 @@
|
||||
import React, { useState, useEffect, useRef, Fragment } from "react";
|
||||
import { Flex, Box, IconButton, Label } from "theme-ui";
|
||||
import React, { useState, Fragment } from "react";
|
||||
import { IconButton, Flex, Box } from "theme-ui";
|
||||
|
||||
import RadioIconButton from "./controls/RadioIconButton";
|
||||
import Divider from "./controls/Divider";
|
||||
|
||||
import SelectMapButton from "./SelectMapButton";
|
||||
import ExpandMoreIcon from "../../icons/ExpandMoreIcon";
|
||||
|
||||
import FogToolSettings from "./controls/FogToolSettings";
|
||||
import BrushToolSettings from "./controls/BrushToolSettings";
|
||||
import ShapeToolSettings from "./controls/ShapeToolSettings";
|
||||
import EraseToolSettings from "./controls/EraseToolSettings";
|
||||
|
||||
import PanToolIcon from "../../icons/PanToolIcon";
|
||||
import FogToolIcon from "../../icons/FogToolIcon";
|
||||
import BrushToolIcon from "../../icons/BrushToolIcon";
|
||||
import ShapeToolIcon from "../../icons/ShapeToolIcon";
|
||||
import EraseToolIcon from "../../icons/EraseToolIcon";
|
||||
import UndoIcon from "../../icons/UndoIcon";
|
||||
import RedoIcon from "../../icons/RedoIcon";
|
||||
import GridOnIcon from "../../icons/GridOnIcon";
|
||||
import GridOffIcon from "../../icons/GridOffIcon";
|
||||
import BlendOnIcon from "../../icons/BlendOnIcon";
|
||||
import BlendOffIcon from "../../icons/BlendOffIcon";
|
||||
import GestureOnIcon from "../../icons/GestureOnIcon";
|
||||
import GestureOffIcon from "../../icons/GestureOffIcon";
|
||||
import ExpandMoreIcon from "../../icons/ExpandMoreIcon";
|
||||
|
||||
import colors, { colorOptions } from "../../helpers/colors";
|
||||
|
||||
import MapMenu from "./MapMenu";
|
||||
import EraseAllIcon from "../../icons/EraseAllIcon";
|
||||
|
||||
function MapControls({
|
||||
function MapContols({
|
||||
onMapChange,
|
||||
onMapStateChange,
|
||||
currentMap,
|
||||
onToolChange,
|
||||
selectedTool,
|
||||
disabledTools,
|
||||
selectedToolId,
|
||||
onSelectedToolChange,
|
||||
toolSettings,
|
||||
onToolSettingChange,
|
||||
disabledControls,
|
||||
onUndo,
|
||||
onRedo,
|
||||
undoDisabled,
|
||||
redoDisabled,
|
||||
brushColor,
|
||||
onBrushColorChange,
|
||||
onEraseAll,
|
||||
useBrushGridSnapping,
|
||||
onBrushGridSnappingChange,
|
||||
useBrushBlending,
|
||||
onBrushBlendingChange,
|
||||
useBrushGesture,
|
||||
onBrushGestureChange,
|
||||
allowDrawing,
|
||||
allowMapChange,
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const subMenus = {
|
||||
brush: (
|
||||
<Box sx={{ width: "104px" }} p={1}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
{colorOptions.map((color) => (
|
||||
<Box
|
||||
key={color}
|
||||
sx={{
|
||||
width: "25%",
|
||||
paddingTop: "25%",
|
||||
borderRadius: "50%",
|
||||
transform: "scale(0.75)",
|
||||
backgroundColor: colors[color],
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => onBrushColorChange(color)}
|
||||
aria-label={`Brush Color ${color}`}
|
||||
>
|
||||
{brushColor === color && (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
border: "2px solid white",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<Flex sx={{ justifyContent: "space-between" }}>
|
||||
<IconButton
|
||||
aria-label={
|
||||
useBrushGridSnapping
|
||||
? "Disable Brush Grid Snapping"
|
||||
: "Enable Brush Grid Snapping"
|
||||
}
|
||||
title={
|
||||
useBrushGridSnapping
|
||||
? "Disable Brush Grid Snapping"
|
||||
: "Enable Brush Grid Snapping"
|
||||
}
|
||||
onClick={() => onBrushGridSnappingChange(!useBrushGridSnapping)}
|
||||
>
|
||||
{useBrushGridSnapping ? <GridOnIcon /> : <GridOffIcon />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label={
|
||||
useBrushBlending
|
||||
? "Disable Brush Blending"
|
||||
: "Enable Brush Blending"
|
||||
}
|
||||
title={
|
||||
useBrushBlending
|
||||
? "Disable Brush Blending"
|
||||
: "Enable Brush Blending"
|
||||
}
|
||||
onClick={() => onBrushBlendingChange(!useBrushBlending)}
|
||||
>
|
||||
{useBrushBlending ? <BlendOnIcon /> : <BlendOffIcon />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label={
|
||||
useBrushGesture
|
||||
? "Disable Gesture Detection"
|
||||
: "Enable Gesture Detection"
|
||||
}
|
||||
title={
|
||||
useBrushGesture
|
||||
? "Disable Gesture Detection"
|
||||
: "Enable Gesture Detection"
|
||||
}
|
||||
onClick={() => onBrushGestureChange(!useBrushGesture)}
|
||||
>
|
||||
{useBrushGesture ? <GestureOnIcon /> : <GestureOffIcon />}
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Box>
|
||||
),
|
||||
erase: (
|
||||
<Box p={1} pr={3}>
|
||||
<Label
|
||||
sx={{
|
||||
fontSize: 1,
|
||||
alignItems: "center",
|
||||
":hover": { color: "primary", cursor: "pointer" },
|
||||
":active": { color: "secondary" },
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
aria-label="Erase All"
|
||||
title="Erase All"
|
||||
onClick={() => {
|
||||
onEraseAll();
|
||||
setCurrentSubmenu(null);
|
||||
setCurrentSubmenuOptions({});
|
||||
}}
|
||||
>
|
||||
<EraseAllIcon />
|
||||
</IconButton>
|
||||
Erase All
|
||||
</Label>
|
||||
</Box>
|
||||
),
|
||||
const toolsById = {
|
||||
pan: {
|
||||
id: "pan",
|
||||
icon: <PanToolIcon />,
|
||||
title: "Pan Tool",
|
||||
},
|
||||
fog: {
|
||||
id: "fog",
|
||||
icon: <FogToolIcon />,
|
||||
title: "Fog Tool",
|
||||
SettingsComponent: FogToolSettings,
|
||||
},
|
||||
brush: {
|
||||
id: "brush",
|
||||
icon: <BrushToolIcon />,
|
||||
title: "Brush Tool",
|
||||
SettingsComponent: BrushToolSettings,
|
||||
},
|
||||
shape: {
|
||||
id: "shape",
|
||||
icon: <ShapeToolIcon />,
|
||||
title: "Shape Tool",
|
||||
SettingsComponent: ShapeToolSettings,
|
||||
},
|
||||
erase: {
|
||||
id: "erase",
|
||||
icon: <EraseToolIcon />,
|
||||
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 = (
|
||||
<Box
|
||||
my={2}
|
||||
bg="text"
|
||||
sx={{ height: "2px", width: "24px", borderRadius: "2px", opacity: 0.5 }}
|
||||
></Box>
|
||||
);
|
||||
|
||||
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) => (
|
||||
<RadioIconButton
|
||||
key={tool}
|
||||
title={toolsById[tool].title}
|
||||
onClick={() => onSelectedToolChange(tool)}
|
||||
isSelected={selectedToolId === tool}
|
||||
disabled={disabledControls.includes(tool)}
|
||||
>
|
||||
{toolsById[tool].icon}
|
||||
</RadioIconButton>
|
||||
)),
|
||||
});
|
||||
sections.push({
|
||||
id: "history",
|
||||
component: (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="Pan Tool"
|
||||
title="Pan Tool"
|
||||
onClick={(e) => handleToolClick(e, "pan")}
|
||||
sx={{ color: selectedTool === "pan" ? "primary" : "text" }}
|
||||
disabled={disabledTools.includes("pan")}
|
||||
>
|
||||
<PanToolIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="Brush Tool"
|
||||
title="Brush Tool"
|
||||
onClick={(e) => handleToolClick(e, "brush")}
|
||||
sx={{ color: selectedTool === "brush" ? "primary" : "text" }}
|
||||
disabled={disabledTools.includes("brush")}
|
||||
>
|
||||
<BrushToolIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="Erase Tool"
|
||||
title="Erase Tool"
|
||||
onClick={(e) => handleToolClick(e, "erase")}
|
||||
sx={{ color: selectedTool === "erase" ? "primary" : "text" }}
|
||||
disabled={disabledTools.includes("erase")}
|
||||
>
|
||||
<EraseToolIcon />
|
||||
</IconButton>
|
||||
{divider}
|
||||
<IconButton
|
||||
aria-label="Undo"
|
||||
title="Undo"
|
||||
onClick={() => onUndo()}
|
||||
disabled={undoDisabled}
|
||||
onClick={onUndo}
|
||||
disabled={disabledControls.includes("undo")}
|
||||
>
|
||||
<UndoIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="Redo"
|
||||
title="Redo"
|
||||
onClick={() => onRedo()}
|
||||
disabled={redoDisabled}
|
||||
onClick={onRedo}
|
||||
disabled={disabledControls.includes("redo")}
|
||||
>
|
||||
<RedoIcon />
|
||||
</IconButton>
|
||||
@@ -298,7 +147,6 @@ function MapControls({
|
||||
>
|
||||
<ExpandMoreIcon />
|
||||
</IconButton>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
@@ -308,12 +156,11 @@ function MapControls({
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
p={2}
|
||||
ref={expanedMenuRef}
|
||||
>
|
||||
{sections.map((section, index) => (
|
||||
<Fragment key={section.id}>
|
||||
{section.component}
|
||||
{index !== sections.length - 1 && divider}
|
||||
{index !== sections.length - 1 && <Divider />}
|
||||
</Fragment>
|
||||
))}
|
||||
</Box>
|
||||
@@ -321,6 +168,34 @@ function MapControls({
|
||||
);
|
||||
}
|
||||
|
||||
function getToolSettings() {
|
||||
const Settings = toolsById[selectedToolId].SettingsComponent;
|
||||
if (Settings) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "4px",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
backgroundColor: "overlay",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
p={1}
|
||||
>
|
||||
<Settings
|
||||
settings={toolSettings[selectedToolId]}
|
||||
onSettingChange={(change) =>
|
||||
onToolSettingChange(selectedToolId, change)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
@@ -335,18 +210,9 @@ function MapControls({
|
||||
>
|
||||
{controls}
|
||||
</Flex>
|
||||
<MapMenu
|
||||
isOpen={!!currentSubmenu}
|
||||
onRequestClose={() => {
|
||||
setCurrentSubmenu(null);
|
||||
setCurrentSubmenuOptions({});
|
||||
}}
|
||||
{...currentSubmenuOptions}
|
||||
>
|
||||
{currentSubmenu && subMenus[currentSubmenu]}
|
||||
</MapMenu>
|
||||
{getToolSettings()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MapControls;
|
||||
export default MapContols;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
152
src/components/map/MapInteraction.js
Normal file
152
src/components/map/MapInteraction.js
Normal file
@@ -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 (
|
||||
<Box
|
||||
className="map"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.1)",
|
||||
userSelect: "none",
|
||||
touchAction: "none",
|
||||
}}
|
||||
bg="background"
|
||||
ref={mapContainerRef}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
>
|
||||
<Box ref={mapMoveContainerRef}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 0,
|
||||
paddingBottom: `${(1 / aspectRatio) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
{controls}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default MapInteraction;
|
||||
19
src/components/map/controls/AlphaBlendToggle.js
Normal file
19
src/components/map/controls/AlphaBlendToggle.js
Normal file
@@ -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 (
|
||||
<IconButton
|
||||
aria-label={useBlending ? "Disable Blending" : "Enable Blending"}
|
||||
title={useBlending ? "Disable Blending" : "Enable Blending"}
|
||||
onClick={() => onBlendingChange(!useBlending)}
|
||||
>
|
||||
{useBlending ? <BlendOnIcon /> : <BlendOffIcon />}
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default AlphaBlendToggle;
|
||||
51
src/components/map/controls/BrushToolSettings.js
Normal file
51
src/components/map/controls/BrushToolSettings.js
Normal file
@@ -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 (
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
<ColorControl
|
||||
color={settings.color}
|
||||
onColorChange={(color) => onSettingChange({ color })}
|
||||
/>
|
||||
<Divider vertical />
|
||||
<RadioIconButton
|
||||
title="Brush Type Stroke"
|
||||
onClick={() => onSettingChange({ type: "stroke" })}
|
||||
isSelected={settings.type === "stroke"}
|
||||
>
|
||||
<BrushStrokeIcon />
|
||||
</RadioIconButton>
|
||||
<RadioIconButton
|
||||
title="Brush Type Fill"
|
||||
onClick={() => onSettingChange({ type: "fill" })}
|
||||
isSelected={settings.type === "fill"}
|
||||
>
|
||||
<BrushFillIcon />
|
||||
</RadioIconButton>
|
||||
<Divider vertical />
|
||||
<AlphaBlendToggle
|
||||
useBlending={settings.useBlending}
|
||||
onBlendingChange={(useBlending) => onSettingChange({ useBlending })}
|
||||
/>
|
||||
<GridSnappingToggle
|
||||
useGridSnapping={settings.useGridSnapping}
|
||||
onGridSnappingChange={(useGridSnapping) =>
|
||||
onSettingChange({ useGridSnapping })
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default BrushToolSettings;
|
||||
107
src/components/map/controls/ColorControl.js
Normal file
107
src/components/map/controls/ColorControl.js
Normal file
@@ -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 (
|
||||
<Box
|
||||
key={color}
|
||||
sx={{
|
||||
borderRadius: "50%",
|
||||
transform: "scale(0.75)",
|
||||
backgroundColor: colors[color],
|
||||
cursor: "pointer",
|
||||
...sx,
|
||||
}}
|
||||
onClick={onClick}
|
||||
aria-label={`Brush Color ${color}`}
|
||||
>
|
||||
{selected && (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
border: "2px solid white",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
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 = (
|
||||
<MapMenu
|
||||
isOpen={showColorMenu}
|
||||
onRequestClose={() => {
|
||||
setShowColorMenu(false);
|
||||
setColorMenuOptions({});
|
||||
}}
|
||||
{...colorMenuOptions}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "104px",
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
p={1}
|
||||
>
|
||||
{colorOptions.map((c) => (
|
||||
<ColorCircle
|
||||
key={c}
|
||||
color={c}
|
||||
selected={c === color}
|
||||
onClick={() => {
|
||||
onColorChange(c);
|
||||
setShowColorMenu(false);
|
||||
setColorMenuOptions({});
|
||||
}}
|
||||
sx={{ width: "25%", paddingTop: "25%" }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</MapMenu>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ColorCircle
|
||||
color={color}
|
||||
selected
|
||||
onClick={handleControlClick}
|
||||
sx={{ width: "24px", height: "24px" }}
|
||||
/>
|
||||
{colorMenu}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ColorControl;
|
||||
24
src/components/map/controls/Divider.js
Normal file
24
src/components/map/controls/Divider.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import { Divider } from "theme-ui";
|
||||
|
||||
function StyledDivider({ vertical }) {
|
||||
return (
|
||||
<Divider
|
||||
my={vertical ? 0 : 2}
|
||||
mx={vertical ? 2 : 0}
|
||||
bg="text"
|
||||
sx={{
|
||||
height: vertical ? "24px" : "2px",
|
||||
width: vertical ? "2px" : "24px",
|
||||
borderRadius: "2px",
|
||||
opacity: 0.5,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
StyledDivider.defaultProps = {
|
||||
vertical: false,
|
||||
};
|
||||
|
||||
export default StyledDivider;
|
||||
21
src/components/map/controls/EdgeSnappingToggle.js
Normal file
21
src/components/map/controls/EdgeSnappingToggle.js
Normal file
@@ -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 (
|
||||
<IconButton
|
||||
aria-label={
|
||||
useEdgeSnapping ? "Disable Edge Snapping" : "Enable Edge Snapping"
|
||||
}
|
||||
title={useEdgeSnapping ? "Disable Edge Snapping" : "Enable Edge Snapping"}
|
||||
onClick={() => onEdgeSnappingChange(!useEdgeSnapping)}
|
||||
>
|
||||
{useEdgeSnapping ? <SnappingOnIcon /> : <SnappingOffIcon />}
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default EdgeSnappingToggle;
|
||||
16
src/components/map/controls/EraseToolSettings.js
Normal file
16
src/components/map/controls/EraseToolSettings.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { Flex, IconButton } from "theme-ui";
|
||||
|
||||
import EraseAllIcon from "../../../icons/EraseAllIcon";
|
||||
|
||||
function EraseToolSettings({ onEraseAll }) {
|
||||
return (
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
<IconButton aria-label="Erase All" title="Erase All" onClick={onEraseAll}>
|
||||
<EraseAllIcon />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default EraseToolSettings;
|
||||
55
src/components/map/controls/FogToolSettings.js
Normal file
55
src/components/map/controls/FogToolSettings.js
Normal file
@@ -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 (
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
<RadioIconButton
|
||||
title="Add Fog"
|
||||
onClick={() => onSettingChange({ type: "add" })}
|
||||
isSelected={settings.type === "add"}
|
||||
>
|
||||
<FogAddIcon />
|
||||
</RadioIconButton>
|
||||
<RadioIconButton
|
||||
title="Remove Fog"
|
||||
onClick={() => onSettingChange({ type: "remove" })}
|
||||
isSelected={settings.type === "remove"}
|
||||
>
|
||||
<FogRemoveIcon />
|
||||
</RadioIconButton>
|
||||
<RadioIconButton
|
||||
title="Toggle Fog"
|
||||
onClick={() => onSettingChange({ type: "toggle" })}
|
||||
isSelected={settings.type === "toggle"}
|
||||
>
|
||||
<FogToggleIcon />
|
||||
</RadioIconButton>
|
||||
<Divider vertical />
|
||||
<GridSnappingToggle
|
||||
useGridSnapping={settings.useGridSnapping}
|
||||
onGridSnappingChange={(useGridSnapping) =>
|
||||
onSettingChange({ useGridSnapping })
|
||||
}
|
||||
/>
|
||||
<EdgeSnappingToggle
|
||||
useEdgeSnapping={settings.useEdgeSnapping}
|
||||
onEdgeSnappingChange={(useEdgeSnapping) =>
|
||||
onSettingChange({ useEdgeSnapping })
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default BrushToolSettings;
|
||||
21
src/components/map/controls/GridSnappingToggle.js
Normal file
21
src/components/map/controls/GridSnappingToggle.js
Normal file
@@ -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 (
|
||||
<IconButton
|
||||
aria-label={
|
||||
useGridSnapping ? "Disable Grid Snapping" : "Enable Grid Snapping"
|
||||
}
|
||||
title={useGridSnapping ? "Disable Grid Snapping" : "Enable Grid Snapping"}
|
||||
onClick={() => onGridSnappingChange(!useGridSnapping)}
|
||||
>
|
||||
{useGridSnapping ? <GridOnIcon /> : <GridOffIcon />}
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default GridSnappingToggle;
|
||||
22
src/components/map/controls/RadioIconButton.js
Normal file
22
src/components/map/controls/RadioIconButton.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { IconButton } from "theme-ui";
|
||||
|
||||
function RadioIconButton({ title, onClick, isSelected, disabled, children }) {
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={title}
|
||||
title={title}
|
||||
onClick={onClick}
|
||||
sx={{ color: isSelected ? "primary" : "text" }}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
RadioIconButton.defaultProps = {
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export default RadioIconButton;
|
||||
59
src/components/map/controls/ShapeToolSettings.js
Normal file
59
src/components/map/controls/ShapeToolSettings.js
Normal file
@@ -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 (
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
<ColorControl
|
||||
color={settings.color}
|
||||
onColorChange={(color) => onSettingChange({ color })}
|
||||
/>
|
||||
<Divider vertical />
|
||||
<RadioIconButton
|
||||
title="Shape Type Rectangle"
|
||||
onClick={() => onSettingChange({ type: "rectangle" })}
|
||||
isSelected={settings.type === "rectangle"}
|
||||
>
|
||||
<ShapeRectangleIcon />
|
||||
</RadioIconButton>
|
||||
<RadioIconButton
|
||||
title="Shape Type Circle"
|
||||
onClick={() => onSettingChange({ type: "circle" })}
|
||||
isSelected={settings.type === "cricle"}
|
||||
>
|
||||
<ShapeTriangleIcon />
|
||||
</RadioIconButton>
|
||||
<RadioIconButton
|
||||
title="Shape Type Triangle"
|
||||
onClick={() => onSettingChange({ type: "triangle" })}
|
||||
isSelected={settings.type === "triangle"}
|
||||
>
|
||||
<ShapeCircleIcon />
|
||||
</RadioIconButton>
|
||||
<Divider vertical />
|
||||
<AlphaBlendToggle
|
||||
useBlending={settings.useBlending}
|
||||
onBlendingChange={(useBlending) => onSettingChange({ useBlending })}
|
||||
/>
|
||||
<GridSnappingToggle
|
||||
useGridSnapping={settings.useGridSnapping}
|
||||
onGridSnappingChange={(useGridSnapping) =>
|
||||
onSettingChange({ useGridSnapping })
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default ShapeToolSettings;
|
||||
Reference in New Issue
Block a user