From b0c1dcf9dd3e636f73d22c679bad8c9741623006 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Mon, 25 May 2020 15:07:12 +1000 Subject: [PATCH] Changed map drag position to use a ref value to avoid re-renders Added a useMapBrush helper --- src/components/map/MapDrawing.js | 223 +++++++++++++------------- src/components/map/MapFog.js | 201 +++++++++++------------ src/components/map/MapInteraction.js | 12 +- src/contexts/MapInteractionContext.js | 2 + src/helpers/useMapBrush.js | 71 ++++++++ 5 files changed, 285 insertions(+), 224 deletions(-) create mode 100644 src/helpers/useMapBrush.js diff --git a/src/components/map/MapDrawing.js b/src/components/map/MapDrawing.js index b4ab726..922b1e4 100644 --- a/src/components/map/MapDrawing.js +++ b/src/components/map/MapDrawing.js @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from "react"; +import React, { useContext, useState, useCallback } from "react"; import shortid from "shortid"; import { Group, Line, Rect, Circle } from "react-konva"; @@ -14,6 +14,7 @@ import { } from "../../helpers/drawing"; import colors from "../../helpers/colors"; +import useMapBrush from "../../helpers/useMapBrush"; function MapDrawing({ shapes, @@ -23,13 +24,7 @@ function MapDrawing({ selectedToolSettings, gridSize, }) { - const { - stageDragState, - mapDragPosition, - stageScale, - mapWidth, - mapHeight, - } = useContext(MapInteractionContext); + const { stageScale, mapWidth, mapHeight } = useContext(MapInteractionContext); const [drawingShape, setDrawingShape] = useState(null); const shouldHover = selectedToolId === "erase"; @@ -38,122 +33,120 @@ function MapDrawing({ selectedToolId === "shape" || selectedToolId === "erase"; - useEffect(() => { - if (!isEditing) { - return; - } - - function startShape() { - const brushPosition = getBrushPositionForTool( - mapDragPosition, - selectedToolId, - selectedToolSettings, - gridSize, - shapes - ); - 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, - }); + const handleShapeDraw = useCallback( + (brushState, mapBrushPosition) => { + function startShape() { + const brushPosition = getBrushPositionForTool( + mapBrushPosition, + selectedToolId, + selectedToolSettings, + gridSize, + shapes + ); + 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 ( - comparePoints( - prevPoints[prevPoints.length - 1], - brushPosition, - 0.001 - ) - ) { - return prevShape; - } - const simplified = simplifyPoints( - [...prevPoints, brushPosition], - gridSize, - stageScale - ); - return { + function continueShape() { + const brushPosition = getBrushPositionForTool( + mapBrushPosition, + selectedToolId, + selectedToolSettings, + gridSize, + shapes + ); + if (selectedToolId === "brush") { + setDrawingShape((prevShape) => { + const prevPoints = prevShape.data.points; + if ( + comparePoints( + prevPoints[prevPoints.length - 1], + brushPosition, + 0.001 + ) + ) { + return prevShape; + } + const simplified = simplifyPoints( + [...prevPoints, brushPosition], + gridSize, + stageScale + ); + return { + ...prevShape, + data: { points: simplified }, + }; + }); + } else if (selectedToolId === "shape") { + setDrawingShape((prevShape) => ({ ...prevShape, - data: { points: simplified }, - }; - }); - } else if (selectedToolId === "shape") { - setDrawingShape((prevShape) => ({ - ...prevShape, - data: getUpdatedShapeData( - prevShape.shapeType, - prevShape.data, - brushPosition, - gridSize - ), - })); + data: getUpdatedShapeData( + prevShape.shapeType, + prevShape.data, + brushPosition, + gridSize + ), + })); + } } - } - function endShape() { - if (selectedToolId === "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 (selectedToolId === "shape" && drawingShape) { - onShapeAdd(drawingShape); + setDrawingShape(null); } - 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, - ]); + switch (brushState) { + case "first": + startShape(); + return; + case "drawing": + continueShape(); + return; + case "last": + endShape(); + return; + default: + return; + } + }, + [ + selectedToolId, + selectedToolSettings, + gridSize, + stageScale, + onShapeAdd, + shapes, + drawingShape, + ] + ); + + useMapBrush(isEditing, handleShapeDraw); function handleShapeClick(_, shape) { if (selectedToolId === "erase") { diff --git a/src/components/map/MapFog.js b/src/components/map/MapFog.js index 6ef7a8a..fbb8782 100644 --- a/src/components/map/MapFog.js +++ b/src/components/map/MapFog.js @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from "react"; +import React, { useContext, useState, useCallback } from "react"; import shortid from "shortid"; import { Group, Line } from "react-konva"; import useImage from "use-image"; @@ -15,6 +15,7 @@ import { } from "../../helpers/drawing"; import colors from "../../helpers/colors"; +import useMapBrush from "../../helpers/useMapBrush"; function MapFog({ shapes, @@ -25,13 +26,7 @@ function MapFog({ selectedToolSettings, gridSize, }) { - const { - stageDragState, - mapDragPosition, - stageScale, - mapWidth, - mapHeight, - } = useContext(MapInteractionContext); + const { stageScale, mapWidth, mapHeight } = useContext(MapInteractionContext); const [drawingShape, setDrawingShape] = useState(null); const isEditing = selectedToolId === "fog"; @@ -42,105 +37,103 @@ function MapFog({ const [patternImage] = useImage(diagonalPattern); - useEffect(() => { - if (!isEditing) { - return; - } - - function startShape() { - const brushPosition = getBrushPositionForTool( - mapDragPosition, - selectedToolId, - selectedToolSettings, - gridSize, - shapes - ); - 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) => { - const prevPoints = prevShape.data.points; - if ( - comparePoints( - prevPoints[prevPoints.length - 1], - brushPosition, - 0.001 - ) - ) { - return prevShape; - } - return { - ...prevShape, - data: { points: [...prevPoints, brushPosition] }, - }; - }); - } - } - - 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); + const handleShapeDraw = useCallback( + (brushState, mapBrushPosition) => { + function startShape() { + const brushPosition = getBrushPositionForTool( + mapBrushPosition, + selectedToolId, + selectedToolSettings, + gridSize, + shapes + ); + if (selectedToolSettings.type === "add") { + setDrawingShape({ + type: "fog", + data: { points: [brushPosition] }, + strokeWidth: 0.5, + color: "black", + blend: false, + id: shortid.generate(), + visible: true, + }); } } - 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 continueShape() { + const brushPosition = getBrushPositionForTool( + mapBrushPosition, + selectedToolId, + selectedToolSettings, + gridSize, + shapes + ); + if (selectedToolSettings.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 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 (brushState) { + case "first": + startShape(); + return; + case "drawing": + continueShape(); + return; + case "last": + endShape(); + return; + default: + return; + } + }, + [ + selectedToolId, + selectedToolSettings, + gridSize, + stageScale, + onShapeAdd, + shapes, + drawingShape, + ] + ); + + useMapBrush(isEditing, handleShapeDraw); function handleShapeClick(_, shape) { if (!isEditing) { diff --git a/src/components/map/MapInteraction.js b/src/components/map/MapInteraction.js index b8359c6..7ae2791 100644 --- a/src/components/map/MapInteraction.js +++ b/src/components/map/MapInteraction.js @@ -29,12 +29,12 @@ function MapInteraction({ map, children, controls, selectedToolId }) { // "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); // Avoid state udpates when panning the map by using a ref and updating the konva element directly const stageTranslateRef = useRef({ x: 0, y: 0 }); + const mapDragPositionRef = useRef({ x: 0, y: 0 }); useEffect(() => { const layer = mapLayerRef.current; @@ -103,9 +103,11 @@ function MapInteraction({ map, children, controls, selectedToolId }) { layer.y(newTranslate.y); layer.draw(); stageTranslateRef.current = newTranslate; - } else { - setMapDragPosition(getMapDragPosition(xy)); - setStageDragState(first ? "first" : last ? "last" : "dragging"); + } + mapDragPositionRef.current = getMapDragPosition(xy); + const newDragState = first ? "first" : last ? "last" : "dragging"; + if (stageDragState !== newDragState) { + setStageDragState(newDragState); } }, onDragEnd: () => { @@ -153,7 +155,7 @@ function MapInteraction({ map, children, controls, selectedToolId }) { setPreventMapInteraction, mapWidth, mapHeight, - mapDragPosition, + mapDragPositionRef, }; return ( diff --git a/src/contexts/MapInteractionContext.js b/src/contexts/MapInteractionContext.js index 3b073fe..6b11577 100644 --- a/src/contexts/MapInteractionContext.js +++ b/src/contexts/MapInteractionContext.js @@ -4,9 +4,11 @@ const MapInteractionContext = React.createContext({ stageScale: 1, stageWidth: 1, stageHeight: 1, + stageDragState: "none", setPreventMapInteraction: () => {}, mapWidth: 1, mapHeight: 1, + mapDragPositionRef: { current: undefined }, }); export const MapInteractionProvider = MapInteractionContext.Provider; diff --git a/src/helpers/useMapBrush.js b/src/helpers/useMapBrush.js new file mode 100644 index 0000000..3c96f7d --- /dev/null +++ b/src/helpers/useMapBrush.js @@ -0,0 +1,71 @@ +import { useContext, useRef, useEffect } from "react"; + +import MapInteractionContext from "../contexts/MapInteractionContext"; + +import { compare } from "./vector2"; + +import usePrevious from "./usePrevious"; + +/** + * @callback onBrushUpdate + * @param {string} drawState "first" | "drawing" | "last" + * @param {Object} brushPosition the normalized x and y coordinates of the brush on the map + */ + +/** + * Helper to get the maps drag position as it changes + * @param {boolean} shouldUpdate + * @param {onBrushUpdate} onBrushUpdate + */ +function useMapBrush(shouldUpdate, onBrushUpdate) { + const { stageDragState, mapDragPositionRef } = useContext( + MapInteractionContext + ); + + const requestRef = useRef(); + const previousDragState = usePrevious(stageDragState); + const previousBrushPositionRef = useRef(mapDragPositionRef.current); + + useEffect(() => { + function updateBrush(forceUpdate) { + const drawState = + stageDragState === "dragging" ? "drawing" : stageDragState; + const brushPosition = mapDragPositionRef.current; + const previousBrushPostition = previousBrushPositionRef.current; + // Only update brush when it has moved + if ( + !compare(brushPosition, previousBrushPostition, 0.0001) || + forceUpdate + ) { + onBrushUpdate(drawState, brushPosition); + previousBrushPositionRef.current = brushPosition; + } + } + + function animate() { + if (!shouldUpdate) { + return; + } + requestRef.current = requestAnimationFrame(animate); + updateBrush(false); + } + + requestRef.current = requestAnimationFrame(animate); + + if (stageDragState !== previousDragState && shouldUpdate) { + updateBrush(true); + } + + return () => { + cancelAnimationFrame(requestRef.current); + }; + }, [ + shouldUpdate, + onBrushUpdate, + stageDragState, + mapDragPositionRef, + previousDragState, + ]); +} + +export default useMapBrush;