Changed map drag position to use a ref value to avoid re-renders

Added a useMapBrush helper
This commit is contained in:
Mitchell McCaffrey 2020-05-25 15:07:12 +10:00
parent 8932ceb1e3
commit b0c1dcf9dd
5 changed files with 285 additions and 224 deletions

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from "react"; import React, { useContext, useState, useCallback } from "react";
import shortid from "shortid"; import shortid from "shortid";
import { Group, Line, Rect, Circle } from "react-konva"; import { Group, Line, Rect, Circle } from "react-konva";
@ -14,6 +14,7 @@ import {
} from "../../helpers/drawing"; } from "../../helpers/drawing";
import colors from "../../helpers/colors"; import colors from "../../helpers/colors";
import useMapBrush from "../../helpers/useMapBrush";
function MapDrawing({ function MapDrawing({
shapes, shapes,
@ -23,13 +24,7 @@ function MapDrawing({
selectedToolSettings, selectedToolSettings,
gridSize, gridSize,
}) { }) {
const { const { stageScale, mapWidth, mapHeight } = useContext(MapInteractionContext);
stageDragState,
mapDragPosition,
stageScale,
mapWidth,
mapHeight,
} = useContext(MapInteractionContext);
const [drawingShape, setDrawingShape] = useState(null); const [drawingShape, setDrawingShape] = useState(null);
const shouldHover = selectedToolId === "erase"; const shouldHover = selectedToolId === "erase";
@ -38,122 +33,120 @@ function MapDrawing({
selectedToolId === "shape" || selectedToolId === "shape" ||
selectedToolId === "erase"; selectedToolId === "erase";
useEffect(() => { const handleShapeDraw = useCallback(
if (!isEditing) { (brushState, mapBrushPosition) => {
return; function startShape() {
} const brushPosition = getBrushPositionForTool(
mapBrushPosition,
function startShape() { selectedToolId,
const brushPosition = getBrushPositionForTool( selectedToolSettings,
mapDragPosition, gridSize,
selectedToolId, shapes
selectedToolSettings, );
gridSize, const commonShapeData = {
shapes color: selectedToolSettings && selectedToolSettings.color,
); blend: selectedToolSettings && selectedToolSettings.useBlending,
const commonShapeData = { id: shortid.generate(),
color: selectedToolSettings && selectedToolSettings.color, };
blend: selectedToolSettings && selectedToolSettings.useBlending, if (selectedToolId === "brush") {
id: shortid.generate(), setDrawingShape({
}; type: "path",
if (selectedToolId === "brush") { pathType: selectedToolSettings.type,
setDrawingShape({ data: { points: [brushPosition] },
type: "path", strokeWidth: selectedToolSettings.type === "stroke" ? 1 : 0,
pathType: selectedToolSettings.type, ...commonShapeData,
data: { points: [brushPosition] }, });
strokeWidth: selectedToolSettings.type === "stroke" ? 1 : 0, } else if (selectedToolId === "shape") {
...commonShapeData, setDrawingShape({
}); type: "shape",
} else if (selectedToolId === "shape") { shapeType: selectedToolSettings.type,
setDrawingShape({ data: getDefaultShapeData(selectedToolSettings.type, brushPosition),
type: "shape", strokeWidth: 0,
shapeType: selectedToolSettings.type, ...commonShapeData,
data: getDefaultShapeData(selectedToolSettings.type, brushPosition), });
strokeWidth: 0, }
...commonShapeData,
});
} }
}
function continueShape() { function continueShape() {
const brushPosition = getBrushPositionForTool( const brushPosition = getBrushPositionForTool(
mapDragPosition, mapBrushPosition,
selectedToolId, selectedToolId,
selectedToolSettings, selectedToolSettings,
gridSize, gridSize,
shapes shapes
); );
if (selectedToolId === "brush") { if (selectedToolId === "brush") {
setDrawingShape((prevShape) => { setDrawingShape((prevShape) => {
const prevPoints = prevShape.data.points; const prevPoints = prevShape.data.points;
if ( if (
comparePoints( comparePoints(
prevPoints[prevPoints.length - 1], prevPoints[prevPoints.length - 1],
brushPosition, brushPosition,
0.001 0.001
) )
) { ) {
return prevShape; return prevShape;
} }
const simplified = simplifyPoints( const simplified = simplifyPoints(
[...prevPoints, brushPosition], [...prevPoints, brushPosition],
gridSize, gridSize,
stageScale stageScale
); );
return { return {
...prevShape,
data: { points: simplified },
};
});
} else if (selectedToolId === "shape") {
setDrawingShape((prevShape) => ({
...prevShape, ...prevShape,
data: { points: simplified }, data: getUpdatedShapeData(
}; prevShape.shapeType,
}); prevShape.data,
} else if (selectedToolId === "shape") { brushPosition,
setDrawingShape((prevShape) => ({ gridSize
...prevShape, ),
data: getUpdatedShapeData( }));
prevShape.shapeType, }
prevShape.data,
brushPosition,
gridSize
),
}));
} }
}
function endShape() { function endShape() {
if (selectedToolId === "brush" && drawingShape) { if (selectedToolId === "brush" && drawingShape) {
if (drawingShape.data.points.length > 1) { if (drawingShape.data.points.length > 1) {
onShapeAdd(drawingShape);
}
} else if (selectedToolId === "shape" && drawingShape) {
onShapeAdd(drawingShape); onShapeAdd(drawingShape);
} }
} else if (selectedToolId === "shape" && drawingShape) { setDrawingShape(null);
onShapeAdd(drawingShape);
} }
setDrawingShape(null);
}
switch (stageDragState) { switch (brushState) {
case "first": case "first":
startShape(); startShape();
return; return;
case "dragging": case "drawing":
continueShape(); continueShape();
return; return;
case "last": case "last":
endShape(); endShape();
return; return;
default: default:
return; return;
} }
}, [ },
stageDragState, [
mapDragPosition, selectedToolId,
selectedToolId, selectedToolSettings,
selectedToolSettings, gridSize,
isEditing, stageScale,
gridSize, onShapeAdd,
stageScale, shapes,
onShapeAdd, drawingShape,
shapes, ]
drawingShape, );
]);
useMapBrush(isEditing, handleShapeDraw);
function handleShapeClick(_, shape) { function handleShapeClick(_, shape) {
if (selectedToolId === "erase") { if (selectedToolId === "erase") {

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from "react"; import React, { useContext, useState, useCallback } from "react";
import shortid from "shortid"; import shortid from "shortid";
import { Group, Line } from "react-konva"; import { Group, Line } from "react-konva";
import useImage from "use-image"; import useImage from "use-image";
@ -15,6 +15,7 @@ import {
} from "../../helpers/drawing"; } from "../../helpers/drawing";
import colors from "../../helpers/colors"; import colors from "../../helpers/colors";
import useMapBrush from "../../helpers/useMapBrush";
function MapFog({ function MapFog({
shapes, shapes,
@ -25,13 +26,7 @@ function MapFog({
selectedToolSettings, selectedToolSettings,
gridSize, gridSize,
}) { }) {
const { const { stageScale, mapWidth, mapHeight } = useContext(MapInteractionContext);
stageDragState,
mapDragPosition,
stageScale,
mapWidth,
mapHeight,
} = useContext(MapInteractionContext);
const [drawingShape, setDrawingShape] = useState(null); const [drawingShape, setDrawingShape] = useState(null);
const isEditing = selectedToolId === "fog"; const isEditing = selectedToolId === "fog";
@ -42,105 +37,103 @@ function MapFog({
const [patternImage] = useImage(diagonalPattern); const [patternImage] = useImage(diagonalPattern);
useEffect(() => { const handleShapeDraw = useCallback(
if (!isEditing) { (brushState, mapBrushPosition) => {
return; function startShape() {
} const brushPosition = getBrushPositionForTool(
mapBrushPosition,
function startShape() { selectedToolId,
const brushPosition = getBrushPositionForTool( selectedToolSettings,
mapDragPosition, gridSize,
selectedToolId, shapes
selectedToolSettings, );
gridSize, if (selectedToolSettings.type === "add") {
shapes setDrawingShape({
); type: "fog",
if (selectedToolSettings.type === "add") { data: { points: [brushPosition] },
setDrawingShape({ strokeWidth: 0.5,
type: "fog", color: "black",
data: { points: [brushPosition] }, blend: false,
strokeWidth: 0.5, id: shortid.generate(),
color: "black", visible: true,
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);
} }
} }
setDrawingShape(null);
}
switch (stageDragState) { function continueShape() {
case "first": const brushPosition = getBrushPositionForTool(
startShape(); mapBrushPosition,
return; selectedToolId,
case "dragging": selectedToolSettings,
continueShape(); gridSize,
return; shapes
case "last": );
endShape(); if (selectedToolSettings.type === "add") {
return; setDrawingShape((prevShape) => {
default: const prevPoints = prevShape.data.points;
return; if (
} comparePoints(
}, [ prevPoints[prevPoints.length - 1],
stageDragState, brushPosition,
mapDragPosition, 0.001
selectedToolId, )
selectedToolSettings, ) {
isEditing, return prevShape;
gridSize, }
stageScale, return {
onShapeAdd, ...prevShape,
shapes, data: { points: [...prevPoints, brushPosition] },
drawingShape, };
]); });
}
}
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) { function handleShapeClick(_, shape) {
if (!isEditing) { if (!isEditing) {

View File

@ -29,12 +29,12 @@ function MapInteraction({ map, children, controls, selectedToolId }) {
// "none" | "first" | "dragging" | "last" // "none" | "first" | "dragging" | "last"
const [stageDragState, setStageDragState] = useState("none"); const [stageDragState, setStageDragState] = useState("none");
const [preventMapInteraction, setPreventMapInteraction] = useState(false); const [preventMapInteraction, setPreventMapInteraction] = useState(false);
const [mapDragPosition, setMapDragPosition] = useState({ x: 0, y: 0 });
const stageWidthRef = useRef(stageWidth); const stageWidthRef = useRef(stageWidth);
const stageHeightRef = useRef(stageHeight); const stageHeightRef = useRef(stageHeight);
// Avoid state udpates when panning the map by using a ref and updating the konva element directly // 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 stageTranslateRef = useRef({ x: 0, y: 0 });
const mapDragPositionRef = useRef({ x: 0, y: 0 });
useEffect(() => { useEffect(() => {
const layer = mapLayerRef.current; const layer = mapLayerRef.current;
@ -103,9 +103,11 @@ function MapInteraction({ map, children, controls, selectedToolId }) {
layer.y(newTranslate.y); layer.y(newTranslate.y);
layer.draw(); layer.draw();
stageTranslateRef.current = newTranslate; stageTranslateRef.current = newTranslate;
} else { }
setMapDragPosition(getMapDragPosition(xy)); mapDragPositionRef.current = getMapDragPosition(xy);
setStageDragState(first ? "first" : last ? "last" : "dragging"); const newDragState = first ? "first" : last ? "last" : "dragging";
if (stageDragState !== newDragState) {
setStageDragState(newDragState);
} }
}, },
onDragEnd: () => { onDragEnd: () => {
@ -153,7 +155,7 @@ function MapInteraction({ map, children, controls, selectedToolId }) {
setPreventMapInteraction, setPreventMapInteraction,
mapWidth, mapWidth,
mapHeight, mapHeight,
mapDragPosition, mapDragPositionRef,
}; };
return ( return (

View File

@ -4,9 +4,11 @@ const MapInteractionContext = React.createContext({
stageScale: 1, stageScale: 1,
stageWidth: 1, stageWidth: 1,
stageHeight: 1, stageHeight: 1,
stageDragState: "none",
setPreventMapInteraction: () => {}, setPreventMapInteraction: () => {},
mapWidth: 1, mapWidth: 1,
mapHeight: 1, mapHeight: 1,
mapDragPositionRef: { current: undefined },
}); });
export const MapInteractionProvider = MapInteractionContext.Provider; export const MapInteractionProvider = MapInteractionContext.Provider;

View File

@ -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;