Added UI elements for the new drawing system

Removed old gesture system
Refactored map interaction into separate component
This commit is contained in:
Mitchell McCaffrey
2020-04-27 17:29:46 +10:00
parent 3112890fd3
commit 2cf93ab77f
29 changed files with 952 additions and 540 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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 (

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;