Added drop shadow to fog, and optimized it with caching
This commit is contained in:
parent
4adc6015f1
commit
c90434b626
@ -333,7 +333,7 @@ function Map({
|
|||||||
active={selectedToolId === "fog"}
|
active={selectedToolId === "fog"}
|
||||||
toolSettings={settings.fog}
|
toolSettings={settings.fog}
|
||||||
gridSize={gridSizeNormalized}
|
gridSize={gridSizeNormalized}
|
||||||
transparent={allowFogDrawing && !settings.fog.preview}
|
editable={allowFogDrawing && !settings.fog.preview}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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 shortid from "shortid";
|
||||||
import { Group } from "react-konva";
|
import { Group, Rect } from "react-konva";
|
||||||
import useImage from "use-image";
|
import useImage from "use-image";
|
||||||
|
|
||||||
import diagonalPattern from "../../images/DiagonalPattern.png";
|
import diagonalPattern from "../../images/DiagonalPattern.png";
|
||||||
@ -13,6 +19,7 @@ import {
|
|||||||
getBrushPositionForTool,
|
getBrushPositionForTool,
|
||||||
simplifyPoints,
|
simplifyPoints,
|
||||||
getStrokeWidth,
|
getStrokeWidth,
|
||||||
|
mergeShapes,
|
||||||
} from "../../helpers/drawing";
|
} from "../../helpers/drawing";
|
||||||
import colors from "../../helpers/colors";
|
import colors from "../../helpers/colors";
|
||||||
import {
|
import {
|
||||||
@ -21,6 +28,7 @@ import {
|
|||||||
Tick,
|
Tick,
|
||||||
} from "../../helpers/konva";
|
} from "../../helpers/konva";
|
||||||
import useKeyboard from "../../helpers/useKeyboard";
|
import useKeyboard from "../../helpers/useKeyboard";
|
||||||
|
import useDebounce from "../../helpers/useDebounce";
|
||||||
|
|
||||||
function MapFog({
|
function MapFog({
|
||||||
map,
|
map,
|
||||||
@ -32,7 +40,7 @@ function MapFog({
|
|||||||
active,
|
active,
|
||||||
toolSettings,
|
toolSettings,
|
||||||
gridSize,
|
gridSize,
|
||||||
transparent,
|
editable,
|
||||||
}) {
|
}) {
|
||||||
const { stageScale, mapWidth, mapHeight, interactionEmitter } = useContext(
|
const { stageScale, mapWidth, mapHeight, interactionEmitter } = useContext(
|
||||||
MapInteractionContext
|
MapInteractionContext
|
||||||
@ -44,12 +52,13 @@ function MapFog({
|
|||||||
|
|
||||||
const shouldHover =
|
const shouldHover =
|
||||||
active &&
|
active &&
|
||||||
|
editable &&
|
||||||
(toolSettings.type === "toggle" || toolSettings.type === "remove");
|
(toolSettings.type === "toggle" || toolSettings.type === "remove");
|
||||||
|
|
||||||
const [patternImage] = useImage(diagonalPattern);
|
const [patternImage] = useImage(diagonalPattern);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!active) {
|
if (!active || !editable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +89,6 @@ function MapFog({
|
|||||||
},
|
},
|
||||||
strokeWidth: 0.5,
|
strokeWidth: 0.5,
|
||||||
color: toolSettings.useFogCut ? "red" : "black",
|
color: toolSettings.useFogCut ? "red" : "black",
|
||||||
blend: false,
|
|
||||||
id: shortid.generate(),
|
id: shortid.generate(),
|
||||||
visible: true,
|
visible: true,
|
||||||
});
|
});
|
||||||
@ -99,7 +107,6 @@ function MapFog({
|
|||||||
},
|
},
|
||||||
strokeWidth: 0.5,
|
strokeWidth: 0.5,
|
||||||
color: toolSettings.useFogCut ? "red" : "black",
|
color: toolSettings.useFogCut ? "red" : "black",
|
||||||
blend: false,
|
|
||||||
id: shortid.generate(),
|
id: shortid.generate(),
|
||||||
visible: true,
|
visible: true,
|
||||||
});
|
});
|
||||||
@ -210,7 +217,6 @@ function MapFog({
|
|||||||
},
|
},
|
||||||
strokeWidth: 0.5,
|
strokeWidth: 0.5,
|
||||||
color: toolSettings.useFogCut ? "red" : "black",
|
color: toolSettings.useFogCut ? "red" : "black",
|
||||||
blend: false,
|
|
||||||
id: shortid.generate(),
|
id: shortid.generate(),
|
||||||
visible: true,
|
visible: true,
|
||||||
};
|
};
|
||||||
@ -355,14 +361,16 @@ function MapFog({
|
|||||||
mapWidth,
|
mapWidth,
|
||||||
mapHeight
|
mapHeight
|
||||||
)}
|
)}
|
||||||
visible={(active && !toolSettings.preview) || shape.visible}
|
opacity={editable ? 0.5 : 1}
|
||||||
opacity={transparent ? 0.5 : 1}
|
|
||||||
fillPatternImage={patternImage}
|
fillPatternImage={patternImage}
|
||||||
fillPriority={active && !shape.visible ? "pattern" : "color"}
|
fillPriority={active && !shape.visible ? "pattern" : "color"}
|
||||||
holes={holes}
|
holes={holes}
|
||||||
// Disable collision if the fog is transparent and we're not editing it
|
// Disable collision if the fog is transparent and we're not editing it
|
||||||
// This allows tokens to be moved under the fog
|
// 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 (
|
return (
|
||||||
<Group>
|
<Group>
|
||||||
{shapes.map(renderShape)}
|
<Group ref={fogGroupRef}>
|
||||||
|
{/* Render a blank shape so cache works with no fog shapes */}
|
||||||
|
<Rect width={1} height={1} />
|
||||||
|
{fogShapes.map(renderShape)}
|
||||||
|
</Group>
|
||||||
{drawingShape && renderShape(drawingShape)}
|
{drawingShape && renderShape(drawingShape)}
|
||||||
{drawingShape &&
|
{drawingShape &&
|
||||||
toolSettings &&
|
toolSettings &&
|
||||||
|
@ -4,7 +4,11 @@ import { IconButton } from "theme-ui";
|
|||||||
import SnappingOnIcon from "../../../icons/SnappingOnIcon";
|
import SnappingOnIcon from "../../../icons/SnappingOnIcon";
|
||||||
import SnappingOffIcon from "../../../icons/SnappingOffIcon";
|
import SnappingOffIcon from "../../../icons/SnappingOffIcon";
|
||||||
|
|
||||||
function EdgeSnappingToggle({ useEdgeSnapping, onEdgeSnappingChange }) {
|
function EdgeSnappingToggle({
|
||||||
|
useEdgeSnapping,
|
||||||
|
onEdgeSnappingChange,
|
||||||
|
disabled,
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={
|
aria-label={
|
||||||
@ -18,6 +22,7 @@ function EdgeSnappingToggle({ useEdgeSnapping, onEdgeSnappingChange }) {
|
|||||||
: "Enable Edge Snapping (S)"
|
: "Enable Edge Snapping (S)"
|
||||||
}
|
}
|
||||||
onClick={() => onEdgeSnappingChange(!useEdgeSnapping)}
|
onClick={() => onEdgeSnappingChange(!useEdgeSnapping)}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{useEdgeSnapping ? <SnappingOnIcon /> : <SnappingOffIcon />}
|
{useEdgeSnapping ? <SnappingOnIcon /> : <SnappingOffIcon />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -4,7 +4,7 @@ import { IconButton } from "theme-ui";
|
|||||||
import CutOnIcon from "../../../icons/FogCutOnIcon";
|
import CutOnIcon from "../../../icons/FogCutOnIcon";
|
||||||
import CutOffIcon from "../../../icons/FogCutOffIcon";
|
import CutOffIcon from "../../../icons/FogCutOffIcon";
|
||||||
|
|
||||||
function FogCutToggle({ useFogCut, onFogCutChange }) {
|
function FogCutToggle({ useFogCut, onFogCutChange, disabled }) {
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={
|
aria-label={
|
||||||
@ -12,6 +12,7 @@ function FogCutToggle({ useFogCut, onFogCutChange }) {
|
|||||||
}
|
}
|
||||||
title={useFogCut ? "Disable Fog Cutting (C)" : "Enable Fog Cutting (C)"}
|
title={useFogCut ? "Disable Fog Cutting (C)" : "Enable Fog Cutting (C)"}
|
||||||
onClick={() => onFogCutChange(!useFogCut)}
|
onClick={() => onFogCutChange(!useFogCut)}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{useFogCut ? <CutOnIcon /> : <CutOffIcon />}
|
{useFogCut ? <CutOnIcon /> : <CutOffIcon />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -80,18 +80,21 @@ function BrushToolSettings({
|
|||||||
title: "Fog Polygon (P)",
|
title: "Fog Polygon (P)",
|
||||||
isSelected: settings.type === "polygon",
|
isSelected: settings.type === "polygon",
|
||||||
icon: <FogPolygonIcon />,
|
icon: <FogPolygonIcon />,
|
||||||
|
disabled: settings.preview,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "rectangle",
|
id: "rectangle",
|
||||||
title: "Fog Rectangle (R)",
|
title: "Fog Rectangle (R)",
|
||||||
isSelected: settings.type === "rectangle",
|
isSelected: settings.type === "rectangle",
|
||||||
icon: <FogRectangleIcon />,
|
icon: <FogRectangleIcon />,
|
||||||
|
disabled: settings.preview,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "brush",
|
id: "brush",
|
||||||
title: "Fog Brush (B)",
|
title: "Fog Brush (B)",
|
||||||
isSelected: settings.type === "brush",
|
isSelected: settings.type === "brush",
|
||||||
icon: <FogBrushIcon />,
|
icon: <FogBrushIcon />,
|
||||||
|
disabled: settings.preview,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -107,6 +110,7 @@ function BrushToolSettings({
|
|||||||
title="Toggle Fog (T)"
|
title="Toggle Fog (T)"
|
||||||
onClick={() => onSettingChange({ type: "toggle" })}
|
onClick={() => onSettingChange({ type: "toggle" })}
|
||||||
isSelected={settings.type === "toggle"}
|
isSelected={settings.type === "toggle"}
|
||||||
|
disabled={settings.preview}
|
||||||
>
|
>
|
||||||
<FogToggleIcon />
|
<FogToggleIcon />
|
||||||
</RadioIconButton>
|
</RadioIconButton>
|
||||||
@ -114,6 +118,7 @@ function BrushToolSettings({
|
|||||||
title="Erase Fog (E)"
|
title="Erase Fog (E)"
|
||||||
onClick={() => onSettingChange({ type: "remove" })}
|
onClick={() => onSettingChange({ type: "remove" })}
|
||||||
isSelected={settings.type === "remove"}
|
isSelected={settings.type === "remove"}
|
||||||
|
disabled={settings.preview}
|
||||||
>
|
>
|
||||||
<FogRemoveIcon />
|
<FogRemoveIcon />
|
||||||
</RadioIconButton>
|
</RadioIconButton>
|
||||||
@ -121,12 +126,14 @@ function BrushToolSettings({
|
|||||||
<FogCutToggle
|
<FogCutToggle
|
||||||
useFogCut={settings.useFogCut}
|
useFogCut={settings.useFogCut}
|
||||||
onFogCutChange={(useFogCut) => onSettingChange({ useFogCut })}
|
onFogCutChange={(useFogCut) => onSettingChange({ useFogCut })}
|
||||||
|
disabled={settings.preview}
|
||||||
/>
|
/>
|
||||||
<EdgeSnappingToggle
|
<EdgeSnappingToggle
|
||||||
useEdgeSnapping={settings.useEdgeSnapping}
|
useEdgeSnapping={settings.useEdgeSnapping}
|
||||||
onEdgeSnappingChange={(useEdgeSnapping) =>
|
onEdgeSnappingChange={(useEdgeSnapping) =>
|
||||||
onSettingChange({ useEdgeSnapping })
|
onSettingChange({ useEdgeSnapping })
|
||||||
}
|
}
|
||||||
|
disabled={settings.preview}
|
||||||
/>
|
/>
|
||||||
<FogPreviewToggle
|
<FogPreviewToggle
|
||||||
useFogPreview={settings.preview}
|
useFogPreview={settings.preview}
|
||||||
|
@ -36,6 +36,7 @@ function ToolSection({ collapse, tools, onToolClick }) {
|
|||||||
onClick={() => handleToolClick(tool)}
|
onClick={() => handleToolClick(tool)}
|
||||||
key={tool.id}
|
key={tool.id}
|
||||||
isSelected={tool.isSelected}
|
isSelected={tool.isSelected}
|
||||||
|
disabled={tool.disabled}
|
||||||
>
|
>
|
||||||
{tool.icon}
|
{tool.icon}
|
||||||
</RadioIconButton>
|
</RadioIconButton>
|
||||||
@ -90,6 +91,7 @@ function ToolSection({ collapse, tools, onToolClick }) {
|
|||||||
onClick={() => handleToolClick(tool)}
|
onClick={() => handleToolClick(tool)}
|
||||||
key={tool.id}
|
key={tool.id}
|
||||||
isSelected={tool.isSelected}
|
isSelected={tool.isSelected}
|
||||||
|
disabled={tool.disabled}
|
||||||
>
|
>
|
||||||
{tool.icon}
|
{tool.icon}
|
||||||
</RadioIconButton>
|
</RadioIconButton>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user