From 5e2c178118a5d89e604e6b6fc6156345737be43b Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Tue, 28 Apr 2020 22:05:47 +1000 Subject: [PATCH] Separated map drawing and map fog into separate action lists --- src/components/map/Map.js | 92 ++++++++---- src/components/map/MapDrawing.js | 103 +++++--------- src/components/map/MapFog.js | 236 +++++++++++++++++++++++++++++++ src/components/map/MapTile.js | 3 +- src/helpers/drawing.js | 9 ++ src/modals/SelectMapModal.js | 6 +- src/routes/Game.js | 65 +++++++-- 7 files changed, 404 insertions(+), 110 deletions(-) create mode 100644 src/components/map/MapFog.js diff --git a/src/components/map/Map.js b/src/components/map/Map.js index 927cff3..9eb2cb8 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -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({ + ); + + const mapFog = ( + ); @@ -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} {allowTokenChange && ( diff --git a/src/components/map/MapDrawing.js b/src/components/map/MapDrawing.js index a76a29c..29c1203 100644 --- a/src/components/map/MapDrawing.js +++ b/src/components/map/MapDrawing.js @@ -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 (
{ + 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 ( +
+ +
+ ); +} + +export default MapFog; diff --git a/src/components/map/MapTile.js b/src/components/map/MapTile.js index 9b656ce..0f3a81e 100644 --- a/src/components/map/MapTile.js +++ b/src/components/map/MapTile.js @@ -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 = ( { 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}