Added fog polygon tool and changed fog interaction method

This commit is contained in:
Mitchell McCaffrey 2020-06-19 18:04:58 +10:00
parent 5a93d9a526
commit aa4ba33a0b
11 changed files with 319 additions and 175 deletions

View File

@ -48,7 +48,7 @@ function Map({
const [selectedToolId, setSelectedToolId] = useState("pan"); const [selectedToolId, setSelectedToolId] = useState("pan");
const [toolSettings, setToolSettings] = useState({ const [toolSettings, setToolSettings] = useState({
fog: { type: "add", useEdgeSnapping: true, useGridSnapping: false }, fog: { type: "polygon", useEdgeSnapping: false, useFogCut: false },
brush: { brush: {
color: "darkGray", color: "darkGray",
type: "stroke", type: "stroke",

View File

@ -159,6 +159,7 @@ function MapDrawing({
] ]
); );
// Move away from this as it is too slow to respond
useMapBrush(isEditing, handleShapeDraw); useMapBrush(isEditing, handleShapeDraw);
function handleShapeOver(shape, isDown) { function handleShapeOver(shape, isDown) {

View File

@ -1,4 +1,4 @@
import React, { useContext, useState, useCallback } from "react"; import React, { useContext, useState, useEffect } from "react";
import shortid from "shortid"; import shortid from "shortid";
import { Group } from "react-konva"; import { Group } from "react-konva";
import useImage from "use-image"; import useImage from "use-image";
@ -6,6 +6,7 @@ import useImage from "use-image";
import diagonalPattern from "../../images/DiagonalPattern.png"; import diagonalPattern from "../../images/DiagonalPattern.png";
import MapInteractionContext from "../../contexts/MapInteractionContext"; import MapInteractionContext from "../../contexts/MapInteractionContext";
import MapStageContext from "../../contexts/MapStageContext";
import { compare as comparePoints } from "../../helpers/vector2"; import { compare as comparePoints } from "../../helpers/vector2";
import { import {
@ -14,8 +15,10 @@ import {
getStrokeWidth, getStrokeWidth,
} from "../../helpers/drawing"; } from "../../helpers/drawing";
import colors from "../../helpers/colors"; import colors from "../../helpers/colors";
import useMapBrush from "../../helpers/useMapBrush"; import {
import { HoleyLine } from "../../helpers/konva"; HoleyLine,
getRelativePointerPositionNormalized,
} from "../../helpers/konva";
function MapFog({ function MapFog({
shapes, shapes,
@ -28,6 +31,7 @@ function MapFog({
gridSize, gridSize,
}) { }) {
const { stageScale, mapWidth, mapHeight } = useContext(MapInteractionContext); const { stageScale, mapWidth, mapHeight } = useContext(MapInteractionContext);
const mapStageRef = useContext(MapStageContext);
const [drawingShape, setDrawingShape] = useState(null); const [drawingShape, setDrawingShape] = useState(null);
const [isBrushDown, setIsBrushDown] = useState(false); const [isBrushDown, setIsBrushDown] = useState(false);
const [editingShapes, setEditingShapes] = useState([]); const [editingShapes, setEditingShapes] = useState([]);
@ -40,149 +44,209 @@ function MapFog({
const [patternImage] = useImage(diagonalPattern); const [patternImage] = useImage(diagonalPattern);
const handleBrushUp = useCallback(() => { useEffect(() => {
setIsBrushDown(false); if (!isEditing) {
if (editingShapes.length > 0) { return;
if (selectedToolSettings.type === "remove") {
onShapesRemove(editingShapes.map((shape) => shape.id));
} else if (selectedToolSettings.type === "toggle") {
onShapesEdit(
editingShapes.map((shape) => ({ ...shape, visible: !shape.visible }))
);
}
setEditingShapes([]);
} }
}, [editingShapes, onShapesRemove, onShapesEdit, selectedToolSettings]);
const handleShapeDraw = useCallback( const mapStage = mapStageRef.current;
(brushState, mapBrushPosition) => {
function startShape() { function getBrushPosition() {
const brushPosition = getBrushPositionForTool( const mapImage = mapStage.findOne("#mapImage");
mapBrushPosition, return getBrushPositionForTool(
selectedToolId, getRelativePointerPositionNormalized(mapImage),
selectedToolSettings, selectedToolId,
gridSize, selectedToolSettings,
shapes gridSize,
); shapes
if ( );
selectedToolSettings.type === "add" || }
selectedToolSettings.type === "subtract"
) { function handleBrushDown() {
setDrawingShape({ const brushPosition = getBrushPosition();
type: "fog", if (selectedToolSettings.type === "brush") {
data: { points: [brushPosition], holes: [] }, setDrawingShape({
strokeWidth: 0.5, type: "fog",
color: selectedToolSettings.type === "add" ? "black" : "red", data: {
blend: false, points: [brushPosition],
id: shortid.generate(), holes: [],
visible: true, },
}); strokeWidth: 0.5,
} color: selectedToolSettings.useFogSubtract ? "red" : "black",
setIsBrushDown(true); blend: false,
id: shortid.generate(),
visible: true,
});
} }
setIsBrushDown(true);
}
function continueShape() { function handleBrushMove() {
const brushPosition = getBrushPositionForTool( if (
mapBrushPosition, selectedToolSettings.type === "brush" &&
selectedToolId, isBrushDown &&
selectedToolSettings, drawingShape
gridSize, ) {
shapes const brushPosition = getBrushPosition();
); setDrawingShape((prevShape) => {
if ( const prevPoints = prevShape.data.points;
selectedToolSettings.type === "add" || if (
selectedToolSettings.type === "subtract" comparePoints(
) { prevPoints[prevPoints.length - 1],
setDrawingShape((prevShape) => { brushPosition,
const prevPoints = prevShape.data.points; 0.001
if ( )
comparePoints( ) {
prevPoints[prevPoints.length - 1], return prevShape;
brushPosition, }
0.001 return {
) ...prevShape,
) { data: {
return prevShape; ...prevShape.data,
} points: [...prevPoints, brushPosition],
return { },
...prevShape, };
data: { });
...prevShape.data,
points: [...prevPoints, brushPosition],
},
};
});
}
} }
if (selectedToolSettings.type === "polygon" && drawingShape) {
const brushPosition = getBrushPosition();
setDrawingShape((prevShape) => {
return {
...prevShape,
data: {
...prevShape.data,
points: [...prevShape.data.points.slice(0, -1), brushPosition],
},
};
});
}
}
function endShape() { function handleBrushUp() {
if (selectedToolSettings.type === "add" && drawingShape) { if (selectedToolSettings.type === "brush" && drawingShape) {
if (drawingShape.data.points.length > 1) { const subtract = selectedToolSettings.useFogSubtract;
const shape = {
...drawingShape, if (drawingShape.data.points.length > 1) {
data: { let shapeData = {};
...drawingShape.data, if (subtract) {
points: simplifyPoints( shapeData = { id: drawingShape.id, type: drawingShape.type };
drawingShape.data.points, } else {
gridSize, shapeData = drawingShape;
// Downscale fog as smoothing doesn't currently work with edge snapping }
stageScale / 2 const shape = {
), ...shapeData,
}, data: {
}; ...drawingShape.data,
points: simplifyPoints(
drawingShape.data.points,
gridSize,
// Downscale fog as smoothing doesn't currently work with edge snapping
stageScale / 2
),
},
};
if (subtract) {
onShapeSubtract(shape);
} else {
onShapeAdd(shape); onShapeAdd(shape);
} }
} }
if (selectedToolSettings.type === "subtract" && drawingShape) {
if (drawingShape.data.points.length > 1) {
const shape = {
data: {
...drawingShape.data,
points: simplifyPoints(
drawingShape.data.points,
gridSize,
// Downscale fog as smoothing doesn't currently work with edge snapping
stageScale / 2
),
},
id: drawingShape.id,
type: drawingShape.type,
};
onShapeSubtract(shape);
}
}
setDrawingShape(null); setDrawingShape(null);
handleBrushUp();
} }
switch (brushState) { if (selectedToolSettings.type === "polygon") {
case "first": const brushPosition = getBrushPosition();
startShape(); setDrawingShape({
return; type: "fog",
case "drawing": data: {
continueShape(); points: [
return; ...(drawingShape ? drawingShape.data.points : [brushPosition]),
case "last": brushPosition,
endShape(); ],
return; holes: [],
default: },
return; strokeWidth: 0.5,
color: selectedToolSettings.useFogSubtract ? "red" : "black",
blend: false,
id: shortid.generate(),
visible: true,
});
} }
},
[
selectedToolId,
selectedToolSettings,
gridSize,
stageScale,
onShapeAdd,
onShapeSubtract,
shapes,
drawingShape,
handleBrushUp,
]
);
useMapBrush(isEditing, handleShapeDraw); if (editingShapes.length > 0) {
if (selectedToolSettings.type === "remove") {
onShapesRemove(editingShapes.map((shape) => shape.id));
} else if (selectedToolSettings.type === "toggle") {
onShapesEdit(
editingShapes.map((shape) => ({
...shape,
visible: !shape.visible,
}))
);
}
setEditingShapes([]);
}
setIsBrushDown(false);
}
function handleKeyDown(event) {
if (
event.key === "Enter" &&
selectedToolSettings.type === "polygon" &&
drawingShape
) {
const subtract = selectedToolSettings.useFogSubtract;
const data = {
...drawingShape.data,
// Remove the last point as it hasn't been placed yet
points: drawingShape.data.points.slice(0, -1),
};
if (subtract) {
onShapeSubtract({
id: drawingShape.id,
type: drawingShape.type,
data: data,
});
} else {
onShapeAdd({ ...drawingShape, data: data });
}
setDrawingShape(null);
}
if (event.key === "Escape" && drawingShape) {
setDrawingShape(null);
}
}
mapStage.on("mousedown", handleBrushDown);
mapStage.on("mousemove", handleBrushMove);
mapStage.on("mouseup", handleBrushUp);
mapStage.container().addEventListener("keydown", handleKeyDown);
return () => {
mapStage.off("mousedown", handleBrushDown);
mapStage.off("mousemove", handleBrushMove);
mapStage.off("mouseup", handleBrushUp);
mapStage.container().removeEventListener("keydown", handleKeyDown);
};
}, [
mapStageRef,
isEditing,
drawingShape,
editingShapes,
gridSize,
isBrushDown,
onShapeAdd,
onShapeSubtract,
onShapesEdit,
onShapesRemove,
selectedToolId,
selectedToolSettings,
shapes,
stageScale,
]);
function handleShapeOver(shape, isDown) { function handleShapeOver(shape, isDown) {
if (shouldHover && isDown) { if (shouldHover && isDown) {

View File

@ -11,7 +11,9 @@ import useDataSource from "../../helpers/useDataSource";
import { mapSources as defaultMapSources } from "../../maps"; import { mapSources as defaultMapSources } from "../../maps";
import { MapInteractionProvider } from "../../contexts/MapInteractionContext"; import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
import MapStageContext from "../../contexts/MapStageContext"; import MapStageContext, {
MapStageProvider,
} from "../../contexts/MapStageContext";
import AuthContext from "../../contexts/AuthContext"; import AuthContext from "../../contexts/AuthContext";
const wheelZoomSpeed = -0.001; const wheelZoomSpeed = -0.001;
@ -201,6 +203,14 @@ function MapInteraction({ map, children, controls, selectedToolId }) {
mapDragPositionRef, mapDragPositionRef,
}; };
// Enable keyboard interaction for map stage container
useEffect(() => {
const container = mapStageRef.current.container();
container.tabIndex = 1;
container.style.outline = "none";
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return ( return (
<Box <Box
sx={{ sx={{
@ -234,7 +244,9 @@ function MapInteraction({ map, children, controls, selectedToolId }) {
{/* Forward auth context to konva elements */} {/* Forward auth context to konva elements */}
<AuthContext.Provider value={auth}> <AuthContext.Provider value={auth}>
<MapInteractionProvider value={mapInteraction}> <MapInteractionProvider value={mapInteraction}>
{children} <MapStageProvider value={mapStageRef}>
{children}
</MapStageProvider>
</MapInteractionProvider> </MapInteractionProvider>
</AuthContext.Provider> </AuthContext.Provider>
</Layer> </Layer>

View File

@ -0,0 +1,19 @@
import React from "react";
import { IconButton } from "theme-ui";
import FogAddIcon from "../../../icons/FogAddIcon";
import FogSubtractIcon from "../../../icons/FogSubtractIcon";
function FogSubtractToggle({ useFogSubtract, onFogSubtractChange }) {
return (
<IconButton
aria-label={useFogSubtract ? "Add Fog" : "Subtract Fog"}
title={useFogSubtract ? "Add Fog" : "Subtract Fog"}
onClick={() => onFogSubtractChange(!useFogSubtract)}
>
{useFogSubtract ? <FogSubtractIcon /> : <FogAddIcon />}
</IconButton>
);
}
export default FogSubtractToggle;

View File

@ -3,10 +3,10 @@ import { Flex } from "theme-ui";
import EdgeSnappingToggle from "./EdgeSnappingToggle"; import EdgeSnappingToggle from "./EdgeSnappingToggle";
import RadioIconButton from "./RadioIconButton"; import RadioIconButton from "./RadioIconButton";
import GridSnappingToggle from "./GridSnappingToggle"; import FogSubtractToggle from "./FogSubtractToggle";
import FogAddIcon from "../../../icons/FogAddIcon"; import FogBrushIcon from "../../../icons/FogBrushIcon";
import FogSubtractIcon from "../../../icons/FogSubtractIcon"; import FogPolygonIcon from "../../../icons/FogPolygonIcon";
import FogRemoveIcon from "../../../icons/FogRemoveIcon"; import FogRemoveIcon from "../../../icons/FogRemoveIcon";
import FogToggleIcon from "../../../icons/FogToggleIcon"; import FogToggleIcon from "../../../icons/FogToggleIcon";
@ -24,18 +24,40 @@ function BrushToolSettings({
return ( return (
<Flex sx={{ alignItems: "center" }}> <Flex sx={{ alignItems: "center" }}>
<RadioIconButton <RadioIconButton
title="Add Fog" title="Fog Polygon"
onClick={() => onSettingChange({ type: "add" })} onClick={() => onSettingChange({ type: "polygon" })}
isSelected={settings.type === "add"} isSelected={settings.type === "polygon"}
> >
<FogAddIcon /> <FogPolygonIcon />
</RadioIconButton> </RadioIconButton>
<RadioIconButton <RadioIconButton
title="Subtract Fog" title="Fog Brush"
onClick={() => onSettingChange({ type: "subtract" })} onClick={() => onSettingChange({ type: "brush" })}
isSelected={settings.type === "subtract"} isSelected={settings.type === "brush"}
> >
<FogSubtractIcon /> <FogBrushIcon />
</RadioIconButton>
<Divider vertical />
<FogSubtractToggle
useFogSubtract={settings.useFogSubtract}
onFogSubtractChange={(useFogSubtract) =>
onSettingChange({ useFogSubtract })
}
/>
{/* TODO: Re-enable edge snapping when holes are fixed */}
{/* <EdgeSnappingToggle
useEdgeSnapping={settings.useEdgeSnapping}
onEdgeSnappingChange={(useEdgeSnapping) =>
onSettingChange({ useEdgeSnapping })
}
/> */}
<Divider vertical />
<RadioIconButton
title="Toggle Fog"
onClick={() => onSettingChange({ type: "toggle" })}
isSelected={settings.type === "toggle"}
>
<FogToggleIcon />
</RadioIconButton> </RadioIconButton>
<RadioIconButton <RadioIconButton
title="Remove Fog" title="Remove Fog"
@ -44,26 +66,6 @@ function BrushToolSettings({
> >
<FogRemoveIcon /> <FogRemoveIcon />
</RadioIconButton> </RadioIconButton>
<RadioIconButton
title="Toggle Fog"
onClick={() => onSettingChange({ type: "toggle" })}
isSelected={settings.type === "toggle"}
>
<FogToggleIcon />
</RadioIconButton>
<Divider vertical />
<EdgeSnappingToggle
useEdgeSnapping={settings.useEdgeSnapping}
onEdgeSnappingChange={(useEdgeSnapping) =>
onSettingChange({ useEdgeSnapping })
}
/>
<GridSnappingToggle
useGridSnapping={settings.useGridSnapping}
onGridSnappingChange={(useGridSnapping) =>
onSettingChange({ useGridSnapping })
}
/>
<Divider vertical /> <Divider vertical />
<UndoButton <UndoButton
onClick={() => onToolAction("fogUndo")} onClick={() => onToolAction("fogUndo")}

View File

@ -105,3 +105,18 @@ export function HoleyLine({ holes, ...props }) {
return <Line sceneFunc={sceneFunc} {...props} />; return <Line sceneFunc={sceneFunc} {...props} />;
} }
export function getRelativePointerPosition(node) {
let transform = node.getAbsoluteTransform().copy();
transform.invert();
let posision = node.getStage().getPointerPosition();
return transform.point(posision);
}
export function getRelativePointerPositionNormalized(node) {
const relativePosition = getRelativePointerPosition(node);
return {
x: relativePosition.x / node.width(),
y: relativePosition.y / node.height(),
};
}

View File

@ -7,15 +7,10 @@ function EraseToolIcon() {
height="24" height="24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="currentcolor"
> >
<g fill="none" fillRule="evenodd"> <path d="M0 0h24v24H0z" fill="none" />
<path d="M0 0h24v24H0z" /> <path d="M3.212 12.303c-1 1-1.182 2.455-.404 3.233l5.656 5.656c.778.778 2.233.596 3.233-.404l9.091-9.091c1-1 1.182-2.455.404-3.233l-5.656-5.656c-.778-.778-2.233-.596-3.233.404l-9.091 9.091zm6.667-2.424l4.242 4.242c.39.39.485.93.212 1.202l-3.96 3.96c-.272.272-.813.177-1.201-.212l-4.243-4.243c-.389-.388-.484-.93-.212-1.202l3.96-3.96c.272-.272.813-.176 1.202.213z" />
<path
d="M3.212 12.303c-1 1-1.182 2.455-.404 3.233l5.656 5.656c.778.778 2.233.596 3.233-.404l9.091-9.091c1-1 1.182-2.455.404-3.233l-5.656-5.656c-.778-.778-2.233-.596-3.233.404l-9.091 9.091zm6.667-2.424l4.242 4.242c.39.39.485.93.212 1.202l-3.96 3.96c-.272.272-.813.177-1.201-.212l-4.243-4.243c-.389-.388-.484-.93-.212-1.202l3.96-3.96c.272-.272.813-.176 1.202.213z"
fill="currentcolor"
fillRule="nonzero"
/>
</g>
</svg> </svg>
); );
} }

18
src/icons/FogBrushIcon.js Normal file
View File

@ -0,0 +1,18 @@
import React from "react";
function FogBrushIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 0 24 24"
width="24"
fill="currentcolor"
>
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M18 4V3c0-.55-.45-1-1-1H5c-.55 0-1 .45-1 1v4c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V6h1v4h-9c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-9h7c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1h-2z" />
</svg>
);
}
export default FogBrushIcon;

View File

@ -0,0 +1,18 @@
import React from "react";
function FogPolygonIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 0 24 24"
width="24"
fill="currentcolor"
>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M15.858 20a1 1 0 110 2h-8a1 1 0 010-2h8zm-5-17.998v9.516a2 2 0 102 0V2.12l5.725 10.312a1 1 0 01.049.87l-.058.117-3.093 5.333a1 1 0 01-.865.498H8.941a1 1 0 01-.875-.516L5.125 13.41a1 1 0 01-.002-.965l5.735-10.443z" />
</svg>
);
}
export default FogPolygonIcon;

View File

@ -10,7 +10,7 @@ function FogRemoveIcon() {
fill="currentcolor" fill="currentcolor"
> >
<path d="M0 0h24v24H0z" fill="none" /> <path d="M0 0h24v24H0z" fill="none" />
<path d="M19.35 10.04A7.49 7.49 0 0012 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 000 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zm-3.814.839L13.414 13l2.122 2.121a1.003 1.003 0 010 1.415 1.003 1.003 0 01-1.415 0L12 14.414l-2.121 2.122a1.003 1.003 0 01-1.415 0 1.003 1.003 0 010-1.415L10.586 13l-2.122-2.121a1.003 1.003 0 010-1.415 1.003 1.003 0 011.415 0L12 11.586l2.121-2.122a1.003 1.003 0 011.415 0 1.003 1.003 0 010 1.415z" /> <path d="M3.212 12.303c-1 1-1.182 2.455-.404 3.233l5.656 5.656c.778.778 2.233.596 3.233-.404l9.091-9.091c1-1 1.182-2.455.404-3.233l-5.656-5.656c-.778-.778-2.233-.596-3.233.404l-9.091 9.091zm6.667-2.424l4.242 4.242c.39.39.485.93.212 1.202l-3.96 3.96c-.272.272-.813.177-1.201-.212l-4.243-4.243c-.389-.388-.484-.93-.212-1.202l3.96-3.96c.272-.272.813-.176 1.202.213z" />
</svg> </svg>
); );
} }