diff --git a/src/components/map/Map.js b/src/components/map/Map.js index 2db34db..cdb7aca 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -333,7 +333,7 @@ function Map({ active={selectedToolId === "fog"} toolSettings={settings.fog} gridSize={gridSizeNormalized} - transparent={allowFogDrawing && !settings.fog.preview} + editable={allowFogDrawing && !settings.fog.preview} /> ); diff --git a/src/components/map/MapFog.js b/src/components/map/MapFog.js index e76cb37..9c346d0 100644 --- a/src/components/map/MapFog.js +++ b/src/components/map/MapFog.js @@ -1,6 +1,12 @@ -import React, { useContext, useState, useEffect, useCallback } from "react"; +import React, { + useContext, + useState, + useEffect, + useCallback, + useRef, +} from "react"; import shortid from "shortid"; -import { Group } from "react-konva"; +import { Group, Rect } from "react-konva"; import useImage from "use-image"; import diagonalPattern from "../../images/DiagonalPattern.png"; @@ -13,6 +19,7 @@ import { getBrushPositionForTool, simplifyPoints, getStrokeWidth, + mergeShapes, } from "../../helpers/drawing"; import colors from "../../helpers/colors"; import { @@ -21,6 +28,7 @@ import { Tick, } from "../../helpers/konva"; import useKeyboard from "../../helpers/useKeyboard"; +import useDebounce from "../../helpers/useDebounce"; function MapFog({ map, @@ -32,7 +40,7 @@ function MapFog({ active, toolSettings, gridSize, - transparent, + editable, }) { const { stageScale, mapWidth, mapHeight, interactionEmitter } = useContext( MapInteractionContext @@ -44,12 +52,13 @@ function MapFog({ const shouldHover = active && + editable && (toolSettings.type === "toggle" || toolSettings.type === "remove"); const [patternImage] = useImage(diagonalPattern); useEffect(() => { - if (!active) { + if (!active || !editable) { return; } @@ -80,7 +89,6 @@ function MapFog({ }, strokeWidth: 0.5, color: toolSettings.useFogCut ? "red" : "black", - blend: false, id: shortid.generate(), visible: true, }); @@ -99,7 +107,6 @@ function MapFog({ }, strokeWidth: 0.5, color: toolSettings.useFogCut ? "red" : "black", - blend: false, id: shortid.generate(), visible: true, }); @@ -210,7 +217,6 @@ function MapFog({ }, strokeWidth: 0.5, color: toolSettings.useFogCut ? "red" : "black", - blend: false, id: shortid.generate(), visible: true, }; @@ -355,14 +361,16 @@ function MapFog({ mapWidth, mapHeight )} - visible={(active && !toolSettings.preview) || shape.visible} - opacity={transparent ? 0.5 : 1} + opacity={editable ? 0.5 : 1} fillPatternImage={patternImage} fillPriority={active && !shape.visible ? "pattern" : "color"} holes={holes} // Disable collision if the fog is transparent and we're not editing it // This allows tokens to be moved under the fog - hitFunc={transparent && !active ? () => {} : undefined} + hitFunc={editable && !active ? () => {} : undefined} + shadowColor={editable ? "rgba(0, 0, 0, 0)" : "rgba(0, 0, 0, 0.33)"} + shadowOffset={{ x: 0, y: 5 }} + shadowBlur={10} /> ); } @@ -398,9 +406,41 @@ function MapFog({ ); } + const [fogShapes, setFogShapes] = useState(shapes); + useEffect(() => { + function shapeVisible(shape) { + return (active && !toolSettings.preview) || shape.visible; + } + + if (editable) { + setFogShapes(shapes.filter(shapeVisible)); + } else { + setFogShapes(mergeShapes(shapes)); + } + }, [shapes, editable, active, toolSettings]); + + const fogGroupRef = useRef(); + const debouncedStageScale = useDebounce(stageScale, 50); + + useEffect(() => { + const fogGroup = fogGroupRef.current; + + const canvas = fogGroup.getChildren()[0].getCanvas(); + const pixelRatio = canvas.pixelRatio || 1; + + fogGroup.cache({ + pixelRatio: Math.min(debouncedStageScale * pixelRatio, 20), + }); + fogGroup.getLayer().draw(); + }, [fogShapes, editable, active, debouncedStageScale, mapWidth]); + return ( - {shapes.map(renderShape)} + + {/* Render a blank shape so cache works with no fog shapes */} + + {fogShapes.map(renderShape)} + {drawingShape && renderShape(drawingShape)} {drawingShape && toolSettings && diff --git a/src/components/map/controls/EdgeSnappingToggle.js b/src/components/map/controls/EdgeSnappingToggle.js index f04bedf..44f7a10 100644 --- a/src/components/map/controls/EdgeSnappingToggle.js +++ b/src/components/map/controls/EdgeSnappingToggle.js @@ -4,7 +4,11 @@ import { IconButton } from "theme-ui"; import SnappingOnIcon from "../../../icons/SnappingOnIcon"; import SnappingOffIcon from "../../../icons/SnappingOffIcon"; -function EdgeSnappingToggle({ useEdgeSnapping, onEdgeSnappingChange }) { +function EdgeSnappingToggle({ + useEdgeSnapping, + onEdgeSnappingChange, + disabled, +}) { return ( onEdgeSnappingChange(!useEdgeSnapping)} + disabled={disabled} > {useEdgeSnapping ? : } diff --git a/src/components/map/controls/FogCutToggle.js b/src/components/map/controls/FogCutToggle.js index 34c3726..d401e59 100644 --- a/src/components/map/controls/FogCutToggle.js +++ b/src/components/map/controls/FogCutToggle.js @@ -4,7 +4,7 @@ import { IconButton } from "theme-ui"; import CutOnIcon from "../../../icons/FogCutOnIcon"; import CutOffIcon from "../../../icons/FogCutOffIcon"; -function FogCutToggle({ useFogCut, onFogCutChange }) { +function FogCutToggle({ useFogCut, onFogCutChange, disabled }) { return ( onFogCutChange(!useFogCut)} + disabled={disabled} > {useFogCut ? : } diff --git a/src/components/map/controls/FogToolSettings.js b/src/components/map/controls/FogToolSettings.js index 201ec13..e9b7d0f 100644 --- a/src/components/map/controls/FogToolSettings.js +++ b/src/components/map/controls/FogToolSettings.js @@ -80,18 +80,21 @@ function BrushToolSettings({ title: "Fog Polygon (P)", isSelected: settings.type === "polygon", icon: , + disabled: settings.preview, }, { id: "rectangle", title: "Fog Rectangle (R)", isSelected: settings.type === "rectangle", icon: , + disabled: settings.preview, }, { id: "brush", title: "Fog Brush (B)", isSelected: settings.type === "brush", icon: , + disabled: settings.preview, }, ]; @@ -107,6 +110,7 @@ function BrushToolSettings({ title="Toggle Fog (T)" onClick={() => onSettingChange({ type: "toggle" })} isSelected={settings.type === "toggle"} + disabled={settings.preview} > @@ -114,6 +118,7 @@ function BrushToolSettings({ title="Erase Fog (E)" onClick={() => onSettingChange({ type: "remove" })} isSelected={settings.type === "remove"} + disabled={settings.preview} > @@ -121,12 +126,14 @@ function BrushToolSettings({ onSettingChange({ useFogCut })} + disabled={settings.preview} /> onSettingChange({ useEdgeSnapping }) } + disabled={settings.preview} /> handleToolClick(tool)} key={tool.id} isSelected={tool.isSelected} + disabled={tool.disabled} > {tool.icon} @@ -90,6 +91,7 @@ function ToolSection({ collapse, tools, onToolClick }) { onClick={() => handleToolClick(tool)} key={tool.id} isSelected={tool.isSelected} + disabled={tool.disabled} > {tool.icon} diff --git a/src/helpers/drawing.js b/src/helpers/drawing.js index 8dca66b..4e60ffa 100644 --- a/src/helpers/drawing.js +++ b/src/helpers/drawing.js @@ -294,3 +294,40 @@ function addPolygonIntersectionToShapes(shape, intersection, shapes) { }; } } + +export function mergeShapes(shapes) { + if (shapes.length === 0) { + return shapes; + } + let geometries = []; + for (let shape of shapes) { + if (!shape.visible) { + continue; + } + const shapePoints = shape.data.points.map(({ x, y }) => [x, y]); + const shapeHoles = shape.data.holes.map((hole) => + hole.map(({ x, y }) => [x, y]) + ); + let shapeGeom = [[shapePoints, ...shapeHoles]]; + geometries.push(shapeGeom); + } + let union = polygonClipping.union(...geometries); + let merged = []; + for (let i = 0; i < union.length; i++) { + let holes = []; + if (union[i].length > 1) { + for (let j = 1; j < union[i].length; j++) { + holes.push(union[i][j].map(([x, y]) => ({ x, y }))); + } + } + merged.push({ + ...shapes[0], + id: `merged-${i}`, + data: { + points: union[i][0].map(([x, y]) => ({ x, y })), + holes, + }, + }); + } + return merged; +}