2020-05-21 16:46:50 +10:00
|
|
|
import React, { useContext, useState, useEffect, useRef } from "react";
|
2020-05-21 20:57:52 +10:00
|
|
|
import { Image as KonvaImage, Group } from "react-konva";
|
2020-05-25 10:37:28 +10:00
|
|
|
import { useSpring, animated } from "react-spring/konva";
|
2020-05-21 16:46:50 +10:00
|
|
|
import useImage from "use-image";
|
2020-04-13 00:24:03 +10:00
|
|
|
|
2020-04-24 15:50:05 +10:00
|
|
|
import useDataSource from "../../helpers/useDataSource";
|
2020-05-21 16:46:50 +10:00
|
|
|
import useDebounce from "../../helpers/useDebounce";
|
2020-05-22 22:17:30 +10:00
|
|
|
import usePrevious from "../../helpers/usePrevious";
|
2020-04-24 15:50:05 +10:00
|
|
|
|
2020-05-18 19:21:29 +10:00
|
|
|
import AuthContext from "../../contexts/AuthContext";
|
2020-05-21 16:46:50 +10:00
|
|
|
import MapInteractionContext from "../../contexts/MapInteractionContext";
|
2020-05-18 19:21:29 +10:00
|
|
|
|
2020-05-21 20:57:52 +10:00
|
|
|
import TokenStatus from "../token/TokenStatus";
|
|
|
|
|
import TokenLabel from "../token/TokenLabel";
|
|
|
|
|
|
2020-05-20 11:35:14 +10:00
|
|
|
import { tokenSources, unknownSource } from "../../tokens";
|
2020-04-13 00:24:03 +10:00
|
|
|
|
2020-05-21 16:46:50 +10:00
|
|
|
function MapToken({
|
|
|
|
|
token,
|
|
|
|
|
tokenState,
|
|
|
|
|
tokenSizePercent,
|
|
|
|
|
onTokenStateChange,
|
|
|
|
|
onTokenMenuOpen,
|
2020-05-21 22:57:44 +10:00
|
|
|
onTokenDragStart,
|
|
|
|
|
onTokenDragEnd,
|
2020-05-22 15:11:18 +10:00
|
|
|
draggable,
|
2020-05-22 20:43:07 +10:00
|
|
|
mapState,
|
2020-05-21 16:46:50 +10:00
|
|
|
}) {
|
2020-05-18 19:21:29 +10:00
|
|
|
const { userId } = useContext(AuthContext);
|
2020-05-21 16:46:50 +10:00
|
|
|
const {
|
|
|
|
|
setPreventMapInteraction,
|
|
|
|
|
mapWidth,
|
|
|
|
|
mapHeight,
|
|
|
|
|
stageScale,
|
|
|
|
|
} = useContext(MapInteractionContext);
|
|
|
|
|
|
|
|
|
|
const tokenSource = useDataSource(token, tokenSources, unknownSource);
|
|
|
|
|
const [tokenSourceImage, tokenSourceStatus] = useImage(tokenSource);
|
|
|
|
|
const [tokenAspectRatio, setTokenAspectRatio] = useState(1);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (tokenSourceImage) {
|
|
|
|
|
setTokenAspectRatio(tokenSourceImage.width / tokenSourceImage.height);
|
|
|
|
|
}
|
|
|
|
|
}, [tokenSourceImage]);
|
|
|
|
|
|
2020-05-22 20:43:07 +10:00
|
|
|
function handleDragStart(event) {
|
|
|
|
|
const tokenImage = event.target;
|
|
|
|
|
const tokenImageRect = tokenImage.getClientRect();
|
|
|
|
|
|
2020-05-31 10:23:45 +10:00
|
|
|
if (token && token.isVehicle) {
|
2020-05-22 20:43:07 +10:00
|
|
|
// Find all other tokens on the map
|
|
|
|
|
const layer = tokenImage.getLayer();
|
|
|
|
|
const tokens = layer.find(".token");
|
|
|
|
|
for (let other of tokens) {
|
|
|
|
|
if (other === tokenImage) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const otherRect = other.getClientRect();
|
|
|
|
|
const otherCenter = {
|
|
|
|
|
x: otherRect.x + otherRect.width / 2,
|
|
|
|
|
y: otherRect.y + otherRect.height / 2,
|
|
|
|
|
};
|
|
|
|
|
// Check the other tokens center overlaps this tokens bounding box
|
|
|
|
|
if (
|
|
|
|
|
otherCenter.x > tokenImageRect.x &&
|
|
|
|
|
otherCenter.x < tokenImageRect.x + tokenImageRect.width &&
|
|
|
|
|
otherCenter.y > tokenImageRect.y &&
|
|
|
|
|
otherCenter.y < tokenImageRect.y + tokenImageRect.height
|
|
|
|
|
) {
|
|
|
|
|
// Save and restore token position after moving layer
|
|
|
|
|
const position = other.absolutePosition();
|
|
|
|
|
other.moveTo(tokenImage);
|
|
|
|
|
other.absolutePosition(position);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onTokenDragStart(event);
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-21 16:46:50 +10:00
|
|
|
function handleDragEnd(event) {
|
2020-05-22 20:43:07 +10:00
|
|
|
const tokenImage = event.target;
|
|
|
|
|
|
2020-05-22 22:17:30 +10:00
|
|
|
const mountChanges = {};
|
2020-05-31 10:23:45 +10:00
|
|
|
if (token && token.isVehicle) {
|
2020-05-22 20:43:07 +10:00
|
|
|
const layer = tokenImage.getLayer();
|
|
|
|
|
const mountedTokens = tokenImage.find(".token");
|
|
|
|
|
for (let mountedToken of mountedTokens) {
|
|
|
|
|
// Save and restore token position after moving layer
|
|
|
|
|
const position = mountedToken.absolutePosition();
|
|
|
|
|
mountedToken.moveTo(layer);
|
|
|
|
|
mountedToken.absolutePosition(position);
|
2020-05-22 22:17:30 +10:00
|
|
|
mountChanges[mountedToken.id()] = {
|
2020-05-22 20:43:07 +10:00
|
|
|
...mapState.tokens[mountedToken.id()],
|
|
|
|
|
x: mountedToken.x() / mapWidth,
|
|
|
|
|
y: mountedToken.y() / mapHeight,
|
|
|
|
|
lastEditedBy: userId,
|
2020-05-22 22:17:30 +10:00
|
|
|
};
|
2020-05-22 20:43:07 +10:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-21 21:31:15 +10:00
|
|
|
setPreventMapInteraction(false);
|
2020-05-21 16:46:50 +10:00
|
|
|
onTokenStateChange({
|
2020-05-22 22:17:30 +10:00
|
|
|
...mountChanges,
|
|
|
|
|
[tokenState.id]: {
|
|
|
|
|
...tokenState,
|
|
|
|
|
x: tokenImage.x() / mapWidth,
|
|
|
|
|
y: tokenImage.y() / mapHeight,
|
|
|
|
|
lastEditedBy: userId,
|
|
|
|
|
},
|
2020-05-21 16:46:50 +10:00
|
|
|
});
|
2020-05-22 20:43:07 +10:00
|
|
|
onTokenDragEnd(event);
|
2020-05-21 16:46:50 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleClick(event) {
|
2020-05-22 15:11:18 +10:00
|
|
|
if (draggable) {
|
|
|
|
|
const tokenImage = event.target;
|
|
|
|
|
onTokenMenuOpen(tokenState.id, tokenImage);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [tokenOpacity, setTokenOpacity] = useState(1);
|
|
|
|
|
function handlePointerDown() {
|
|
|
|
|
if (draggable) {
|
|
|
|
|
setPreventMapInteraction(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handlePointerUp() {
|
|
|
|
|
if (draggable) {
|
|
|
|
|
setPreventMapInteraction(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handlePointerOver() {
|
|
|
|
|
if (!draggable) {
|
|
|
|
|
setTokenOpacity(0.5);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handlePointerOut() {
|
|
|
|
|
if (!draggable) {
|
|
|
|
|
setTokenOpacity(1.0);
|
|
|
|
|
}
|
2020-05-21 16:46:50 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tokenWidth = tokenSizePercent * mapWidth * tokenState.size;
|
|
|
|
|
const tokenHeight =
|
|
|
|
|
tokenSizePercent * (mapWidth / tokenAspectRatio) * tokenState.size;
|
2020-04-24 15:50:05 +10:00
|
|
|
|
2020-05-21 16:46:50 +10:00
|
|
|
const debouncedStageScale = useDebounce(stageScale, 50);
|
2020-04-13 00:24:03 +10:00
|
|
|
const imageRef = useRef();
|
2020-05-21 16:46:50 +10:00
|
|
|
useEffect(() => {
|
|
|
|
|
const image = imageRef.current;
|
2020-05-22 22:17:30 +10:00
|
|
|
if (
|
|
|
|
|
image &&
|
|
|
|
|
tokenSourceStatus === "loaded" &&
|
|
|
|
|
tokenWidth > 0 &&
|
|
|
|
|
tokenHeight > 0
|
|
|
|
|
) {
|
2020-05-21 16:46:50 +10:00
|
|
|
image.cache({
|
2020-05-22 13:44:05 +10:00
|
|
|
pixelRatio: debouncedStageScale * window.devicePixelRatio,
|
2020-05-21 16:46:50 +10:00
|
|
|
});
|
|
|
|
|
image.drawHitFromCache();
|
|
|
|
|
// Force redraw
|
2020-05-21 20:57:52 +10:00
|
|
|
image.getLayer().draw();
|
2020-05-21 16:46:50 +10:00
|
|
|
}
|
|
|
|
|
}, [debouncedStageScale, tokenWidth, tokenHeight, tokenSourceStatus]);
|
2020-04-13 00:24:03 +10:00
|
|
|
|
2020-05-22 22:17:30 +10:00
|
|
|
// Animate to new token positions if edited by others
|
|
|
|
|
const tokenX = tokenState.x * mapWidth;
|
|
|
|
|
const tokenY = tokenState.y * mapHeight;
|
|
|
|
|
const previousWidth = usePrevious(mapWidth);
|
|
|
|
|
const previousHeight = usePrevious(mapHeight);
|
|
|
|
|
const resized = mapWidth !== previousWidth || mapHeight !== previousHeight;
|
2020-05-25 10:37:28 +10:00
|
|
|
const skipAnimation = tokenState.lastEditedBy === userId || resized;
|
|
|
|
|
const props = useSpring({
|
|
|
|
|
x: tokenX,
|
|
|
|
|
y: tokenY,
|
|
|
|
|
immediate: skipAnimation,
|
|
|
|
|
});
|
2020-05-21 22:57:44 +10:00
|
|
|
|
2020-04-13 00:24:03 +10:00
|
|
|
return (
|
2020-05-25 10:37:28 +10:00
|
|
|
<animated.Group
|
|
|
|
|
{...props}
|
2020-05-21 16:46:50 +10:00
|
|
|
width={tokenWidth}
|
|
|
|
|
height={tokenHeight}
|
2020-05-22 15:11:18 +10:00
|
|
|
draggable={draggable}
|
|
|
|
|
onMouseDown={handlePointerDown}
|
|
|
|
|
onMouseUp={handlePointerUp}
|
|
|
|
|
onMouseOver={handlePointerOver}
|
|
|
|
|
onMouseOut={handlePointerOut}
|
|
|
|
|
onTouchStart={handlePointerDown}
|
|
|
|
|
onTouchEnd={handlePointerUp}
|
2020-05-21 20:57:52 +10:00
|
|
|
onClick={handleClick}
|
2020-05-22 23:55:50 +10:00
|
|
|
onTap={handleClick}
|
2020-05-21 20:57:52 +10:00
|
|
|
onDragEnd={handleDragEnd}
|
2020-05-22 20:43:07 +10:00
|
|
|
onDragStart={handleDragStart}
|
2020-05-22 15:11:18 +10:00
|
|
|
opacity={tokenOpacity}
|
2020-05-22 21:10:05 +10:00
|
|
|
name={token && token.isVehicle ? "vehicle" : "token"}
|
2020-05-22 20:43:07 +10:00
|
|
|
id={tokenState.id}
|
2020-05-21 20:57:52 +10:00
|
|
|
>
|
|
|
|
|
<KonvaImage
|
|
|
|
|
ref={imageRef}
|
|
|
|
|
width={tokenWidth}
|
|
|
|
|
height={tokenHeight}
|
|
|
|
|
x={0}
|
|
|
|
|
y={0}
|
|
|
|
|
image={tokenSourceImage}
|
2020-05-22 21:10:05 +10:00
|
|
|
rotation={tokenState.rotation}
|
|
|
|
|
offsetX={tokenWidth / 2}
|
|
|
|
|
offsetY={tokenHeight / 2}
|
2020-05-21 20:57:52 +10:00
|
|
|
/>
|
2020-05-22 21:10:05 +10:00
|
|
|
<Group offsetX={tokenWidth / 2} offsetY={tokenHeight / 2}>
|
|
|
|
|
<TokenStatus
|
|
|
|
|
tokenState={tokenState}
|
|
|
|
|
width={tokenWidth}
|
|
|
|
|
height={tokenHeight}
|
|
|
|
|
/>
|
|
|
|
|
<TokenLabel
|
|
|
|
|
tokenState={tokenState}
|
|
|
|
|
width={tokenWidth}
|
|
|
|
|
height={tokenHeight}
|
|
|
|
|
/>
|
|
|
|
|
</Group>
|
2020-05-25 10:37:28 +10:00
|
|
|
</animated.Group>
|
2020-04-13 00:24:03 +10:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default MapToken;
|