Converted token label and status to konva

This commit is contained in:
Mitchell McCaffrey 2020-05-21 20:57:52 +10:00
parent 5b70f69fb7
commit b9968053b7
4 changed files with 89 additions and 127 deletions

View File

@ -1,5 +1,5 @@
import React, { useContext, useState, useEffect, useRef } from "react";
import { Image as KonvaImage } from "react-konva";
import { Image as KonvaImage, Group } from "react-konva";
import useImage from "use-image";
import useDataSource from "../../helpers/useDataSource";
@ -8,6 +8,9 @@ import useDebounce from "../../helpers/useDebounce";
import AuthContext from "../../contexts/AuthContext";
import MapInteractionContext from "../../contexts/MapInteractionContext";
import TokenStatus from "../token/TokenStatus";
import TokenLabel from "../token/TokenLabel";
import { tokenSources, unknownSource } from "../../tokens";
function MapToken({
@ -57,32 +60,49 @@ function MapToken({
const imageRef = useRef();
useEffect(() => {
const image = imageRef.current;
if (image) {
if (image && tokenSourceStatus === "loaded") {
image.cache({
pixelRatio: debouncedStageScale,
});
image.drawHitFromCache();
// Force redraw
image.parent.draw();
image.getLayer().draw();
}
}, [debouncedStageScale, tokenWidth, tokenHeight, tokenSourceStatus]);
return (
<KonvaImage
ref={imageRef}
<Group
width={tokenWidth}
height={tokenHeight}
x={tokenState.x * mapWidth}
y={tokenState.y * mapHeight}
image={tokenSourceImage}
draggable
onDragEnd={handleDragEnd}
onClick={handleClick}
onMouseDown={() => setPreventMapInteraction(true)}
onMouseUp={() => setPreventMapInteraction(false)}
onTouchStart={() => setPreventMapInteraction(true)}
onTouchEnd={() => setPreventMapInteraction(false)}
/>
onClick={handleClick}
onDragEnd={handleDragEnd}
>
<KonvaImage
ref={imageRef}
width={tokenWidth}
height={tokenHeight}
x={0}
y={0}
image={tokenSourceImage}
/>
<TokenStatus
tokenState={tokenState}
width={tokenWidth}
height={tokenHeight}
/>
<TokenLabel
tokenState={tokenState}
width={tokenWidth}
height={tokenHeight}
/>
</Group>
);
}

View File

@ -5,9 +5,6 @@ import interact from "interactjs";
import usePortal from "../../helpers/usePortal";
import TokenLabel from "./TokenLabel";
import TokenStatus from "./TokenStatus";
import MapStageContext from "../../contexts/MapStageContext";
/**
@ -21,15 +18,9 @@ import MapStageContext from "../../contexts/MapStageContext";
* @param {string} tokenClassName The class name to attach the interactjs handler to
* @param {onProxyDragEnd} onProxyDragEnd Called when the proxy token is dropped
* @param {Object} tokens An optional mapping of tokens to use as a base when calling OnProxyDragEnd
* @param {Object} disabledTokens An optional mapping of tokens that shouldn't allow movement
*/
function ProxyToken({
tokenClassName,
onProxyDragEnd,
tokens,
disabledTokens,
}) {
function ProxyToken({ tokenClassName, onProxyDragEnd, tokens }) {
const proxyContainer = usePortal("root");
const [imageSource, setImageSource] = useState("");
@ -39,11 +30,9 @@ function ProxyToken({
// Store the tokens in a ref and access in the interactjs loop
// This is needed to stop interactjs from creating multiple listeners
const tokensRef = useRef(tokens);
const disabledTokensRef = useRef(disabledTokens);
useEffect(() => {
tokensRef.current = tokens;
disabledTokensRef.current = disabledTokens;
}, [tokens, disabledTokens]);
}, [tokens]);
const proxyOnMap = useRef(false);
const mapStageRef = useContext(MapStageContext);
@ -54,9 +43,6 @@ function ProxyToken({
start: (event) => {
let target = event.target;
const id = target.dataset.id;
if (id in disabledTokensRef.current) {
return;
}
// Hide the token and copy it's image to the proxy
target.parentElement.style.opacity = "0.25";
@ -108,9 +94,6 @@ function ProxyToken({
end: (event) => {
let target = event.target;
const id = target.dataset.id;
if (id in disabledTokensRef.current) {
return;
}
let proxy = proxyRef.current;
if (proxy) {
const mapStage = mapStageRef.current;
@ -187,12 +170,6 @@ function ProxyToken({
width: "100%",
}}
/>
{tokens[tokenId] && tokens[tokenId].statuses && (
<TokenStatus token={tokens[tokenId]} />
)}
{tokens[tokenId] && tokens[tokenId].label && (
<TokenLabel token={tokens[tokenId]} />
)}
</Box>
</Box>,
proxyContainer
@ -201,7 +178,6 @@ function ProxyToken({
ProxyToken.defaultProps = {
tokens: {},
disabledTokens: {},
};
export default ProxyToken;

View File

@ -1,60 +1,49 @@
import React from "react";
import { Box, Text } from "theme-ui";
import React, { useRef, useEffect, useState } from "react";
import { Rect, Text, Group } from "react-konva";
function TokenLabel({ tokenState, width, height }) {
const fontSize = height / 6 / tokenState.size;
const paddingY = height / 16 / tokenState.size;
const paddingX = height / 8 / tokenState.size;
const [rectWidth, setRectWidth] = useState(0);
useEffect(() => {
const text = textRef.current;
if (text && tokenState.label) {
setRectWidth(text.getTextWidth() + paddingX);
} else {
setRectWidth(0);
}
}, [tokenState.label, paddingX]);
const textRef = useRef();
function TokenLabel({ token }) {
return (
<Box
sx={{
position: "absolute",
transform: `scale(${0.3 / token.size}) translate(0, 20%)`,
transformOrigin: "bottom center",
pointerEvents: "none",
width: "100%",
height: "100%",
}}
>
{/* Use SVG so text is scaled with token size */}
<svg
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
style={{ overflow: "visible" }}
>
<foreignObject
width="100%"
height="100%"
style={{ overflow: "visible" }}
>
<Box sx={{ width: "100%", height: "100%", position: "relative" }}>
<Text
as="p"
variant="heading"
sx={{
fontSize: "66px",
textAlign: "center",
verticalAlign: "middle",
lineHeight: 1.4,
whiteSpace: "nowrap",
minWidth: "100%",
display: "inline-block",
position: "absolute",
top: 0,
left: "50%",
transform: "translateX(-50%)",
borderRadius: "66px",
border: "2px solid",
borderColor: "muted",
}}
bg="overlay"
px={4}
>
{token.label}
</Text>
</Box>
</foreignObject>
</svg>
</Box>
<Group y={height - (fontSize + paddingY) / 2}>
<Rect
y={-paddingY / 2}
width={rectWidth}
offsetX={width / 2}
x={width - rectWidth / 2}
height={fontSize + paddingY}
fill="hsla(230, 25%, 18%, 0.8)"
cornerRadius={(fontSize + paddingY) / 2}
/>
<Text
ref={textRef}
width={width}
text={tokenState.label}
fontSize={fontSize}
lineHeight={1}
align="center"
verticalAlign="bottom"
fill="white"
paddingX={paddingX}
paddingY={paddingY}
wrap="none"
ellipsis={true}
/>
</Group>
);
}

View File

@ -1,46 +1,23 @@
import React from "react";
import { Box } from "theme-ui";
import { Circle, Group } from "react-konva";
import colors from "../../helpers/colors";
function TokenStatus({ token }) {
function TokenStatus({ tokenState, width, height }) {
return (
<Box
sx={{
position: "absolute",
width: "100%",
height: "100%",
pointerEvents: "none",
}}
>
{token.statuses.map((status, index) => (
<Box
key={status}
sx={{
width: "100%",
height: "100%",
position: "absolute",
opacity: 0.8,
transform: `scale(${1 - index / 10 / token.size})`,
}}
>
<svg
style={{ position: "absolute" }}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
>
<circle
r={47}
cx={50}
cy={50}
fill="none"
stroke={colors[status]}
strokeWidth={4 / token.size}
/>
</svg>
</Box>
<Group x={width} y={height} offsetX={width / 2} offsetY={height / 2}>
{tokenState.statuses.map((status, index) => (
<Circle
width={width}
height={height}
stroke={colors[status]}
strokeWidth={width / 20 / tokenState.size}
scaleX={1 - index / 10 / tokenState.size}
scaleY={1 - index / 10 / tokenState.size}
opacity={0.8}
/>
))}
</Box>
</Group>
);
}