From df1e59f666de19d4dad50aaa27c99f153fa146df Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Sun, 25 Jul 2021 13:48:50 +1000 Subject: [PATCH] Add custom transformer --- src/components/konva/Token.tsx | 140 ++++++++++++++++----------- src/components/konva/Transformer.tsx | 113 +++++++++++++++++++++ src/components/token/TokenMenu.tsx | 54 ----------- src/hooks/useMapTokens.tsx | 4 + 4 files changed, 199 insertions(+), 112 deletions(-) create mode 100644 src/components/konva/Transformer.tsx diff --git a/src/components/konva/Token.tsx b/src/components/konva/Token.tsx index b273468..5246795 100644 --- a/src/components/konva/Token.tsx +++ b/src/components/konva/Token.tsx @@ -31,6 +31,7 @@ import { TokenMenuOpenChangeEventHandler, TokenStateChangeEventHandler, } from "../../types/Events"; +import Transformer from "./Transformer"; type MapTokenProps = { tokenState: TokenState; @@ -41,6 +42,7 @@ type MapTokenProps = { draggable: boolean; fadeOnHover: boolean; map: Map; + selected: boolean; }; function Token({ @@ -52,6 +54,7 @@ function Token({ draggable, fadeOnHover, map, + selected, }: MapTokenProps) { const userId = useUserId(); @@ -193,6 +196,22 @@ function Token({ } } + const tokenRef = useRef(null); + function handleTransformEnd(event: Konva.KonvaEventObject) { + if (tokenRef.current) { + const sizeChange = event.target.scaleX(); + const rotation = event.target.rotation(); + onTokenStateChange({ + [tokenState.id]: { + size: tokenState.size * sizeChange, + rotation: rotation, + }, + }); + tokenRef.current.scaleX(1); + tokenRef.current.scaleY(1); + } + } + const minCellSize = Math.min( gridCellPixelSize.width, gridCellPixelSize.height @@ -228,68 +247,73 @@ function Token({ } return ( - - + - - {}} + + + + {}} + /> + + {tokenState.statuses?.length > 0 ? ( + + ) : null} + + + + {tokenState.label ? ( + + ) : null} + + + - - {tokenState.statuses?.length > 0 ? ( - - ) : null} - {tokenState.label ? ( - - ) : null} - - + ); } diff --git a/src/components/konva/Transformer.tsx b/src/components/konva/Transformer.tsx new file mode 100644 index 0000000..6154bbd --- /dev/null +++ b/src/components/konva/Transformer.tsx @@ -0,0 +1,113 @@ +import Konva from "konva"; +import { Transform } from "konva/lib/Util"; +import { useEffect, useRef } from "react"; +import { Transformer as KonvaTransformer } from "react-konva"; + +import { useSetPreventMapInteraction } from "../../contexts/MapInteractionContext"; + +type ResizerProps = { + active: boolean; + nodeRef: React.RefObject; + onTransformStart?: (event: Konva.KonvaEventObject) => void; + onTransformEnd?: (event: Konva.KonvaEventObject) => void; +}; + +function Transformer({ + active, + nodeRef, + onTransformStart, + onTransformEnd, +}: ResizerProps) { + const setPreventMapInteraction = useSetPreventMapInteraction(); + + const transformerRef = useRef(null); + useEffect(() => { + if (active && transformerRef.current && nodeRef.current) { + // we need to attach transformer manually + transformerRef.current.nodes([nodeRef.current]); + transformerRef.current.getLayer()?.batchDraw(); + } + }, [active, nodeRef]); + + const movingAnchorRef = useRef(); + function handleTransformStart(e: Konva.KonvaEventObject) { + if (transformerRef.current) { + movingAnchorRef.current = transformerRef.current._movingAnchorName; + if (active) { + setPreventMapInteraction(true); + } + onTransformStart && onTransformStart(e); + } + } + + function handleTransformEnd(e: Konva.KonvaEventObject) { + if (active) { + setPreventMapInteraction(false); + } + onTransformEnd && onTransformEnd(e); + } + + if (!active) { + return null; + } + + return ( + { + let snapBox = { ...newBox }; + const movingAnchor = movingAnchorRef.current; + if (movingAnchor === "middle-left" || movingAnchor === "middle-right") { + const deltaWidth = newBox.width - oldBox.width; + // Account for node ratio + const inverseRatio = + Math.round(oldBox.height) / Math.round(oldBox.width); + const deltaHeight = inverseRatio * deltaWidth; + + // Account for node rotation + // Create a transform to unrotate the x,y position of the Box + const rotator = new Transform(); + rotator.rotate(-snapBox.rotation); + + // Unrotate and add the resize amount + let rotatedMin = rotator.point({ x: snapBox.x, y: snapBox.y }); + rotatedMin.y = rotatedMin.y - deltaHeight / 2; + + // Rotated back + rotator.invert(); + rotatedMin = rotator.point(rotatedMin); + + snapBox = { + ...snapBox, + height: snapBox.height + deltaHeight, + x: rotatedMin.x, + y: rotatedMin.y, + }; + } + + if (snapBox.width < 5 || snapBox.height < 5) { + return oldBox; + } + return snapBox; + }} + onTransformStart={handleTransformStart} + onTransformEnd={handleTransformEnd} + centeredScaling={true} + rotationSnaps={[...Array(24).keys()].map((n) => n * 15)} + rotateAnchorOffset={20} + anchorCornerRadius={10} + enabledAnchors={["middle-left", "middle-right"]} + flipEnabled={false} + ignoreStroke={true} + borderStroke="transparent" + anchorStroke="hsl(210, 50%, 96%" + anchorFill="hsla(230, 25%, 15%, 80%)" + anchorStrokeWidth={3} + borderStrokeWidth={2} + anchorSize={15} + useSingleNodeRotation={true} + /> + ); +} + +export default Transformer; diff --git a/src/components/token/TokenMenu.tsx b/src/components/token/TokenMenu.tsx index b4e8031..fc8751c 100644 --- a/src/components/token/TokenMenu.tsx +++ b/src/components/token/TokenMenu.tsx @@ -2,8 +2,6 @@ import React, { useEffect, useState } from "react"; import { Box, Input, Flex, Text, IconButton } from "theme-ui"; import Konva from "konva"; -import Slider from "../Slider"; - import MapMenu from "../map/MapMenu"; import colors, { Color, colorOptions } from "../../helpers/colors"; @@ -33,7 +31,6 @@ type TokenMenuProps = { map: Map | null; }; -const defaultTokenMaxSize = 6; function TokenMenu({ isOpen, onRequestClose, @@ -46,12 +43,10 @@ function TokenMenu({ const wasOpen = usePrevious(isOpen); - const [tokenMaxSize, setTokenMaxSize] = useState(defaultTokenMaxSize); const [menuLeft, setMenuLeft] = useState(0); const [menuTop, setMenuTop] = useState(0); useEffect(() => { if (isOpen && !wasOpen && tokenState) { - setTokenMaxSize(Math.max(tokenState.size, defaultTokenMaxSize)); // Update menu position if (tokenImage) { const imageRect = tokenImage.getClientRect(); @@ -89,19 +84,6 @@ function TokenMenu({ }); } - function handleSizeChange(event: React.ChangeEvent) { - const newSize = parseFloat(event.target.value); - tokenState && onTokenStateChange({ [tokenState.id]: { size: newSize } }); - } - - function handleRotationChange(event: React.ChangeEvent) { - const newRotation = parseInt(event.target.value); - tokenState && - onTokenStateChange({ - [tokenState.id]: { rotation: newRotation }, - }); - } - function handleVisibleChange() { tokenState && onTokenStateChange({ @@ -223,42 +205,6 @@ function TokenMenu({ ))} - - - Size: - - - - - - Rotate: - - - {/* Only show hide and lock token actions to map owners */} {map && map.owner === userId && ( diff --git a/src/hooks/useMapTokens.tsx b/src/hooks/useMapTokens.tsx index da67183..666e446 100644 --- a/src/hooks/useMapTokens.tsx +++ b/src/hooks/useMapTokens.tsx @@ -96,6 +96,10 @@ function useMapTokens( } fadeOnHover={selectedToolId === "drawing"} map={map} + selected={ + !!tokenMenuOptions && + tokenMenuOptions.tokenStateId === tokenState.id + } /> ))}