Readded map fog

This commit is contained in:
Mitchell McCaffrey 2020-05-22 17:22:32 +10:00
parent a81eed1a48
commit a6768727fe
2 changed files with 189 additions and 218 deletions

View File

@ -5,6 +5,7 @@ import MapControls from "./MapControls";
import MapInteraction from "./MapInteraction"; import MapInteraction from "./MapInteraction";
import MapToken from "./MapToken"; import MapToken from "./MapToken";
import MapDrawing from "./MapDrawing"; import MapDrawing from "./MapDrawing";
import MapFog from "./MapFog";
import TokenDataContext from "../../contexts/TokenDataContext"; import TokenDataContext from "../../contexts/TokenDataContext";
import TokenMenu from "../token/TokenMenu"; import TokenMenu from "../token/TokenMenu";
@ -85,8 +86,29 @@ function Map({
} }
const [mapShapes, setMapShapes] = useState([]); const [mapShapes, setMapShapes] = useState([]);
function handleMapShapeAdd(shape) {
onMapDraw({ type: "add", shapes: [shape] });
}
function handleMapShapeRemove(shapeId) {
onMapDraw({ type: "remove", shapeIds: [shapeId] });
}
const [fogShapes, setFogShapes] = useState([]); const [fogShapes, setFogShapes] = useState([]);
function handleFogShapeAdd(shape) {
onFogDraw({ type: "add", shapes: [shape] });
}
function handleFogShapeRemove(shapeId) {
onFogDraw({ type: "remove", shapeIds: [shapeId] });
}
function handleFogShapeEdit(shape) {
onFogDraw({ type: "edit", shapes: [shape] });
}
// Replay the draw actions and convert them to shapes for the map drawing // Replay the draw actions and convert them to shapes for the map drawing
useEffect(() => { useEffect(() => {
if (!mapState) { if (!mapState) {
@ -116,14 +138,6 @@ function Map({
); );
}, [mapState]); }, [mapState]);
function handleMapShapeAdd(shape) {
onMapDraw({ type: "add", shapes: [shape] });
}
function handleMapShapeRemove(shapeId) {
onMapDraw({ type: "remove", shapeIds: [shapeId] });
}
const disabledControls = []; const disabledControls = [];
if (!allowMapDrawing) { if (!allowMapDrawing) {
disabledControls.push("brush"); disabledControls.push("brush");
@ -237,6 +251,18 @@ function Map({
/> />
); );
const mapFog = (
<MapFog
shapes={fogShapes}
onShapeAdd={handleFogShapeAdd}
onShapeRemove={handleFogShapeRemove}
onShapeEdit={handleFogShapeEdit}
selectedToolId={selectedToolId}
selectedToolSettings={toolSettings[selectedToolId]}
gridSize={gridSizeNormalized}
/>
);
return ( return (
<MapInteraction <MapInteraction
map={map} map={map}
@ -251,6 +277,7 @@ function Map({
> >
{mapDrawing} {mapDrawing}
{mapTokens} {mapTokens}
{mapFog}
</MapInteraction> </MapInteraction>
); );
} }

View File

@ -1,104 +1,82 @@
import React, { useRef, useEffect, useState, useContext } from "react"; import React, { useContext, useEffect, useState } from "react";
import shortid from "shortid"; import shortid from "shortid";
import { Group, Line } from "react-konva";
import useImage from "use-image";
import diagonalPattern from "../../images/DiagonalPattern.png";
import MapInteractionContext from "../../contexts/MapInteractionContext";
import { compare as comparePoints } from "../../helpers/vector2"; import { compare as comparePoints } from "../../helpers/vector2";
import { import {
getBrushPositionForTool, getBrushPositionForTool,
isShapeHovered,
drawShape,
simplifyPoints, simplifyPoints,
getRelativePointerPosition, getStrokeWidth,
} from "../../helpers/drawing"; } from "../../helpers/drawing";
import MapInteractionContext from "../../contexts/MapInteractionContext"; import colors from "../../helpers/colors";
import diagonalPattern from "../../images/DiagonalPattern.png";
function MapFog({ function MapFog({
width,
height,
isEditing,
toolSettings,
shapes, shapes,
onShapeAdd, onShapeAdd,
onShapeRemove, onShapeRemove,
onShapeEdit, onShapeEdit,
selectedToolId,
selectedToolSettings,
gridSize, gridSize,
}) { }) {
const canvasRef = useRef(); const {
const containerRef = useRef(); stageDragState,
mapDragPosition,
const [isPointerDown, setIsPointerDown] = useState(false); stageScale,
mapWidth,
mapHeight,
} = useContext(MapInteractionContext);
const [drawingShape, setDrawingShape] = useState(null); const [drawingShape, setDrawingShape] = useState(null);
const [pointerPosition, setPointerPosition] = useState({ x: -1, y: -1 });
const isEditing = selectedToolId === "fog";
const shouldHover = const shouldHover =
isEditing && isEditing &&
(toolSettings.type === "toggle" || toolSettings.type === "remove"); (selectedToolSettings.type === "toggle" ||
selectedToolSettings.type === "remove");
const { scaleRef } = useContext(MapInteractionContext); const [patternImage] = useImage(diagonalPattern);
// Reset pointer position when tool changes
useEffect(() => { useEffect(() => {
setPointerPosition({ x: -1, y: -1 });
}, [isEditing, toolSettings]);
function handleStart(event) {
if (!isEditing) { if (!isEditing) {
return; return;
} }
if (event.touches && event.touches.length !== 1) {
setIsPointerDown(false);
setDrawingShape(null);
return;
}
const pointer = event.touches ? event.touches[0] : event;
const position = getRelativePointerPosition(pointer, containerRef.current);
setPointerPosition(position);
setIsPointerDown(true);
const brushPosition = getBrushPositionForTool(
position,
"fog",
toolSettings,
gridSize,
shapes
);
if (isEditing && toolSettings.type === "add") {
setDrawingShape({
type: "fog",
data: { points: [brushPosition] },
strokeWidth: 0.5,
color: "black",
blend: true, // Blend while drawing
id: shortid.generate(),
visible: true,
});
}
}
function handleMove(event) { function startShape() {
if (!isEditing) {
return;
}
if (event.touches && event.touches.length !== 1) {
return;
}
const pointer = event.touches ? event.touches[0] : event;
const position = getRelativePointerPosition(pointer, containerRef.current);
// Set pointer position every frame for erase tool and fog
if (shouldHover) {
setPointerPosition(position);
}
if (isPointerDown) {
setPointerPosition(position);
const brushPosition = getBrushPositionForTool( const brushPosition = getBrushPositionForTool(
position, mapDragPosition,
"fog", selectedToolId,
toolSettings, selectedToolSettings,
gridSize, gridSize,
shapes shapes
); );
if (isEditing && toolSettings.type === "add" && drawingShape) { if (selectedToolSettings.type === "add") {
setDrawingShape({
type: "fog",
data: { points: [brushPosition] },
strokeWidth: 0.5,
color: "black",
blend: false,
id: shortid.generate(),
visible: true,
});
}
}
function continueShape() {
const brushPosition = getBrushPositionForTool(
mapDragPosition,
selectedToolId,
selectedToolSettings,
gridSize,
shapes
);
if (selectedToolSettings.type === "add") {
setDrawingShape((prevShape) => { setDrawingShape((prevShape) => {
const prevPoints = prevShape.data.points; const prevPoints = prevShape.data.points;
if ( if (
@ -117,159 +95,125 @@ function MapFog({
}); });
} }
} }
}
function handleStop(event) { function endShape() {
if (selectedToolSettings.type === "add" && drawingShape) {
if (drawingShape.data.points.length > 1) {
const shape = {
...drawingShape,
data: {
points: simplifyPoints(
drawingShape.data.points,
gridSize,
// Downscale fog as smoothing doesn't currently work with edge snapping
stageScale / 2
),
},
};
onShapeAdd(shape);
}
}
setDrawingShape(null);
}
switch (stageDragState) {
case "first":
startShape();
return;
case "dragging":
continueShape();
return;
case "last":
endShape();
return;
default:
return;
}
}, [
stageDragState,
mapDragPosition,
selectedToolId,
selectedToolSettings,
isEditing,
gridSize,
stageScale,
onShapeAdd,
shapes,
drawingShape,
]);
function handleShapeClick(_, shape) {
if (!isEditing) { if (!isEditing) {
return; return;
} }
if (event.touches && event.touches.length !== 0) {
return;
}
if (isEditing && toolSettings.type === "add" && drawingShape) {
if (drawingShape.data.points.length > 1) {
const shape = {
...drawingShape,
data: {
points: simplifyPoints(
drawingShape.data.points,
gridSize,
// Downscale fog as smoothing doesn't currently work with edge snapping
scaleRef.current / 2
),
},
blend: false,
};
onShapeAdd(shape);
}
}
if (hoveredShapeRef.current && isPointerDown) { if (selectedToolSettings.type === "remove") {
if (toolSettings.type === "remove") { onShapeRemove(shape.id);
onShapeRemove(hoveredShapeRef.current.id); } else if (selectedToolSettings.type === "toggle") {
} else if (toolSettings.type === "toggle") { onShapeEdit({ ...shape, visible: !shape.visible });
onShapeEdit({
...hoveredShapeRef.current,
visible: !hoveredShapeRef.current.visible,
});
}
} }
setDrawingShape(null);
setIsPointerDown(false);
} }
// Add listeners for draw events on map to allow drawing past the bounds function handleShapeMouseOver(event, shape) {
// of the container if (shouldHover) {
useEffect(() => { const path = event.target;
const map = document.querySelector(".map"); if (shape.visible) {
map.addEventListener("mousedown", handleStart); const hoverColor = "#BB99FF";
map.addEventListener("mousemove", handleMove); path.fill(hoverColor);
map.addEventListener("mouseup", handleStop);
map.addEventListener("touchstart", handleStart);
map.addEventListener("touchmove", handleMove);
map.addEventListener("touchend", handleStop);
return () => {
map.removeEventListener("mousedown", handleStart);
map.removeEventListener("mousemove", handleMove);
map.removeEventListener("mouseup", handleStop);
map.removeEventListener("touchstart", handleStart);
map.removeEventListener("touchmove", handleMove);
map.removeEventListener("touchend", handleStop);
};
});
/**
* Rendering
*/
const hoveredShapeRef = useRef(null);
const diagonalPatternRef = useRef();
useEffect(() => {
let image = new Image();
image.src = diagonalPattern;
diagonalPatternRef.current = image;
}, []);
useEffect(() => {
const canvas = canvasRef.current;
if (canvas) {
const context = canvas.getContext("2d");
context.clearRect(0, 0, width, height);
let hoveredShape = null;
if (isEditing) {
const editPattern = context.createPattern(
diagonalPatternRef.current,
"repeat"
);
for (let shape of shapes) {
if (shouldHover) {
if (
isShapeHovered(shape, context, pointerPosition, width, height)
) {
hoveredShape = shape;
}
}
drawShape(
{
...shape,
blend: true,
color: shape.visible ? "black" : editPattern,
},
context,
gridSize,
width,
height
);
}
if (drawingShape) {
drawShape(drawingShape, context, gridSize, width, height);
}
if (hoveredShape) {
const shape = { ...hoveredShape, color: "#BB99FF", blend: true };
drawShape(shape, context, gridSize, width, height);
}
} else { } else {
// Not editing path.opacity(1);
for (let shape of shapes) {
if (shape.visible) {
drawShape(shape, context, gridSize, width, height);
}
}
} }
hoveredShapeRef.current = hoveredShape; path.getLayer().draw();
} }
}, [ }
shapes,
width, function handleShapeMouseOut(event, shape) {
height, if (shouldHover) {
pointerPosition, const path = event.target;
isEditing, if (shape.visible) {
drawingShape, const color = colors[shape.color] || shape.color;
gridSize, path.fill(color);
shouldHover, } else {
]); path.opacity(0.5);
}
path.getLayer().draw();
}
}
function renderShape(shape) {
return (
<Line
key={shape.id}
onMouseOver={(e) => handleShapeMouseOver(e, shape)}
onMouseOut={(e) => handleShapeMouseOut(e, shape)}
onClick={(e) => handleShapeClick(e, shape)}
points={shape.data.points.reduce(
(acc, point) => [...acc, point.x * mapWidth, point.y * mapHeight],
[]
)}
stroke={colors[shape.color] || shape.color}
fill={colors[shape.color] || shape.color}
closed
lineCap="round"
strokeWidth={getStrokeWidth(
shape.strokeWidth,
gridSize,
mapWidth,
mapHeight
)}
visible={isEditing || shape.visible}
opacity={isEditing ? 0.5 : 1}
fillPatternImage={patternImage}
fillPriority={isEditing && !shape.visible ? "pattern" : "color"}
/>
);
}
return ( return (
<div <Group>
style={{ {shapes.map(renderShape)}
position: "absolute", {drawingShape && renderShape(drawingShape)}
top: 0, </Group>
left: 0,
right: 0,
bottom: 0,
pointerEvents: "none",
}}
ref={containerRef}
>
<canvas
ref={canvasRef}
width={width}
height={height}
style={{ width: "100%", height: "100%" }}
/>
</div>
); );
} }