Separated map drawing and map fog into separate action lists

This commit is contained in:
Mitchell McCaffrey 2020-04-28 22:05:47 +10:00
parent b34a7df443
commit 5e2c178118
7 changed files with 404 additions and 110 deletions

View File

@ -5,6 +5,7 @@ import ProxyToken from "../token/ProxyToken";
import TokenMenu from "../token/TokenMenu";
import MapToken from "./MapToken";
import MapDrawing from "./MapDrawing";
import MapFog from "./MapFog";
import MapControls from "./MapControls";
import { omit } from "../../helpers/shared";
@ -25,8 +26,9 @@ function Map({
onMapChange,
onMapStateChange,
onMapDraw,
onMapDrawUndo,
onMapDrawRedo,
onFogDraw,
onMapUndo,
onMapRedo,
allowDrawing,
allowTokenChange,
allowMapChange,
@ -71,17 +73,30 @@ function Map({
}));
}
const [drawnShapes, setDrawnShapes] = useState([]);
function handleShapeAdd(shape) {
onMapDraw({ type: "add", shapes: [shape] });
const [mapShapes, setMapShapes] = useState([]);
function handleMapShapeAdd(shape) {
onMapDraw({ type: "add", shapes: [shape], timestamp: Date.now() });
}
function handleShapeRemove(shapeId) {
onMapDraw({ type: "remove", shapeIds: [shapeId] });
function handleMapShapeRemove(shapeId) {
onMapDraw({ type: "remove", shapeIds: [shapeId], timestamp: Date.now() });
}
function handleShapeRemoveAll() {
onMapDraw({ type: "remove", shapeIds: drawnShapes.map((s) => s.id) });
onMapDraw({
type: "remove",
shapeIds: mapShapes.map((s) => s.id),
timestamp: Date.now(),
});
}
const [fogShapes, setFogShapes] = useState([]);
function handleFogShapeAdd(shape) {
onFogDraw({ type: "add", shapes: [shape], timestamp: Date.now() });
}
function handleFogShapeRemove(shapeId) {
onFogDraw({ type: "remove", shapeIds: [shapeId], timestamp: Date.now() });
}
// Replay the draw actions and convert them to shapes for the map drawing
@ -89,19 +104,28 @@ function Map({
if (!mapState) {
return;
}
let shapesById = {};
for (let i = 0; i <= mapState.drawActionIndex; i++) {
const action = mapState.drawActions[i];
if (action.type === "add") {
for (let shape of action.shapes) {
shapesById[shape.id] = shape;
function actionsToShapes(actions, actionIndex) {
let shapesById = {};
for (let i = 0; i <= actionIndex; i++) {
const action = actions[i];
if (action.type === "add") {
for (let shape of action.shapes) {
shapesById[shape.id] = shape;
}
}
if (action.type === "remove") {
shapesById = omit(shapesById, action.shapeIds);
}
}
if (action.type === "remove") {
shapesById = omit(shapesById, action.shapeIds);
}
return Object.values(shapesById);
}
setDrawnShapes(Object.values(shapesById));
setMapShapes(
actionsToShapes(mapState.mapDrawActions, mapState.mapDrawActionIndex)
);
setFogShapes(
actionsToShapes(mapState.fogDrawActions, mapState.fogDrawActionIndex)
);
}, [mapState]);
const disabledControls = [];
@ -115,15 +139,15 @@ function Map({
disabledControls.push("pan");
disabledControls.push("brush");
}
if (drawnShapes.length === 0) {
if (mapShapes.length === 0) {
disabledControls.push("erase");
}
if (!mapState || mapState.drawActionIndex < 0) {
if (!mapState || mapState.mapDrawActionIndex < 0) {
disabledControls.push("undo");
}
if (
!mapState ||
mapState.drawActionIndex === mapState.drawActions.length - 1
mapState.mapDrawActionIndex === mapState.mapDrawActions.length - 1
) {
disabledControls.push("redo");
}
@ -191,11 +215,24 @@ function Map({
<MapDrawing
width={map ? map.width : 0}
height={map ? map.height : 0}
selectedTool={selectedToolId}
selectedTool={selectedToolId !== "fog" ? selectedToolId : "none"}
toolSettings={toolSettings[selectedToolId]}
shapes={drawnShapes}
onShapeAdd={handleShapeAdd}
onShapeRemove={handleShapeRemove}
shapes={mapShapes}
onShapeAdd={handleMapShapeAdd}
onShapeRemove={handleMapShapeRemove}
gridSize={gridSizeNormalized}
/>
);
const mapFog = (
<MapFog
width={map ? map.width : 0}
height={map ? map.height : 0}
isEditing={selectedToolId === "fog"}
toolSettings={toolSettings["fog"]}
shapes={fogShapes}
onShapeAdd={handleFogShapeAdd}
onShapeRemove={handleFogShapeRemove}
gridSize={gridSizeNormalized}
/>
);
@ -210,8 +247,8 @@ function Map({
toolSettings={toolSettings}
onToolSettingChange={handleToolSettingChange}
disabledControls={disabledControls}
onUndo={onMapDrawUndo}
onRedo={onMapDrawRedo}
onUndo={onMapUndo}
onRedo={onMapRedo}
/>
);
return (
@ -224,6 +261,7 @@ function Map({
>
{map && mapImage}
{map && mapDrawing}
{map && mapFog}
{map && mapTokens}
</MapInteraction>
{allowTokenChange && (

View File

@ -10,6 +10,7 @@ import {
isShapeHovered,
drawShape,
simplifyPoints,
getRelativePointerPosition,
} from "../../helpers/drawing";
function MapDrawing({
@ -29,34 +30,28 @@ function MapDrawing({
const [drawingShape, setDrawingShape] = useState(null);
const [pointerPosition, setPointerPosition] = useState({ x: -1, y: -1 });
const shouldHover =
selectedTool === "erase" ||
(selectedTool === "fog" &&
(toolSettings.type === "toggle" || toolSettings.type === "remove"));
const shouldHover = selectedTool === "erase";
const isEditing =
selectedTool === "brush" ||
selectedTool === "shape" ||
selectedTool === "erase";
// Reset pointer position when tool changes
useEffect(() => {
setPointerPosition({ x: -1, y: -1 });
}, [selectedTool]);
function getRelativePointerPosition(event) {
const container = containerRef.current;
if (container) {
const containerRect = container.getBoundingClientRect();
const x = (event.clientX - containerRect.x) / containerRect.width;
const y = (event.clientY - containerRect.y) / containerRect.height;
return { x, y };
}
}
function handleStart(event) {
if (!isEditing) {
return;
}
if (event.touches && event.touches.length !== 1) {
setIsDrawing(false);
setDrawingShape(null);
return;
}
const pointer = event.touches ? event.touches[0] : event;
const position = getRelativePointerPosition(pointer);
const position = getRelativePointerPosition(pointer, containerRef.current);
setPointerPosition(position);
setIsDrawing(true);
const brushPosition = getBrushPositionForTool(
@ -67,6 +62,8 @@ function MapDrawing({
shapes
);
const commonShapeData = {
color: toolSettings && toolSettings.color,
blend: toolSettings && toolSettings.useBlending,
id: shortid.generate(),
};
if (selectedTool === "brush") {
@ -75,8 +72,6 @@ function MapDrawing({
pathType: toolSettings.type,
data: { points: [brushPosition] },
strokeWidth: toolSettings.type === "stroke" ? 1 : 0,
color: toolSettings && toolSettings.color,
blend: toolSettings && toolSettings.useBlending,
...commonShapeData,
});
} else if (selectedTool === "shape") {
@ -85,33 +80,32 @@ function MapDrawing({
shapeType: toolSettings.type,
data: getDefaultShapeData(toolSettings.type, brushPosition),
strokeWidth: 0,
color: toolSettings && toolSettings.color,
blend: toolSettings && toolSettings.useBlending,
...commonShapeData,
});
} else if (selectedTool === "fog" && toolSettings.type === "add") {
setDrawingShape({
type: "fog",
data: { points: [brushPosition] },
strokeWidth: 0.1,
color: "black",
blend: true, // Blend while drawing
...commonShapeData,
});
}
}
function handleMove(event) {
if (!isEditing) {
return;
}
if (event.touches && event.touches.length !== 1) {
return;
}
const pointer = event.touches ? event.touches[0] : event;
const position = getRelativePointerPosition(pointer);
// Set pointer position every frame for erase tool and fog
if (shouldHover) {
const position = getRelativePointerPosition(
pointer,
containerRef.current
);
setPointerPosition(position);
}
if (isDrawing) {
const position = getRelativePointerPosition(
pointer,
containerRef.current
);
setPointerPosition(position);
const brushPosition = getBrushPositionForTool(
position,
@ -150,28 +144,14 @@ function MapDrawing({
brushPosition
),
}));
} else if (selectedTool === "fog" && toolSettings.type === "add") {
setDrawingShape((prevShape) => {
const prevPoints = prevShape.data.points;
if (
comparePoints(
prevPoints[prevPoints.length - 1],
brushPosition,
0.001
)
) {
return prevShape;
}
return {
...prevShape,
data: { points: [...prevPoints, brushPosition] },
};
});
}
}
}
function handleStop(event) {
if (!isEditing) {
return;
}
if (event.touches && event.touches.length !== 0) {
return;
}
@ -182,15 +162,6 @@ function MapDrawing({
}
} else if (selectedTool === "shape") {
onShapeAdd(drawingShape);
} else if (selectedTool === "fog" && toolSettings.type === "add") {
if (drawingShape.data.points.length > 1) {
const shape = {
...drawingShape,
data: { points: simplifyPoints(drawingShape.data.points, gridSize) },
blend: false,
};
onShapeAdd(shape);
}
}
setDrawingShape(null);
@ -237,17 +208,7 @@ function MapDrawing({
hoveredShape = shape;
}
}
if (selectedTool === "fog") {
drawShape(
{ ...shape, blend: true },
context,
gridSize,
width,
height
);
} else {
drawShape(shape, context, gridSize, width, height);
}
drawShape(shape, context, gridSize, width, height);
}
if (drawingShape) {
drawShape(drawingShape, context, gridSize, width, height);
@ -267,11 +228,19 @@ function MapDrawing({
selectedTool,
drawingShape,
gridSize,
shouldHover,
]);
return (
<div
style={{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0 }}
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
pointerEvents: "none",
}}
ref={containerRef}
>
<canvas

View File

@ -0,0 +1,236 @@
import React, { useRef, useEffect, useState } from "react";
import shortid from "shortid";
import { compare as comparePoints } from "../../helpers/vector2";
import {
getBrushPositionForTool,
isShapeHovered,
drawShape,
simplifyPoints,
getRelativePointerPosition,
} from "../../helpers/drawing";
function MapFog({
width,
height,
isEditing,
toolSettings,
shapes,
onShapeAdd,
onShapeRemove,
gridSize,
}) {
const canvasRef = useRef();
const containerRef = useRef();
const [isDrawing, setIsDrawing] = useState(false);
const [drawingShape, setDrawingShape] = useState(null);
const [pointerPosition, setPointerPosition] = useState({ x: -1, y: -1 });
const shouldHover =
isEditing &&
(toolSettings.type === "toggle" || toolSettings.type === "remove");
// Reset pointer position when tool changes
useEffect(() => {
setPointerPosition({ x: -1, y: -1 });
}, [isEditing, toolSettings]);
function handleStart(event) {
if (!isEditing) {
return;
}
if (event.touches && event.touches.length !== 1) {
setIsDrawing(false);
setDrawingShape(null);
return;
}
const pointer = event.touches ? event.touches[0] : event;
const position = getRelativePointerPosition(pointer, containerRef.current);
setPointerPosition(position);
setIsDrawing(true);
const brushPosition = getBrushPositionForTool(
position,
"fog",
toolSettings,
gridSize,
shapes
);
const commonShapeData = {
id: shortid.generate(),
};
if (isEditing && toolSettings.type === "add") {
setDrawingShape({
type: "fog",
data: { points: [brushPosition] },
strokeWidth: 0.1,
color: "black",
blend: true, // Blend while drawing
...commonShapeData,
});
}
}
function handleMove(event) {
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 (isDrawing) {
setPointerPosition(position);
const brushPosition = getBrushPositionForTool(
position,
"fog",
toolSettings,
gridSize,
shapes
);
if (isEditing && toolSettings.type === "add" && drawingShape) {
setDrawingShape((prevShape) => {
const prevPoints = prevShape.data.points;
if (
comparePoints(
prevPoints[prevPoints.length - 1],
brushPosition,
0.001
)
) {
return prevShape;
}
return {
...prevShape,
data: { points: [...prevPoints, brushPosition] },
};
});
}
}
}
function handleStop(event) {
if (!isEditing) {
return;
}
if (event.touches && event.touches.length !== 0) {
return;
}
setIsDrawing(false);
if (isEditing && toolSettings.type === "add" && drawingShape) {
if (drawingShape.data.points.length > 1) {
const shape = {
...drawingShape,
data: { points: simplifyPoints(drawingShape.data.points, gridSize) },
blend: false,
};
onShapeAdd(shape);
}
}
setDrawingShape(null);
if (toolSettings.type === "remove" && hoveredShapeRef.current) {
onShapeRemove(hoveredShapeRef.current.id);
}
}
// Add listeners for draw events on map to allow drawing past the bounds
// of the container
useEffect(() => {
const map = document.querySelector(".map");
map.addEventListener("mousedown", handleStart);
map.addEventListener("mousemove", handleMove);
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);
useEffect(() => {
const canvas = canvasRef.current;
if (canvas) {
const context = canvas.getContext("2d");
context.clearRect(0, 0, width, height);
let hoveredShape = null;
for (let shape of shapes) {
if (shouldHover) {
if (isShapeHovered(shape, context, pointerPosition, width, height)) {
hoveredShape = shape;
}
}
if (isEditing) {
drawShape(
{ ...shape, blend: true },
context,
gridSize,
width,
height
);
} else {
drawShape(shape, 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);
}
hoveredShapeRef.current = hoveredShape;
}
}, [
shapes,
width,
height,
pointerPosition,
isDrawing,
isEditing,
drawingShape,
gridSize,
shouldHover,
]);
return (
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
pointerEvents: "none",
}}
ref={containerRef}
>
<canvas
ref={canvasRef}
width={width}
height={height}
style={{ width: "100%", height: "100%" }}
/>
</div>
);
}
export default MapFog;

View File

@ -23,7 +23,8 @@ function MapTile({
const hasMapState =
mapState &&
(Object.values(mapState.tokens).length > 0 ||
mapState.drawActions.length > 0);
mapState.mapDrawActions.length > 0 ||
mapState.fogDrawActions.length > 0);
const expandButton = (
<IconButton

View File

@ -308,3 +308,12 @@ const defaultSimplifySize = 1 / 100;
export function simplifyPoints(points, gridSize) {
return simplify(points, Vector2.min(gridSize) * defaultSimplifySize);
}
export function getRelativePointerPosition(event, container) {
if (container) {
const containerRect = container.getBoundingClientRect();
const x = (event.clientX - containerRect.x) / containerRect.width;
const y = (event.clientY - containerRect.y) / containerRect.height;
return { x, y };
}
}

View File

@ -19,8 +19,10 @@ const defaultMapState = {
tokens: {},
// An index into the draw actions array to which only actions before the
// index will be performed (used in undo and redo)
drawActionIndex: -1,
drawActions: [],
mapDrawActionIndex: -1,
mapDrawActions: [],
fogDrawActionIndex: -1,
fogDrawActions: [],
// Flags to determine what other people can edit
editFlags: ["map", "drawings", "tokens"],
};

View File

@ -122,14 +122,17 @@ function Game() {
function addNewMapDrawActions(actions) {
setMapState((prevMapState) => {
const newActions = [
...prevMapState.drawActions.slice(0, prevMapState.drawActionIndex + 1),
...prevMapState.mapDrawActions.slice(
0,
prevMapState.mapDrawActionIndex + 1
),
...actions,
];
const newIndex = newActions.length - 1;
return {
...prevMapState,
drawActions: newActions,
drawActionIndex: newIndex,
mapDrawActions: newActions,
mapDrawActionIndex: newIndex,
};
});
}
@ -141,31 +144,57 @@ function Game() {
}
}
function handleMapDrawUndo() {
const newIndex = Math.max(mapState.drawActionIndex - 1, -1);
function handleMapUndo() {
// TODO: Check whether to pull from draw actions or fog actions
const newIndex = Math.max(mapState.mapDrawActionIndex - 1, -1);
setMapState((prevMapState) => ({
...prevMapState,
drawActionIndex: newIndex,
mapDrawActionIndex: newIndex,
}));
for (let peer of Object.values(peers)) {
peer.connection.send({ id: "mapDrawIndex", data: newIndex });
}
}
function handleMapDrawRedo() {
function handleMapRedo() {
const newIndex = Math.min(
mapState.drawActionIndex + 1,
mapState.drawActions.length - 1
mapState.mapDrawActionIndex + 1,
mapState.mapDrawActions.length - 1
);
setMapState((prevMapState) => ({
...prevMapState,
drawActionIndex: newIndex,
mapDrawActionIndex: newIndex,
}));
for (let peer of Object.values(peers)) {
peer.connection.send({ id: "mapDrawIndex", data: newIndex });
}
}
function addNewFogDrawActions(actions) {
setMapState((prevMapState) => {
const newActions = [
...prevMapState.fogDrawActions.slice(
0,
prevMapState.fogDrawActionIndex + 1
),
...actions,
];
const newIndex = newActions.length - 1;
return {
...prevMapState,
fogDrawActions: newActions,
fogDrawActionIndex: newIndex,
};
});
}
function handleFogDraw(action) {
addNewFogDrawActions([action]);
for (let peer of Object.values(peers)) {
peer.connection.send({ id: "mapFog", data: [action] });
}
}
/**
* Party state
*/
@ -238,7 +267,16 @@ function Game() {
if (data.id === "mapDrawIndex") {
setMapState((prevMapState) => ({
...prevMapState,
drawActionIndex: data.data,
mapDrawActionIndex: data.data,
}));
}
if (data.id === "mapFog") {
addNewFogDrawActions(data.data);
}
if (data.id === "mapFogIndex") {
setMapState((prevMapState) => ({
...prevMapState,
fogDrawActionIndex: data.data,
}));
}
}
@ -375,8 +413,9 @@ function Game() {
onMapChange={handleMapChange}
onMapStateChange={handleMapStateChange}
onMapDraw={handleMapDraw}
onMapDrawUndo={handleMapDrawUndo}
onMapDrawRedo={handleMapDrawRedo}
onMapUndo={handleMapUndo}
onMapRedo={handleMapRedo}
onFogDraw={handleFogDraw}
allowDrawing={canEditMapDrawings}
allowTokenChange={canEditTokens}
allowMapChange={canChangeMap}