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 }) {
+