Files
grungnet/src/components/map/MapToken.js

226 lines
6.1 KiB
JavaScript
Raw Normal View History

2020-05-21 16:46:50 +10:00
import React, { useContext, useState, useEffect, useRef } from "react";
import { Image as KonvaImage, Group } from "react-konva";
import { useSpring, animated } from "react-spring/konva";
2020-05-21 16:46:50 +10:00
import useImage from "use-image";
import useDataSource from "../../helpers/useDataSource";
2020-05-21 16:46:50 +10:00
import useDebounce from "../../helpers/useDebounce";
import usePrevious from "../../helpers/usePrevious";
import AuthContext from "../../contexts/AuthContext";
2020-05-21 16:46:50 +10:00
import MapInteractionContext from "../../contexts/MapInteractionContext";
import TokenStatus from "../token/TokenStatus";
import TokenLabel from "../token/TokenLabel";
import { tokenSources, unknownSource } from "../../tokens";
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,
draggable,
mapState,
2020-05-21 16:46:50 +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]);
function handleDragStart(event) {
const tokenGroup = event.target;
const tokenImage = imageRef.current;
if (token && token.isVehicle) {
// Find all other tokens on the map
const layer = tokenGroup.getLayer();
const tokens = layer.find(".token");
for (let other of tokens) {
if (other === tokenGroup) {
continue;
}
const otherRect = other.getClientRect();
const otherCenter = {
x: otherRect.x + otherRect.width / 2,
y: otherRect.y + otherRect.height / 2,
};
if (tokenImage.intersects(otherCenter)) {
// Save and restore token position after moving layer
const position = other.absolutePosition();
other.moveTo(tokenGroup);
other.absolutePosition(position);
}
}
}
onTokenDragStart(event);
}
2020-05-21 16:46:50 +10:00
function handleDragEnd(event) {
const tokenGroup = event.target;
const mountChanges = {};
if (token && token.isVehicle) {
const layer = tokenGroup.getLayer();
const mountedTokens = tokenGroup.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);
mountChanges[mountedToken.id()] = {
...mapState.tokens[mountedToken.id()],
x: mountedToken.x() / mapWidth,
y: mountedToken.y() / mapHeight,
lastEditedBy: userId,
};
}
}
setPreventMapInteraction(false);
2020-05-21 16:46:50 +10:00
onTokenStateChange({
...mountChanges,
[tokenState.id]: {
...tokenState,
x: tokenGroup.x() / mapWidth,
y: tokenGroup.y() / mapHeight,
lastEditedBy: userId,
},
2020-05-21 16:46:50 +10:00
});
onTokenDragEnd(event);
2020-05-21 16:46:50 +10:00
}
function handleClick(event) {
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 (tokenOpacity !== 1.0) {
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-05-21 16:46:50 +10:00
const debouncedStageScale = useDebounce(stageScale, 50);
const imageRef = useRef();
2020-05-21 16:46:50 +10:00
useEffect(() => {
const image = imageRef.current;
if (
image &&
tokenSourceStatus === "loaded" &&
tokenWidth > 0 &&
tokenHeight > 0
) {
2020-05-21 16:46:50 +10:00
image.cache({
pixelRatio: debouncedStageScale * window.devicePixelRatio,
2020-05-21 16:46:50 +10:00
});
image.drawHitFromCache();
// Force redraw
image.getLayer().draw();
2020-05-21 16:46:50 +10:00
}
}, [debouncedStageScale, tokenWidth, tokenHeight, tokenSourceStatus]);
// 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;
const skipAnimation = tokenState.lastEditedBy === userId || resized;
const props = useSpring({
x: tokenX,
y: tokenY,
immediate: skipAnimation,
});
2020-05-21 22:57:44 +10:00
return (
<animated.Group
{...props}
2020-05-21 16:46:50 +10:00
width={tokenWidth}
height={tokenHeight}
draggable={draggable}
onMouseDown={handlePointerDown}
onMouseUp={handlePointerUp}
onMouseOver={handlePointerOver}
onMouseOut={handlePointerOut}
onTouchStart={handlePointerDown}
onTouchEnd={handlePointerUp}
onClick={handleClick}
2020-05-22 23:55:50 +10:00
onTap={handleClick}
onDragEnd={handleDragEnd}
onDragStart={handleDragStart}
opacity={tokenOpacity}
2020-05-22 21:10:05 +10:00
name={token && token.isVehicle ? "vehicle" : "token"}
id={tokenState.id}
>
<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-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>
</animated.Group>
);
}
export default MapToken;