diff --git a/src/components/konva/Selection.tsx b/src/components/konva/Selection.tsx index 6846b83..439a854 100644 --- a/src/components/konva/Selection.tsx +++ b/src/components/konva/Selection.tsx @@ -16,22 +16,26 @@ import { Selection as SelectionType, SelectionItemType, } from "../../types/Select"; -import { useRef } from "react"; +import { useEffect, useRef } from "react"; import Vector2 from "../../helpers/Vector2"; import { SelectionItemsChangeEventHandler } from "../../types/Events"; import { TokenState } from "../../types/TokenState"; import { Note } from "../../types/Note"; +const dashAnimationSpeed = -0.01; + type SelectionProps = { selection: SelectionType; onSelectionChange: (selection: SelectionType | null) => void; onSelectionItemsChange: SelectionItemsChangeEventHandler; + onPreventSelectionChange: (preventSelection: boolean) => void; } & Konva.ShapeConfig; function Selection({ selection, onSelectionChange, onSelectionItemsChange, + onPreventSelectionChange, ...props }: SelectionProps) { const userId = useUserId(); @@ -97,26 +101,69 @@ function Selection({ y: event.target.y() / mapHeight, }); intersectingNodesRef.current = []; + onPreventSelectionChange(false); } - function handleClick() { - onSelectionChange(null); + function handlePointerDown() { + onPreventSelectionChange(true); } - const strokeWidth = gridStrokeWidth / stageScale; + function handlePointerUp() { + onPreventSelectionChange(false); + } + + const hasItems = selection.items.length > 0; + + const requestRef = useRef(); + const lineRef = useRef(null); + const rectRef = useRef(null); + useEffect(() => { + let prevTime = performance.now(); + function animate(time: number) { + const delta = time - prevTime; + prevTime = time; + if (!hasItems) { + return; + } + requestRef.current = requestAnimationFrame(animate); + if (lineRef.current) { + lineRef.current.dashOffset( + lineRef.current.dashOffset() + delta * dashAnimationSpeed + ); + } + if (rectRef.current) { + rectRef.current.dashOffset( + rectRef.current.dashOffset() + delta * dashAnimationSpeed + ); + } + } + + requestRef.current = requestAnimationFrame(animate); + + return () => { + if (requestRef.current !== undefined) { + cancelAnimationFrame(requestRef.current); + } + }; + }, [hasItems]); + + const strokeWidth = (gridStrokeWidth * 0.75) / stageScale; const defaultProps = { stroke: colors.primary, strokeWidth: strokeWidth, - dash: [strokeWidth / 2, strokeWidth * 2], + dash: hasItems ? [strokeWidth / 2, strokeWidth * 2] : [], onDragStart: handleDragStart, onDragMove: handleDragMove, onDragEnd: handleDragEnd, draggable: true, - onClick: handleClick, - onTap: handleClick, + onMouseDown: handlePointerDown, + onMouseUp: handlePointerUp, + onTouchStart: handlePointerDown, + onTouchEnd: handlePointerUp, }; const x = selection.x * mapWidth; const y = selection.y * mapHeight; + if (selection.type === "path") { return ( 0} + closed={hasItems} lineCap="round" lineJoin="round" x={x} y={y} {...defaultProps} {...props} + ref={lineRef} /> ); } else { @@ -147,6 +195,7 @@ function Selection({ lineJoin="round" {...defaultProps} {...props} + ref={rectRef} /> ); } diff --git a/src/components/tools/SelectTool.tsx b/src/components/tools/SelectTool.tsx index b6b7e41..0ac5317 100644 --- a/src/components/tools/SelectTool.tsx +++ b/src/components/tools/SelectTool.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { Group } from "react-konva"; import { @@ -54,6 +54,10 @@ function SelectTool({ const [selection, setSelection] = useState(null); const [isBrushDown, setIsBrushDown] = useState(false); + // Use a ref here to prevent case where brush down event + // would fire before React state was refreshed + const preventSelectionRef = useRef(false); + useEffect(() => { if (!active) { return; @@ -77,7 +81,7 @@ function SelectTool({ function handleBrushDown() { const brushPosition = getBrushPosition(); - if (!brushPosition || selection) { + if (!brushPosition || preventSelectionRef.current) { return; } if (toolSettings.type === "path") { @@ -102,7 +106,10 @@ function SelectTool({ function handleBrushMove() { const brushPosition = getBrushPosition(); - if (isBrushDown && selection && brushPosition && mapImage) { + if (!brushPosition || preventSelectionRef.current) { + return; + } + if (isBrushDown && selection && mapImage) { if (selection.type === "path") { setSelection((prevSelection) => { if (prevSelection?.type !== "path") { @@ -149,6 +156,9 @@ function SelectTool({ } function handleBrushUp() { + if (preventSelectionRef.current) { + return; + } if (selection && mapStage) { const tokensGroup = mapStage.findOne("#tokens"); const notesGroup = mapStage.findOne("#notes"); @@ -223,14 +233,23 @@ function SelectTool({ setIsBrushDown(false); } + function handlePointerClick() { + if (preventSelectionRef.current) { + return; + } + setSelection(null); + } + interactionEmitter?.on("dragStart", handleBrushDown); interactionEmitter?.on("drag", handleBrushMove); interactionEmitter?.on("dragEnd", handleBrushUp); + mapStage?.on("click tap", handlePointerClick); return () => { interactionEmitter?.off("dragStart", handleBrushDown); interactionEmitter?.off("drag", handleBrushMove); interactionEmitter?.off("dragEnd", handleBrushUp); + mapStage?.off("click tap", handlePointerClick); }; }); @@ -241,6 +260,9 @@ function SelectTool({ selection={selection} onSelectionChange={setSelection} onSelectionItemsChange={onSelectionItemsChange} + onPreventSelectionChange={(prevent: boolean) => + (preventSelectionRef.current = prevent) + } /> )}