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 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 useImage from "use-image";
import useDataSource from "../../helpers/useDataSource"; import useDataSource from "../../helpers/useDataSource";
@ -8,6 +8,9 @@ import useDebounce from "../../helpers/useDebounce";
import AuthContext from "../../contexts/AuthContext"; import AuthContext from "../../contexts/AuthContext";
import MapInteractionContext from "../../contexts/MapInteractionContext"; import MapInteractionContext from "../../contexts/MapInteractionContext";
import TokenStatus from "../token/TokenStatus";
import TokenLabel from "../token/TokenLabel";
import { tokenSources, unknownSource } from "../../tokens"; import { tokenSources, unknownSource } from "../../tokens";
function MapToken({ function MapToken({
@ -57,32 +60,49 @@ function MapToken({
const imageRef = useRef(); const imageRef = useRef();
useEffect(() => { useEffect(() => {
const image = imageRef.current; const image = imageRef.current;
if (image) { if (image && tokenSourceStatus === "loaded") {
image.cache({ image.cache({
pixelRatio: debouncedStageScale, pixelRatio: debouncedStageScale,
}); });
image.drawHitFromCache(); image.drawHitFromCache();
// Force redraw // Force redraw
image.parent.draw(); image.getLayer().draw();
} }
}, [debouncedStageScale, tokenWidth, tokenHeight, tokenSourceStatus]); }, [debouncedStageScale, tokenWidth, tokenHeight, tokenSourceStatus]);
return ( return (
<KonvaImage <Group
ref={imageRef}
width={tokenWidth} width={tokenWidth}
height={tokenHeight} height={tokenHeight}
x={tokenState.x * mapWidth} x={tokenState.x * mapWidth}
y={tokenState.y * mapHeight} y={tokenState.y * mapHeight}
image={tokenSourceImage}
draggable draggable
onDragEnd={handleDragEnd}
onClick={handleClick}
onMouseDown={() => setPreventMapInteraction(true)} onMouseDown={() => setPreventMapInteraction(true)}
onMouseUp={() => setPreventMapInteraction(false)} onMouseUp={() => setPreventMapInteraction(false)}
onTouchStart={() => setPreventMapInteraction(true)} onTouchStart={() => setPreventMapInteraction(true)}
onTouchEnd={() => setPreventMapInteraction(false)} 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 usePortal from "../../helpers/usePortal";
import TokenLabel from "./TokenLabel";
import TokenStatus from "./TokenStatus";
import MapStageContext from "../../contexts/MapStageContext"; 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 {string} tokenClassName The class name to attach the interactjs handler to
* @param {onProxyDragEnd} onProxyDragEnd Called when the proxy token is dropped * @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} 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({ function ProxyToken({ tokenClassName, onProxyDragEnd, tokens }) {
tokenClassName,
onProxyDragEnd,
tokens,
disabledTokens,
}) {
const proxyContainer = usePortal("root"); const proxyContainer = usePortal("root");
const [imageSource, setImageSource] = useState(""); const [imageSource, setImageSource] = useState("");
@ -39,11 +30,9 @@ function ProxyToken({
// Store the tokens in a ref and access in the interactjs loop // Store the tokens in a ref and access in the interactjs loop
// This is needed to stop interactjs from creating multiple listeners // This is needed to stop interactjs from creating multiple listeners
const tokensRef = useRef(tokens); const tokensRef = useRef(tokens);
const disabledTokensRef = useRef(disabledTokens);
useEffect(() => { useEffect(() => {
tokensRef.current = tokens; tokensRef.current = tokens;
disabledTokensRef.current = disabledTokens; }, [tokens]);
}, [tokens, disabledTokens]);
const proxyOnMap = useRef(false); const proxyOnMap = useRef(false);
const mapStageRef = useContext(MapStageContext); const mapStageRef = useContext(MapStageContext);
@ -54,9 +43,6 @@ function ProxyToken({
start: (event) => { start: (event) => {
let target = event.target; let target = event.target;
const id = target.dataset.id; const id = target.dataset.id;
if (id in disabledTokensRef.current) {
return;
}
// Hide the token and copy it's image to the proxy // Hide the token and copy it's image to the proxy
target.parentElement.style.opacity = "0.25"; target.parentElement.style.opacity = "0.25";
@ -108,9 +94,6 @@ function ProxyToken({
end: (event) => { end: (event) => {
let target = event.target; let target = event.target;
const id = target.dataset.id; const id = target.dataset.id;
if (id in disabledTokensRef.current) {
return;
}
let proxy = proxyRef.current; let proxy = proxyRef.current;
if (proxy) { if (proxy) {
const mapStage = mapStageRef.current; const mapStage = mapStageRef.current;
@ -187,12 +170,6 @@ function ProxyToken({
width: "100%", width: "100%",
}} }}
/> />
{tokens[tokenId] && tokens[tokenId].statuses && (
<TokenStatus token={tokens[tokenId]} />
)}
{tokens[tokenId] && tokens[tokenId].label && (
<TokenLabel token={tokens[tokenId]} />
)}
</Box> </Box>
</Box>, </Box>,
proxyContainer proxyContainer
@ -201,7 +178,6 @@ function ProxyToken({
ProxyToken.defaultProps = { ProxyToken.defaultProps = {
tokens: {}, tokens: {},
disabledTokens: {},
}; };
export default ProxyToken; export default ProxyToken;

View File

@ -1,60 +1,49 @@
import React from "react"; import React, { useRef, useEffect, useState } from "react";
import { Box, Text } from "theme-ui"; 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 ( return (
<Box <Group y={height - (fontSize + paddingY) / 2}>
sx={{ <Rect
position: "absolute", y={-paddingY / 2}
transform: `scale(${0.3 / token.size}) translate(0, 20%)`, width={rectWidth}
transformOrigin: "bottom center", offsetX={width / 2}
pointerEvents: "none", x={width - rectWidth / 2}
width: "100%", height={fontSize + paddingY}
height: "100%", fill="hsla(230, 25%, 18%, 0.8)"
}} cornerRadius={(fontSize + paddingY) / 2}
> />
{/* Use SVG so text is scaled with token size */} <Text
<svg ref={textRef}
viewBox="0 0 100 100" width={width}
xmlns="http://www.w3.org/2000/svg" text={tokenState.label}
width="100%" fontSize={fontSize}
height="100%" lineHeight={1}
style={{ overflow: "visible" }} align="center"
> verticalAlign="bottom"
<foreignObject fill="white"
width="100%" paddingX={paddingX}
height="100%" paddingY={paddingY}
style={{ overflow: "visible" }} wrap="none"
> ellipsis={true}
<Box sx={{ width: "100%", height: "100%", position: "relative" }}> />
<Text </Group>
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>
); );
} }

View File

@ -1,46 +1,23 @@
import React from "react"; import React from "react";
import { Box } from "theme-ui"; import { Circle, Group } from "react-konva";
import colors from "../../helpers/colors"; import colors from "../../helpers/colors";
function TokenStatus({ token }) { function TokenStatus({ tokenState, width, height }) {
return ( return (
<Box <Group x={width} y={height} offsetX={width / 2} offsetY={height / 2}>
sx={{ {tokenState.statuses.map((status, index) => (
position: "absolute", <Circle
width: "100%", width={width}
height: "100%", height={height}
pointerEvents: "none", stroke={colors[status]}
}} strokeWidth={width / 20 / tokenState.size}
> scaleX={1 - index / 10 / tokenState.size}
{token.statuses.map((status, index) => ( scaleY={1 - index / 10 / tokenState.size}
<Box opacity={0.8}
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>
))} ))}
</Box> </Group>
); );
} }