Add custom transformer
This commit is contained in:
parent
97f1294a23
commit
df1e59f666
@ -31,6 +31,7 @@ import {
|
|||||||
TokenMenuOpenChangeEventHandler,
|
TokenMenuOpenChangeEventHandler,
|
||||||
TokenStateChangeEventHandler,
|
TokenStateChangeEventHandler,
|
||||||
} from "../../types/Events";
|
} from "../../types/Events";
|
||||||
|
import Transformer from "./Transformer";
|
||||||
|
|
||||||
type MapTokenProps = {
|
type MapTokenProps = {
|
||||||
tokenState: TokenState;
|
tokenState: TokenState;
|
||||||
@ -41,6 +42,7 @@ type MapTokenProps = {
|
|||||||
draggable: boolean;
|
draggable: boolean;
|
||||||
fadeOnHover: boolean;
|
fadeOnHover: boolean;
|
||||||
map: Map;
|
map: Map;
|
||||||
|
selected: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function Token({
|
function Token({
|
||||||
@ -52,6 +54,7 @@ function Token({
|
|||||||
draggable,
|
draggable,
|
||||||
fadeOnHover,
|
fadeOnHover,
|
||||||
map,
|
map,
|
||||||
|
selected,
|
||||||
}: MapTokenProps) {
|
}: MapTokenProps) {
|
||||||
const userId = useUserId();
|
const userId = useUserId();
|
||||||
|
|
||||||
@ -193,6 +196,22 @@ function Token({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tokenRef = useRef<Konva.Group>(null);
|
||||||
|
function handleTransformEnd(event: Konva.KonvaEventObject<Event>) {
|
||||||
|
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(
|
const minCellSize = Math.min(
|
||||||
gridCellPixelSize.width,
|
gridCellPixelSize.width,
|
||||||
gridCellPixelSize.height
|
gridCellPixelSize.height
|
||||||
@ -228,68 +247,73 @@ function Token({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<animated.Group
|
<>
|
||||||
{...props}
|
<animated.Group
|
||||||
width={tokenWidth}
|
{...props}
|
||||||
height={tokenHeight}
|
|
||||||
draggable={draggable}
|
|
||||||
onMouseDown={handlePointerDown}
|
|
||||||
onMouseUp={handlePointerUp}
|
|
||||||
onMouseEnter={handlePointerEnter}
|
|
||||||
onMouseLeave={handlePointerLeave}
|
|
||||||
onTouchStart={handlePointerDown}
|
|
||||||
onTouchEnd={handlePointerUp}
|
|
||||||
onClick={handleClick}
|
|
||||||
onTap={handleClick}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragMove={handleDragMove}
|
|
||||||
opacity={tokenState.visible ? tokenOpacity : 0.5}
|
|
||||||
name={tokenName}
|
|
||||||
id={tokenState.id}
|
|
||||||
>
|
|
||||||
<Group
|
|
||||||
width={tokenWidth}
|
width={tokenWidth}
|
||||||
height={tokenHeight}
|
height={tokenHeight}
|
||||||
x={0}
|
draggable={draggable}
|
||||||
y={0}
|
onMouseDown={handlePointerDown}
|
||||||
rotation={tokenState.rotation}
|
onMouseUp={handlePointerUp}
|
||||||
offsetX={tokenWidth / 2}
|
onMouseEnter={handlePointerEnter}
|
||||||
offsetY={tokenHeight / 2}
|
onMouseLeave={handlePointerLeave}
|
||||||
|
onTouchStart={handlePointerDown}
|
||||||
|
onTouchEnd={handlePointerUp}
|
||||||
|
onClick={handleClick}
|
||||||
|
onTap={handleClick}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragMove={handleDragMove}
|
||||||
|
opacity={tokenState.visible ? tokenOpacity : 0.5}
|
||||||
|
name={tokenName}
|
||||||
|
id={tokenState.id}
|
||||||
>
|
>
|
||||||
<TokenOutline
|
<Group
|
||||||
outline={getScaledOutline(tokenState, tokenWidth, tokenHeight)}
|
ref={tokenRef}
|
||||||
hidden={!!tokenImage}
|
rotation={tokenState.rotation}
|
||||||
/>
|
offsetX={tokenWidth / 2}
|
||||||
</Group>
|
offsetY={tokenHeight / 2}
|
||||||
<KonvaImage
|
>
|
||||||
width={tokenWidth}
|
<Group width={tokenWidth} height={tokenHeight} x={0} y={0}>
|
||||||
height={tokenHeight}
|
<TokenOutline
|
||||||
x={0}
|
outline={getScaledOutline(tokenState, tokenWidth, tokenHeight)}
|
||||||
y={0}
|
hidden={!!tokenImage}
|
||||||
image={tokenImage}
|
/>
|
||||||
rotation={tokenState.rotation}
|
</Group>
|
||||||
offsetX={tokenWidth / 2}
|
<KonvaImage
|
||||||
offsetY={tokenHeight / 2}
|
width={tokenWidth}
|
||||||
hitFunc={() => {}}
|
height={tokenHeight}
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
image={tokenImage}
|
||||||
|
hitFunc={() => {}}
|
||||||
|
/>
|
||||||
|
<Group>
|
||||||
|
{tokenState.statuses?.length > 0 ? (
|
||||||
|
<TokenStatus
|
||||||
|
tokenState={tokenState}
|
||||||
|
width={tokenWidth}
|
||||||
|
height={tokenHeight}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
<Group offsetX={tokenWidth / 2} offsetY={tokenHeight / 2}>
|
||||||
|
{tokenState.label ? (
|
||||||
|
<TokenLabel
|
||||||
|
tokenState={tokenState}
|
||||||
|
width={tokenWidth}
|
||||||
|
height={tokenHeight}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Group>
|
||||||
|
</animated.Group>
|
||||||
|
<Transformer
|
||||||
|
active={selected}
|
||||||
|
nodeRef={tokenRef}
|
||||||
|
onTransformEnd={handleTransformEnd}
|
||||||
/>
|
/>
|
||||||
<Group offsetX={tokenWidth / 2} offsetY={tokenHeight / 2}>
|
</>
|
||||||
{tokenState.statuses?.length > 0 ? (
|
|
||||||
<TokenStatus
|
|
||||||
tokenState={tokenState}
|
|
||||||
width={tokenWidth}
|
|
||||||
height={tokenHeight}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{tokenState.label ? (
|
|
||||||
<TokenLabel
|
|
||||||
tokenState={tokenState}
|
|
||||||
width={tokenWidth}
|
|
||||||
height={tokenHeight}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</Group>
|
|
||||||
</animated.Group>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
113
src/components/konva/Transformer.tsx
Normal file
113
src/components/konva/Transformer.tsx
Normal file
@ -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<Konva.Node>;
|
||||||
|
onTransformStart?: (event: Konva.KonvaEventObject<Event>) => void;
|
||||||
|
onTransformEnd?: (event: Konva.KonvaEventObject<Event>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Transformer({
|
||||||
|
active,
|
||||||
|
nodeRef,
|
||||||
|
onTransformStart,
|
||||||
|
onTransformEnd,
|
||||||
|
}: ResizerProps) {
|
||||||
|
const setPreventMapInteraction = useSetPreventMapInteraction();
|
||||||
|
|
||||||
|
const transformerRef = useRef<Konva.Transformer>(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<string>();
|
||||||
|
function handleTransformStart(e: Konva.KonvaEventObject<Event>) {
|
||||||
|
if (transformerRef.current) {
|
||||||
|
movingAnchorRef.current = transformerRef.current._movingAnchorName;
|
||||||
|
if (active) {
|
||||||
|
setPreventMapInteraction(true);
|
||||||
|
}
|
||||||
|
onTransformStart && onTransformStart(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTransformEnd(e: Konva.KonvaEventObject<Event>) {
|
||||||
|
if (active) {
|
||||||
|
setPreventMapInteraction(false);
|
||||||
|
}
|
||||||
|
onTransformEnd && onTransformEnd(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KonvaTransformer
|
||||||
|
ref={transformerRef}
|
||||||
|
boundBoxFunc={(oldBox, newBox) => {
|
||||||
|
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;
|
@ -2,8 +2,6 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { Box, Input, Flex, Text, IconButton } from "theme-ui";
|
import { Box, Input, Flex, Text, IconButton } from "theme-ui";
|
||||||
import Konva from "konva";
|
import Konva from "konva";
|
||||||
|
|
||||||
import Slider from "../Slider";
|
|
||||||
|
|
||||||
import MapMenu from "../map/MapMenu";
|
import MapMenu from "../map/MapMenu";
|
||||||
|
|
||||||
import colors, { Color, colorOptions } from "../../helpers/colors";
|
import colors, { Color, colorOptions } from "../../helpers/colors";
|
||||||
@ -33,7 +31,6 @@ type TokenMenuProps = {
|
|||||||
map: Map | null;
|
map: Map | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultTokenMaxSize = 6;
|
|
||||||
function TokenMenu({
|
function TokenMenu({
|
||||||
isOpen,
|
isOpen,
|
||||||
onRequestClose,
|
onRequestClose,
|
||||||
@ -46,12 +43,10 @@ function TokenMenu({
|
|||||||
|
|
||||||
const wasOpen = usePrevious(isOpen);
|
const wasOpen = usePrevious(isOpen);
|
||||||
|
|
||||||
const [tokenMaxSize, setTokenMaxSize] = useState(defaultTokenMaxSize);
|
|
||||||
const [menuLeft, setMenuLeft] = useState(0);
|
const [menuLeft, setMenuLeft] = useState(0);
|
||||||
const [menuTop, setMenuTop] = useState(0);
|
const [menuTop, setMenuTop] = useState(0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && !wasOpen && tokenState) {
|
if (isOpen && !wasOpen && tokenState) {
|
||||||
setTokenMaxSize(Math.max(tokenState.size, defaultTokenMaxSize));
|
|
||||||
// Update menu position
|
// Update menu position
|
||||||
if (tokenImage) {
|
if (tokenImage) {
|
||||||
const imageRect = tokenImage.getClientRect();
|
const imageRect = tokenImage.getClientRect();
|
||||||
@ -89,19 +84,6 @@ function TokenMenu({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSizeChange(event: React.ChangeEvent<HTMLInputElement>) {
|
|
||||||
const newSize = parseFloat(event.target.value);
|
|
||||||
tokenState && onTokenStateChange({ [tokenState.id]: { size: newSize } });
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRotationChange(event: React.ChangeEvent<HTMLInputElement>) {
|
|
||||||
const newRotation = parseInt(event.target.value);
|
|
||||||
tokenState &&
|
|
||||||
onTokenStateChange({
|
|
||||||
[tokenState.id]: { rotation: newRotation },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleVisibleChange() {
|
function handleVisibleChange() {
|
||||||
tokenState &&
|
tokenState &&
|
||||||
onTokenStateChange({
|
onTokenStateChange({
|
||||||
@ -223,42 +205,6 @@ function TokenMenu({
|
|||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
<Flex sx={{ alignItems: "center" }}>
|
|
||||||
<Text
|
|
||||||
as="label"
|
|
||||||
variant="body2"
|
|
||||||
sx={{ width: "40%", fontSize: "16px" }}
|
|
||||||
p={1}
|
|
||||||
>
|
|
||||||
Size:
|
|
||||||
</Text>
|
|
||||||
<Slider
|
|
||||||
value={(tokenState && tokenState.size) || 1}
|
|
||||||
onChange={handleSizeChange}
|
|
||||||
step={0.5}
|
|
||||||
min={0.5}
|
|
||||||
max={tokenMaxSize}
|
|
||||||
mr={1}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
<Flex sx={{ alignItems: "center" }}>
|
|
||||||
<Text
|
|
||||||
as="label"
|
|
||||||
variant="body2"
|
|
||||||
sx={{ width: "65%", fontSize: "16px" }}
|
|
||||||
p={1}
|
|
||||||
>
|
|
||||||
Rotate:
|
|
||||||
</Text>
|
|
||||||
<Slider
|
|
||||||
value={(tokenState && tokenState.rotation) || 0}
|
|
||||||
onChange={handleRotationChange}
|
|
||||||
step={15}
|
|
||||||
min={0}
|
|
||||||
max={360}
|
|
||||||
mr={1}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
{/* Only show hide and lock token actions to map owners */}
|
{/* Only show hide and lock token actions to map owners */}
|
||||||
{map && map.owner === userId && (
|
{map && map.owner === userId && (
|
||||||
<Flex sx={{ alignItems: "center", justifyContent: "space-around" }}>
|
<Flex sx={{ alignItems: "center", justifyContent: "space-around" }}>
|
||||||
|
@ -96,6 +96,10 @@ function useMapTokens(
|
|||||||
}
|
}
|
||||||
fadeOnHover={selectedToolId === "drawing"}
|
fadeOnHover={selectedToolId === "drawing"}
|
||||||
map={map}
|
map={map}
|
||||||
|
selected={
|
||||||
|
!!tokenMenuOptions &&
|
||||||
|
tokenMenuOptions.tokenStateId === tokenState.id
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Group>
|
</Group>
|
||||||
|
Loading…
Reference in New Issue
Block a user