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

336 lines
9.3 KiB
JavaScript
Raw Normal View History

import React, { useState, useRef } from "react";
import { Image as KonvaImage, Group, Line, Rect, Circle } from "react-konva";
import { useSpring, animated } from "react-spring/konva";
2020-05-21 16:46:50 +10:00
import useImage from "use-image";
2020-11-21 11:12:05 +11:00
import Konva from "konva";
import usePrevious from "../../hooks/usePrevious";
import useGridSnapping from "../../hooks/useGridSnapping";
import { useAuth } from "../../contexts/AuthContext";
2021-03-12 11:02:58 +11:00
import {
useSetPreventMapInteraction,
useMapWidth,
useMapHeight,
} from "../../contexts/MapInteractionContext";
import { useGridCellPixelSize } from "../../contexts/GridContext";
import { useDataURL } from "../../contexts/AssetsContext";
import TokenStatus from "../token/TokenStatus";
import TokenLabel from "../token/TokenLabel";
2021-04-24 18:21:49 +10:00
import colors from "../../helpers/colors";
import { tokenSources } from "../../tokens";
2020-05-21 16:46:50 +10:00
function MapToken({
tokenState,
onTokenStateChange,
onTokenMenuOpen,
2020-05-21 22:57:44 +10:00
onTokenDragStart,
onTokenDragEnd,
draggable,
fadeOnHover,
2020-08-07 12:55:16 +10:00
map,
2020-05-21 16:46:50 +10:00
}) {
const { userId } = useAuth();
2021-03-12 11:02:58 +11:00
const mapWidth = useMapWidth();
const mapHeight = useMapHeight();
const setPreventMapInteraction = useSetPreventMapInteraction();
const gridCellPixelSize = useGridCellPixelSize();
2020-05-21 16:46:50 +10:00
2021-04-24 18:21:49 +10:00
const tokenURL = useDataURL(tokenState, tokenSources);
const [tokenImage] = useImage(tokenURL);
2020-05-21 16:46:50 +10:00
2021-04-24 18:21:49 +10:00
const tokenAspectRatio = tokenState.width / tokenState.height;
2020-05-21 16:46:50 +10:00
const snapPositionToGrid = useGridSnapping();
function handleDragStart(event) {
const tokenGroup = event.target;
const tokenImage = imageRef.current;
if (tokenState.category === "vehicle") {
// Enable hit detection for .intersects() function
Konva.hitOnDragEnabled = true;
// Find all other tokens on the map
const layer = tokenGroup.getLayer();
2020-08-27 19:09:16 +10:00
const tokens = layer.find(".character");
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-08-07 12:55:16 +10:00
function handleDragMove(event) {
const tokenGroup = event.target;
// Snap to corners of grid
if (map.snapToGrid) {
tokenGroup.position(snapPositionToGrid(tokenGroup.position()));
2020-08-07 12:55:16 +10:00
}
}
2020-05-21 16:46:50 +10:00
function handleDragEnd(event) {
const tokenGroup = event.target;
const mountChanges = {};
if (tokenState.category === "vehicle") {
Konva.hitOnDragEnabled = false;
const parent = tokenGroup.getParent();
2020-08-27 19:09:16 +10:00
const mountedTokens = tokenGroup.find(".character");
for (let mountedToken of mountedTokens) {
// Save and restore token position after moving layer
const position = mountedToken.absolutePosition();
mountedToken.moveTo(parent);
mountedToken.absolutePosition(position);
mountChanges[mountedToken.id()] = {
x: mountedToken.x() / mapWidth,
y: mountedToken.y() / mapHeight,
lastModifiedBy: userId,
lastModified: Date.now(),
};
}
}
setPreventMapInteraction(false);
2020-05-21 16:46:50 +10:00
onTokenStateChange({
...mountChanges,
[tokenState.id]: {
x: tokenGroup.x() / mapWidth,
y: tokenGroup.y() / mapHeight,
lastModifiedBy: userId,
lastModified: Date.now(),
},
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);
// Store token pointer down time to check for a click when token is locked
const tokenPointerDownTimeRef = useRef();
function handlePointerDown(event) {
if (draggable) {
setPreventMapInteraction(true);
}
if (tokenState.locked && map.owner === userId) {
tokenPointerDownTimeRef.current = event.evt.timeStamp;
}
}
function handlePointerUp(event) {
if (draggable) {
setPreventMapInteraction(false);
}
// Check token click when locked and we are the map owner
// We can't use onClick because that doesn't check pointer distance
if (tokenState.locked && map.owner === userId) {
// If down and up time is small trigger a click
const delta = event.evt.timeStamp - tokenPointerDownTimeRef.current;
if (delta < 300) {
const tokenImage = event.target;
onTokenMenuOpen(tokenState.id, tokenImage);
}
}
}
function handlePointerEnter() {
if (fadeOnHover) {
setTokenOpacity(0.5);
}
}
function handlePointerLeave() {
if (tokenOpacity !== 1.0) {
setTokenOpacity(1.0);
}
2020-05-21 16:46:50 +10:00
}
2021-02-07 11:16:36 +11:00
const minCellSize = Math.min(
gridCellPixelSize.width,
gridCellPixelSize.height
);
const tokenWidth = minCellSize * tokenState.size;
const tokenHeight = (minCellSize / tokenAspectRatio) * tokenState.size;
const imageRef = useRef();
// 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.lastModifiedBy === userId || resized;
const props = useSpring({
x: tokenX,
y: tokenY,
immediate: skipAnimation,
});
2020-05-21 22:57:44 +10:00
// When a token is hidden if you aren't the map owner hide it completely
if (map && !tokenState.visible && map.owner !== userId) {
return null;
}
2020-08-27 19:09:16 +10:00
// Token name is used by on click to find whether a token is a vehicle or prop
2020-08-27 17:58:41 +10:00
let tokenName = "";
if (tokenState) {
tokenName = tokenState.category;
2020-08-27 17:58:41 +10:00
}
if (tokenState && tokenState.locked) {
tokenName = tokenName + "-locked";
}
function getScaledOutline() {
let outline = tokenState.outline;
if (outline.type === "rect") {
return {
...outline,
x: (outline.x / tokenState.width) * tokenWidth,
y: (outline.y / tokenState.height) * tokenHeight,
width: (outline.width / tokenState.width) * tokenWidth,
height: (outline.height / tokenState.height) * tokenHeight,
};
} else if (outline.type === "circle") {
return {
...outline,
x: (outline.x / tokenState.width) * tokenWidth,
y: (outline.y / tokenState.height) * tokenHeight,
radius: (outline.radius / tokenState.width) * tokenWidth,
};
} else {
let points = [...outline.points]; // Copy array so we can edit it imutably
for (let i = 0; i < points.length; i += 2) {
// Scale outline to the token
points[i] = (points[i] / tokenState.width) * tokenWidth;
points[i + 1] = (points[i + 1] / tokenState.height) * tokenHeight;
}
return { ...outline, points };
}
}
function renderOutline() {
const outline = getScaledOutline();
const sharedProps = {
fill: colors.black,
opacity: tokenImage ? 0 : 0.8,
};
if (outline.type === "rect") {
return (
<Rect
width={outline.width}
height={outline.height}
x={outline.x}
y={outline.y}
{...sharedProps}
/>
);
} else if (outline.type === "circle") {
return (
<Circle
radius={outline.radius}
x={outline.x}
y={outline.y}
{...sharedProps}
/>
);
} else {
return (
<Line
points={outline.points}
closed
tension={outline.points < 200 ? 0 : 0.33}
{...sharedProps}
/>
);
}
}
return (
<animated.Group
{...props}
2020-05-21 16:46:50 +10:00
width={tokenWidth}
height={tokenHeight}
draggable={draggable}
onMouseDown={handlePointerDown}
onMouseUp={handlePointerUp}
onMouseEnter={handlePointerEnter}
onMouseLeave={handlePointerLeave}
onTouchStart={handlePointerDown}
onTouchEnd={handlePointerUp}
onClick={handleClick}
2020-05-22 23:55:50 +10:00
onTap={handleClick}
onDragEnd={handleDragEnd}
onDragStart={handleDragStart}
2020-08-07 12:55:16 +10:00
onDragMove={handleDragMove}
opacity={tokenState.visible ? tokenOpacity : 0.5}
2020-08-27 17:58:41 +10:00
name={tokenName}
id={tokenState.id}
>
<Group
width={tokenWidth}
height={tokenHeight}
x={0}
y={0}
rotation={tokenState.rotation}
offsetX={tokenWidth / 2}
offsetY={tokenHeight / 2}
>
{renderOutline()}
</Group>
<KonvaImage
ref={imageRef}
width={tokenWidth}
height={tokenHeight}
x={0}
y={0}
image={tokenImage}
rotation={tokenState.rotation}
offsetX={tokenWidth / 2}
offsetY={tokenHeight / 2}
hitFunc={() => {}}
/>
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;