diff --git a/src/components/Grid.js b/src/components/Grid.js new file mode 100644 index 0000000..e50de78 --- /dev/null +++ b/src/components/Grid.js @@ -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( + + ); + } + for (let y = 1; y < gridY; y++) { + lines.push( + + ); + } + + return {lines}; +} + +Grid.defaultProps = { + strokeWidth: 0.1, + gridInset: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 } }, + stroke: "white", +}; + +export default Grid; diff --git a/src/components/map/MapGrid.js b/src/components/map/MapGrid.js index 66ca389..589d0e4 100644 --- a/src/components/map/MapGrid.js +++ b/src/components/map/MapGrid.js @@ -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( - - ); - } - for (let y = 1; y < gridY; y++) { - lines.push( - - ); - } - - return {lines}; + return ( + + ); } MapGrid.defaultProps = { diff --git a/src/components/token/TokenPreview.js b/src/components/token/TokenPreview.js new file mode 100644 index 0000000..33033e4 --- /dev/null +++ b/src/components/token/TokenPreview.js @@ -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 ( + + + + + + {showGridPreview && ( + + + + + )} + + + + setShowGridPreview(!showGridPreview)} + bg="overlay" + sx={{ + borderRadius: "50%", + position: "absolute", + bottom: 0, + right: 0, + }} + m={2} + p="6px" + > + {showGridPreview ? : } + + + ); +} + +export default TokenPreview; diff --git a/src/modals/EditTokenModal.js b/src/modals/EditTokenModal.js index 5512135..3fb6d5e 100644 --- a/src/modals/EditTokenModal.js +++ b/src/modals/EditTokenModal.js @@ -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 }) { +