From d26932d17c1d0353e5e39d97b842dd22378f24a1 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 22 May 2020 13:47:11 +1000 Subject: [PATCH] Added back map drawing --- src/components/map/Map.js | 55 +++- src/components/map/MapDrawing.js | 371 ++++++++++++++------------- src/components/map/MapInteraction.js | 45 +++- src/helpers/drawing.js | 12 +- 4 files changed, 286 insertions(+), 197 deletions(-) diff --git a/src/components/map/Map.js b/src/components/map/Map.js index 4cd04f1..9df0541 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -1,14 +1,17 @@ -import React, { useState, useContext } from "react"; +import React, { useState, useContext, useEffect } from "react"; import MapControls from "./MapControls"; import MapInteraction from "./MapInteraction"; import MapToken from "./MapToken"; +import MapDrawing from "./MapDrawing"; import TokenDataContext from "../../contexts/TokenDataContext"; import TokenMenu from "../token/TokenMenu"; import TokenDragOverlay from "../token/TokenDragOverlay"; +import { omit } from "../../helpers/shared"; + function Map({ map, mapState, @@ -84,6 +87,43 @@ function Map({ const [mapShapes, setMapShapes] = useState([]); const [fogShapes, setFogShapes] = useState([]); + // Replay the draw actions and convert them to shapes for the map drawing + useEffect(() => { + if (!mapState) { + return; + } + function actionsToShapes(actions, actionIndex) { + let shapesById = {}; + for (let i = 0; i <= actionIndex; i++) { + const action = actions[i]; + if (action.type === "add" || action.type === "edit") { + for (let shape of action.shapes) { + shapesById[shape.id] = shape; + } + } + if (action.type === "remove") { + shapesById = omit(shapesById, action.shapeIds); + } + } + return Object.values(shapesById); + } + + setMapShapes( + actionsToShapes(mapState.mapDrawActions, mapState.mapDrawActionIndex) + ); + setFogShapes( + actionsToShapes(mapState.fogDrawActions, mapState.fogDrawActionIndex) + ); + }, [mapState]); + + function handleMapShapeAdd(shape) { + onMapDraw({ type: "add", shapes: [shape] }); + } + + function handleMapShapeRemove(shapeId) { + onMapDraw({ type: "remove", shapeIds: [shapeId] }); + } + const disabledControls = []; if (!allowMapDrawing) { disabledControls.push("brush"); @@ -182,6 +222,17 @@ function Map({ /> ); + const mapDrawing = ( + + ); + return ( } + selectedToolId={selectedToolId} > + {mapDrawing} {mapTokens} ); diff --git a/src/components/map/MapDrawing.js b/src/components/map/MapDrawing.js index fbc5416..8f446f8 100644 --- a/src/components/map/MapDrawing.js +++ b/src/components/map/MapDrawing.js @@ -1,123 +1,89 @@ -import React, { useRef, useEffect, useState, useContext } from "react"; +import React, { useContext, useEffect, useState } from "react"; import shortid from "shortid"; +import { Group, Line, Rect, Circle } from "react-konva"; + +import MapInteractionContext from "../../contexts/MapInteractionContext"; import { compare as comparePoints } from "../../helpers/vector2"; import { getBrushPositionForTool, getDefaultShapeData, getUpdatedShapeData, - isShapeHovered, - drawShape, simplifyPoints, - getRelativePointerPosition, + getStrokeWidth, } from "../../helpers/drawing"; -import MapInteractionContext from "../../contexts/MapInteractionContext"; +import colors from "../../helpers/colors"; function MapDrawing({ - width, - height, - selectedTool, - toolSettings, shapes, onShapeAdd, onShapeRemove, + selectedToolId, + selectedToolSettings, gridSize, }) { - const canvasRef = useRef(); - const containerRef = useRef(); - - const [isPointerDown, setIsPointerDown] = useState(false); + const { + stageDragState, + mapDragPosition, + stageScale, + mapWidth, + mapHeight, + } = useContext(MapInteractionContext); const [drawingShape, setDrawingShape] = useState(null); - const [pointerPosition, setPointerPosition] = useState({ x: -1, y: -1 }); - const shouldHover = selectedTool === "erase"; + const shouldHover = selectedToolId === "erase"; const isEditing = - selectedTool === "brush" || - selectedTool === "shape" || - selectedTool === "erase"; + selectedToolId === "brush" || + selectedToolId === "shape" || + selectedToolId === "erase"; - const { scaleRef } = useContext(MapInteractionContext); - - // Reset pointer position when tool changes useEffect(() => { - setPointerPosition({ x: -1, y: -1 }); - }, [selectedTool]); - - function handleStart(event) { if (!isEditing) { 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, - selectedTool, - toolSettings, - gridSize, - shapes - ); - const commonShapeData = { - color: toolSettings && toolSettings.color, - blend: toolSettings && toolSettings.useBlending, - id: shortid.generate(), - }; - if (selectedTool === "brush") { - setDrawingShape({ - type: "path", - pathType: toolSettings.type, - data: { points: [brushPosition] }, - strokeWidth: toolSettings.type === "stroke" ? 1 : 0, - ...commonShapeData, - }); - } else if (selectedTool === "shape") { - setDrawingShape({ - type: "shape", - shapeType: toolSettings.type, - data: getDefaultShapeData(toolSettings.type, brushPosition), - strokeWidth: 0, - ...commonShapeData, - }); - } - } - function handleMove(event) { - if (!isEditing) { - return; - } - if (event.touches && event.touches.length !== 1) { - return; - } - const pointer = event.touches ? event.touches[0] : event; - // Set pointer position every frame for erase tool and fog - if (shouldHover) { - const position = getRelativePointerPosition( - pointer, - containerRef.current - ); - setPointerPosition(position); - } - if (isPointerDown) { - const position = getRelativePointerPosition( - pointer, - containerRef.current - ); - setPointerPosition(position); + function startShape() { const brushPosition = getBrushPositionForTool( - position, - selectedTool, - toolSettings, + mapDragPosition, + selectedToolId, + selectedToolSettings, gridSize, shapes ); - if (selectedTool === "brush") { + const commonShapeData = { + color: selectedToolSettings && selectedToolSettings.color, + blend: selectedToolSettings && selectedToolSettings.useBlending, + id: shortid.generate(), + }; + if (selectedToolId === "brush") { + setDrawingShape({ + type: "path", + pathType: selectedToolSettings.type, + data: { points: [brushPosition] }, + strokeWidth: selectedToolSettings.type === "stroke" ? 1 : 0, + ...commonShapeData, + }); + } else if (selectedToolId === "shape") { + setDrawingShape({ + type: "shape", + shapeType: selectedToolSettings.type, + data: getDefaultShapeData(selectedToolSettings.type, brushPosition), + strokeWidth: 0, + ...commonShapeData, + }); + } + } + + function continueShape() { + const brushPosition = getBrushPositionForTool( + mapDragPosition, + selectedToolId, + selectedToolSettings, + gridSize, + shapes + ); + if (selectedToolId === "brush") { setDrawingShape((prevShape) => { const prevPoints = prevShape.data.points; if ( @@ -132,14 +98,14 @@ function MapDrawing({ const simplified = simplifyPoints( [...prevPoints, brushPosition], gridSize, - scaleRef.current + stageScale ); return { ...prevShape, data: { points: simplified }, }; }); - } else if (selectedTool === "shape") { + } else if (selectedToolId === "shape") { setDrawingShape((prevShape) => ({ ...prevShape, data: getUpdatedShapeData( @@ -151,110 +117,145 @@ function MapDrawing({ })); } } - } - function handleStop(event) { - if (!isEditing) { - return; - } - if (event.touches && event.touches.length !== 0) { - return; - } - if (selectedTool === "brush" && drawingShape) { - if (drawingShape.data.points.length > 1) { + function endShape() { + if (selectedToolId === "brush" && drawingShape) { + if (drawingShape.data.points.length > 1) { + onShapeAdd(drawingShape); + } + } else if (selectedToolId === "shape" && drawingShape) { onShapeAdd(drawingShape); } - } else if (selectedTool === "shape" && drawingShape) { - onShapeAdd(drawingShape); + setDrawingShape(null); } - if (selectedTool === "erase" && hoveredShapeRef.current && isPointerDown) { - onShapeRemove(hoveredShapeRef.current.id); - } - setIsPointerDown(false); - setDrawingShape(null); - } - - // 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; - } - } - 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; + switch (stageDragState) { + case "first": + startShape(); + return; + case "dragging": + continueShape(); + return; + case "last": + endShape(); + return; + default: + return; } }, [ - shapes, - width, - height, - pointerPosition, - isPointerDown, - selectedTool, - drawingShape, + stageDragState, + mapDragPosition, + selectedToolId, + selectedToolSettings, + isEditing, gridSize, - shouldHover, + stageScale, + onShapeAdd, + shapes, + drawingShape, ]); + function handleShapeClick(_, shape) { + if (selectedToolId === "erase") { + onShapeRemove(shape.id); + } + } + + function handleShapeMouseOver(event, shape) { + if (shouldHover) { + const path = event.target; + const hoverColor = "#BB99FF"; + path.fill(hoverColor); + if (shape.type === "path") { + path.stroke(hoverColor); + } + path.getLayer().draw(); + } + } + + function handleShapeMouseOut(event, shape) { + if (shouldHover) { + const path = event.target; + const color = colors[shape.color] || shape.color; + path.fill(color); + if (shape.type === "path") { + path.stroke(color); + } + path.getLayer().draw(); + } + } + + function renderShape(shape) { + const defaultProps = { + key: shape.id, + onMouseOver: (e) => handleShapeMouseOver(e, shape), + onMouseOut: (e) => handleShapeMouseOut(e, shape), + onClick: (e) => handleShapeClick(e, shape), + fill: colors[shape.color] || shape.color, + opacity: shape.blend ? 0.5 : 1, + }; + if (shape.type === "path") { + return ( + [...acc, point.x * mapWidth, point.y * mapHeight], + [] + )} + stroke={colors[shape.color] || shape.color} + tension={0.5} + closed={shape.pathType === "fill"} + fillEnabled={shape.pathType === "fill"} + lineCap="round" + strokeWidth={getStrokeWidth( + shape.strokeWidth, + gridSize, + mapWidth, + mapHeight + )} + {...defaultProps} + /> + ); + } else if (shape.type === "shape") { + if (shape.shapeType === "rectangle") { + return ( + + ); + } else if (shape.shapeType === "circle") { + const minSide = mapWidth < mapHeight ? mapWidth : mapHeight; + return ( + + ); + } else if (shape.shapeType === "triangle") { + return ( + [...acc, point.x * mapWidth, point.y * mapHeight], + [] + )} + closed={true} + {...defaultProps} + /> + ); + } + } + } + return ( -
- -
+ + {shapes.map(renderShape)} + {drawingShape && renderShape(drawingShape)} + ); } diff --git a/src/components/map/MapInteraction.js b/src/components/map/MapInteraction.js index 142c393..7227d27 100644 --- a/src/components/map/MapInteraction.js +++ b/src/components/map/MapInteraction.js @@ -18,7 +18,7 @@ const zoomSpeed = -0.001; const minZoom = 0.1; const maxZoom = 5; -function MapInteraction({ map, children, controls }) { +function MapInteraction({ map, children, controls, selectedToolId }) { const mapSource = useDataSource(map, defaultMapSources); const [mapSourceImage] = useImage(mapSource); @@ -26,7 +26,10 @@ function MapInteraction({ map, children, controls }) { const [stageHeight, setStageHeight] = useState(1); const [stageScale, setStageScale] = useState(1); const [stageTranslate, setStageTranslate] = useState({ x: 0, y: 0 }); + // "none" | "first" | "dragging" | "last" + const [stageDragState, setStageDragState] = useState("none"); const [preventMapInteraction, setPreventMapInteraction] = useState(false); + const [mapDragPosition, setMapDragPosition] = useState({ x: 0, y: 0 }); const stageWidthRef = useRef(stageWidth); const stageHeightRef = useRef(stageHeight); @@ -40,6 +43,25 @@ function MapInteraction({ map, children, controls }) { } }, [map]); + // Convert a client space XY to be normalized to the map image + function getMapDragPosition(xy) { + const [x, y] = xy; + const container = containerRef.current; + const mapImage = mapImageRef.current; + if (container && mapImage) { + const containerRect = container.getBoundingClientRect(); + const mapRect = mapImage.getClientRect(); + + const offsetX = x - containerRect.left - mapRect.x; + const offsetY = y - containerRect.top - mapRect.y; + + const normalizedX = offsetX / mapRect.width; + const normalizedY = offsetY / mapRect.height; + + return { x: normalizedX, y: normalizedY }; + } + } + const bind = useGesture({ onWheel: ({ delta }) => { const newScale = Math.min( @@ -49,16 +71,25 @@ function MapInteraction({ map, children, controls }) { setStageScale(newScale); stageScaleRef.current = newScale; }, - onDrag: ({ delta }) => { - if (!preventMapInteraction) { + onDrag: ({ delta, xy, first, last }) => { + if (preventMapInteraction) { + return; + } + setMapDragPosition(getMapDragPosition(xy)); + setStageDragState(first ? "first" : last ? "last" : "dragging"); + const [dx, dy] = delta; + if (selectedToolId === "pan") { const newTranslate = { - x: stageTranslate.x + delta[0] / stageScale, - y: stageTranslate.y + delta[1] / stageScale, + x: stageTranslate.x + dx / stageScale, + y: stageTranslate.y + dy / stageScale, }; setStageTranslate(newTranslate); stageTranslateRef.current = newTranslate; } }, + onDragEnd: () => { + setStageDragState("none"); + }, }); function handleResize(width, height) { @@ -75,6 +106,7 @@ function MapInteraction({ map, children, controls }) { const mapHeight = map ? stageWidth * (map.height / map.width) : stageHeight; const mapStageRef = useContext(MapStageContext); + const mapImageRef = useRef(); const auth = useContext(AuthContext); @@ -83,9 +115,11 @@ function MapInteraction({ map, children, controls }) { stageScale, stageWidth, stageHeight, + stageDragState, setPreventMapInteraction, mapWidth, mapHeight, + mapDragPosition, }; return ( @@ -111,6 +145,7 @@ function MapInteraction({ map, children, controls }) { width={mapWidth} height={mapHeight} id="mapImage" + ref={mapImageRef} /> {/* Forward auth context to konva elements */} diff --git a/src/helpers/drawing.js b/src/helpers/drawing.js index 89a192b..4be7c41 100644 --- a/src/helpers/drawing.js +++ b/src/helpers/drawing.js @@ -140,13 +140,13 @@ export function getUpdatedShapeData(type, data, brushPosition, gridSize) { } } -const defaultStrokeSize = 1 / 10; -export function getStrokeSize(multiplier, gridSize, canvasWidth, canvasHeight) { +const defaultStrokeWidth = 1 / 10; +export function getStrokeWidth(multiplier, gridSize, mapWidth, mapHeight) { const gridPixelSize = Vector2.multiply(gridSize, { - x: canvasWidth, - y: canvasHeight, + x: mapWidth, + y: mapHeight, }); - return Vector2.min(gridPixelSize) * defaultStrokeSize * multiplier; + return Vector2.min(gridPixelSize) * defaultStrokeWidth * multiplier; } export function shapeHasFill(shape) { @@ -330,7 +330,7 @@ export function drawShape(shape, context, gridSize, canvasWidth, canvasHeight) { context.strokeStyle = color; if (shape.strokeWidth > 0) { context.lineCap = "round"; - context.lineWidth = getStrokeSize( + context.lineWidth = getStrokeWidth( shape.strokeWidth, gridSize, canvasWidth,