Added token preview to edit token modal and refactored Grid from Map Grid
This commit is contained in:
parent
0235bdb7ef
commit
e9e7794027
72
src/components/Grid.js
Normal file
72
src/components/Grid.js
Normal 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;
|
@ -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 = {
|
||||
|
186
src/components/token/TokenPreview.js
Normal file
186
src/components/token/TokenPreview.js
Normal 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;
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user