Added token preview to edit token modal and refactored Grid from Map Grid

This commit is contained in:
Mitchell McCaffrey 2020-10-17 10:45:59 +11:00
parent 0235bdb7ef
commit e9e7794027
4 changed files with 273 additions and 60 deletions

72
src/components/Grid.js Normal file
View File

@ -0,0 +1,72 @@
import React from "react";
import { Line, Group } from "react-konva";
import { getStrokeWidth } from "../helpers/drawing";
function Grid({ gridX, gridY, gridInset, strokeWidth, width, height, stroke }) {
if (!gridX || !gridY) {
return null;
}
const gridSizeNormalized = {
x: (gridInset.bottomRight.x - gridInset.topLeft.x) / gridX,
y: (gridInset.bottomRight.y - gridInset.topLeft.y) / gridY,
};
const insetWidth = (gridInset.bottomRight.x - gridInset.topLeft.x) * width;
const insetHeight = (gridInset.bottomRight.y - gridInset.topLeft.y) * height;
const lineSpacingX = insetWidth / gridX;
const lineSpacingY = insetHeight / gridY;
const offsetX = gridInset.topLeft.x * width * -1;
const offsetY = gridInset.topLeft.y * height * -1;
const lines = [];
for (let x = 1; x < gridX; x++) {
lines.push(
<Line
key={`grid_x_${x}`}
points={[x * lineSpacingX, 0, x * lineSpacingX, insetHeight]}
stroke={stroke}
strokeWidth={getStrokeWidth(
strokeWidth,
gridSizeNormalized,
width,
height
)}
opacity={0.5}
offsetX={offsetX}
offsetY={offsetY}
/>
);
}
for (let y = 1; y < gridY; y++) {
lines.push(
<Line
key={`grid_y_${y}`}
points={[0, y * lineSpacingY, insetWidth, y * lineSpacingY]}
stroke={stroke}
strokeWidth={getStrokeWidth(
strokeWidth,
gridSizeNormalized,
width,
height
)}
opacity={0.5}
offsetX={offsetX}
offsetY={offsetY}
/>
);
}
return <Group>{lines}</Group>;
}
Grid.defaultProps = {
strokeWidth: 0.1,
gridInset: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 } },
stroke: "white",
};
export default Grid;

View File

@ -1,5 +1,4 @@
import React, { useContext, useEffect, useState } from "react";
import { Line, Group } from "react-konva";
import useImage from "use-image";
import MapInteractionContext from "../../contexts/MapInteractionContext";
@ -7,9 +6,10 @@ import MapInteractionContext from "../../contexts/MapInteractionContext";
import useDataSource from "../../helpers/useDataSource";
import { mapSources as defaultMapSources } from "../../maps";
import { getStrokeWidth } from "../../helpers/drawing";
import { getImageLightness } from "../../helpers/image";
import Grid from "../Grid";
function MapGrid({ map, strokeWidth }) {
const { mapWidth, mapHeight } = useContext(MapInteractionContext);
@ -36,66 +36,19 @@ function MapGrid({ map, strokeWidth }) {
const gridX = map && map.grid.size.x;
const gridY = map && map.grid.size.y;
if (!gridX || !gridY) {
return null;
}
const gridInset = map && map.grid.inset;
const gridSizeNormalized = {
x: (gridInset.bottomRight.x - gridInset.topLeft.x) / gridX,
y: (gridInset.bottomRight.y - gridInset.topLeft.y) / gridY,
};
const insetWidth = (gridInset.bottomRight.x - gridInset.topLeft.x) * mapWidth;
const insetHeight =
(gridInset.bottomRight.y - gridInset.topLeft.y) * mapHeight;
const lineSpacingX = insetWidth / gridX;
const lineSpacingY = insetHeight / gridY;
const offsetX = gridInset.topLeft.x * mapWidth * -1;
const offsetY = gridInset.topLeft.y * mapHeight * -1;
const lines = [];
for (let x = 1; x < gridX; x++) {
lines.push(
<Line
key={`grid_x_${x}`}
points={[x * lineSpacingX, 0, x * lineSpacingX, insetHeight]}
stroke={isImageLight ? "black" : "white"}
strokeWidth={getStrokeWidth(
strokeWidth,
gridSizeNormalized,
mapWidth,
mapHeight
)}
opacity={0.5}
offsetX={offsetX}
offsetY={offsetY}
/>
);
}
for (let y = 1; y < gridY; y++) {
lines.push(
<Line
key={`grid_y_${y}`}
points={[0, y * lineSpacingY, insetWidth, y * lineSpacingY]}
stroke={isImageLight ? "black" : "white"}
strokeWidth={getStrokeWidth(
strokeWidth,
gridSizeNormalized,
mapWidth,
mapHeight
)}
opacity={0.5}
offsetX={offsetX}
offsetY={offsetY}
/>
);
}
return <Group>{lines}</Group>;
return (
<Grid
gridX={gridX}
gridY={gridY}
gridInset={gridInset}
strokeWidth={strokeWidth}
width={mapWidth}
height={mapHeight}
stroke={isImageLight ? "black" : "white"}
/>
);
}
MapGrid.defaultProps = {

View File

@ -0,0 +1,186 @@
import React, { useState, useRef, useEffect } from "react";
import { Box, IconButton } from "theme-ui";
import { Stage, Layer, Image, Rect, Group } from "react-konva";
import ReactResizeDetector from "react-resize-detector";
import useImage from "use-image";
import usePreventOverscroll from "../../helpers/usePreventOverscroll";
import useStageInteraction from "../../helpers/useStageInteraction";
import useDataSource from "../../helpers/useDataSource";
import GridOnIcon from "../../icons/GridOnIcon";
import GridOffIcon from "../../icons/GridOffIcon";
import { tokenSources, unknownSource } from "../../tokens";
import Grid from "../Grid";
function TokenPreview({ token }) {
const [tokenSourceData, setTokenSourceData] = useState({});
useEffect(() => {
if (token.id !== tokenSourceData.id) {
setTokenSourceData(token);
}
}, [token, tokenSourceData]);
const tokenSource = useDataSource(
tokenSourceData,
tokenSources,
unknownSource
);
const [tokenSourceImage] = useImage(tokenSource);
const [tokenRatio, setTokenRatio] = useState(1);
useEffect(() => {
if (tokenSourceImage) {
setTokenRatio(tokenSourceImage.width / tokenSourceImage.height);
}
}, [tokenSourceImage]);
const [stageWidth, setStageWidth] = useState(1);
const [stageHeight, setStageHeight] = useState(1);
const [stageScale, setStageScale] = useState(1);
const stageRatio = stageWidth / stageHeight;
let tokenWidth;
let tokenHeight;
if (stageRatio > tokenRatio) {
tokenWidth = tokenSourceImage
? stageHeight / (tokenSourceImage.height / tokenSourceImage.width)
: stageWidth;
tokenHeight = stageHeight;
} else {
tokenWidth = stageWidth;
tokenHeight = tokenSourceImage
? stageWidth * (tokenSourceImage.height / tokenSourceImage.width)
: stageHeight;
}
const stageTranslateRef = useRef({ x: 0, y: 0 });
const mapLayerRef = useRef();
function handleResize(width, height) {
setStageWidth(width);
setStageHeight(height);
}
// Reset map translate and scale
useEffect(() => {
const layer = mapLayerRef.current;
const containerRect = containerRef.current.getBoundingClientRect();
if (layer) {
let newTranslate;
if (stageRatio > tokenRatio) {
newTranslate = {
x: -(tokenWidth - containerRect.width) / 2,
y: 0,
};
} else {
newTranslate = {
x: 0,
y: -(tokenHeight - containerRect.height) / 2,
};
}
layer.x(newTranslate.x);
layer.y(newTranslate.y);
layer.draw();
stageTranslateRef.current = newTranslate;
setStageScale(1);
}
}, [token.id, tokenWidth, tokenHeight, stageRatio, tokenRatio]);
const bind = useStageInteraction(
mapLayerRef.current,
stageScale,
setStageScale,
stageTranslateRef,
"pan"
);
const containerRef = useRef();
usePreventOverscroll(containerRef);
const [showGridPreview, setShowGridPreview] = useState(true);
const gridWidth = tokenWidth;
const gridX = token.defaultSize;
const gridSize = gridWidth / gridX;
const gridY = Math.ceil(tokenHeight / gridSize);
const gridHeight = gridY > 0 ? gridY * gridSize : tokenHeight;
const borderWidth = Math.max(
(Math.min(tokenWidth, gridHeight) / 200) * Math.max(1 / stageScale, 1),
1
);
return (
<Box
sx={{
width: "100%",
height: "300px",
cursor: "move",
touchAction: "none",
outline: "none",
position: "relative",
}}
bg="muted"
ref={containerRef}
{...bind()}
>
<ReactResizeDetector handleWidth handleHeight onResize={handleResize}>
<Stage
width={stageWidth}
height={stageHeight}
scale={{ x: stageScale, y: stageScale }}
x={stageWidth / 2}
y={stageHeight / 2}
offset={{ x: stageWidth / 2, y: stageHeight / 2 }}
>
<Layer ref={mapLayerRef}>
<Image
image={tokenSourceImage}
width={tokenWidth}
height={tokenHeight}
/>
{showGridPreview && (
<Group offsetY={gridHeight - tokenHeight}>
<Grid
gridX={gridX}
gridY={gridY}
width={gridWidth}
height={gridHeight}
/>
<Rect
width={gridWidth}
height={gridHeight}
fill="transparent"
stroke="rgba(255, 255, 255, 0.75)"
strokeWidth={borderWidth}
/>
</Group>
)}
</Layer>
</Stage>
</ReactResizeDetector>
<IconButton
title={showGridPreview ? "Hide Grid Preview" : "Show Grid Preview"}
aria-label={showGridPreview ? "Hide Grid Preview" : "Show Grid Preview"}
onClick={() => setShowGridPreview(!showGridPreview)}
bg="overlay"
sx={{
borderRadius: "50%",
position: "absolute",
bottom: 0,
right: 0,
}}
m={2}
p="6px"
>
{showGridPreview ? <GridOnIcon /> : <GridOffIcon />}
</IconButton>
</Box>
);
}
export default TokenPreview;

View File

@ -3,6 +3,7 @@ import { Button, Flex, Label } from "theme-ui";
import Modal from "../components/Modal";
import TokenSettings from "../components/token/TokenSettings";
import TokenPreview from "../components/token/TokenPreview";
import TokenDataContext from "../contexts/TokenDataContext";
@ -62,6 +63,7 @@ function EditTokenModal({ isOpen, onDone, token }) {
<Label pt={2} pb={1}>
Edit token
</Label>
<TokenPreview token={selectedTokenWithChanges} />
<TokenSettings
token={selectedTokenWithChanges}
onSettingsChange={handleTokenSettingsChange}