diff --git a/src/components/Grid.js b/src/components/Grid.js
index 0b2ffec..2850b0c 100644
--- a/src/components/Grid.js
+++ b/src/components/Grid.js
@@ -1,12 +1,7 @@
import React from "react";
import { Line, Group, RegularPolygon } from "react-konva";
-import {
- getCellLocation,
- gridClipFunction,
- shouldClipCell,
- getNearestCellCoordinates,
-} from "../helpers/grid";
+import { getCellLocation } from "../helpers/grid";
import Vector2 from "../helpers/Vector2";
import { useGrid } from "../contexts/GridContext";
@@ -25,6 +20,7 @@ function Grid({ strokeWidth, stroke }) {
}
const negativeGridOffset = Vector2.multiply(gridOffset, -1);
+ const finalStrokeWidth = gridStrokeWidth * strokeWidth;
const shapes = [];
if (grid.type === "square") {
@@ -39,7 +35,7 @@ function Grid({ strokeWidth, stroke }) {
gridPixelSize.height,
]}
stroke={stroke}
- strokeWidth={gridStrokeWidth * strokeWidth}
+ strokeWidth={finalStrokeWidth}
opacity={0.5}
offset={negativeGridOffset}
/>
@@ -56,54 +52,56 @@ function Grid({ strokeWidth, stroke }) {
y * gridCellPixelSize.height,
]}
stroke={stroke}
- strokeWidth={gridStrokeWidth * strokeWidth}
+ strokeWidth={finalStrokeWidth}
opacity={0.5}
offset={negativeGridOffset}
/>
);
}
} else if (grid.type === "hexVertical" || grid.type === "hexHorizontal") {
- // Start at -1 to overshoot the bounds of the grid to ensure all lines are drawn
- for (let x = -1; x < grid.size.x; x++) {
- for (let y = -1; y < grid.size.y; y++) {
+ // End at grid size + 1 to overshoot the bounds of the grid to ensure all lines are drawn
+ for (let x = 0; x < grid.size.x + 1; x++) {
+ for (let y = 0; y < grid.size.y + 1; y++) {
const cellLocation = getCellLocation(grid, x, y, gridCellPixelSize);
shapes.push(
- gridClipFunction(context, grid, x, y, gridCellPixelSize))
- }
x={cellLocation.x}
y={cellLocation.y}
offset={negativeGridOffset}
>
- {
- console.log(
- getNearestCellCoordinates(
- grid,
- cellLocation.x,
- cellLocation.y,
- gridCellPixelSize
- )
- );
- }}
- />
+ {/* Offset the hex tile to align to top left of grid */}
+
+
+
);
}
}
}
- return {shapes};
+ return (
+ {
+ context.rect(
+ gridOffset.x - finalStrokeWidth / 2,
+ gridOffset.y - finalStrokeWidth / 2,
+ gridPixelSize.width + finalStrokeWidth,
+ gridPixelSize.height + finalStrokeWidth
+ );
+ }}
+ >
+ {shapes}
+
+ );
}
Grid.defaultProps = {
diff --git a/src/components/map/MapToken.js b/src/components/map/MapToken.js
index a96c386..eae04c8 100644
--- a/src/components/map/MapToken.js
+++ b/src/components/map/MapToken.js
@@ -178,9 +178,12 @@ function MapToken({
}
}
- const tokenWidth = gridCellPixelSize.width * tokenState.size;
- const tokenHeight =
- (gridCellPixelSize.width / tokenAspectRatio) * tokenState.size;
+ const minCellSize = Math.min(
+ gridCellPixelSize.width,
+ gridCellPixelSize.height
+ );
+ const tokenWidth = minCellSize * tokenState.size;
+ const tokenHeight = (minCellSize / tokenAspectRatio) * tokenState.size;
const debouncedStageScale = useDebounce(stageScale, 50);
const imageRef = useRef();
diff --git a/src/components/note/Note.js b/src/components/note/Note.js
index 2a45c97..914b78a 100644
--- a/src/components/note/Note.js
+++ b/src/components/note/Note.js
@@ -27,7 +27,11 @@ function Note({
const { mapWidth, mapHeight, setPreventMapInteraction } = useMapInteraction();
const { gridCellPixelSize } = useGrid();
- const noteWidth = gridCellPixelSize.width * note.size;
+ const minCellSize = Math.min(
+ gridCellPixelSize.width,
+ gridCellPixelSize.height
+ );
+ const noteWidth = minCellSize * note.size;
const noteHeight = noteWidth;
const notePadding = noteWidth / 10;
diff --git a/src/helpers/Vector2.js b/src/helpers/Vector2.js
index 01dce79..30c430c 100644
--- a/src/helpers/Vector2.js
+++ b/src/helpers/Vector2.js
@@ -2,6 +2,7 @@ import {
toRadians,
roundTo as roundToNumber,
lerp as lerpNumber,
+ floorTo as floorToNumber,
} from "./shared";
/**
@@ -186,6 +187,19 @@ class Vector2 {
};
}
+ /**
+ * Floors `p` to the nearest value of `to`
+ * @param {Vector2} p
+ * @param {Vector2} to
+ * @returns {Vector2}
+ */
+ static floorTo(p, to) {
+ return {
+ x: floorToNumber(p.x, to.x),
+ y: floorToNumber(p.y, to.y),
+ };
+ }
+
/**
* @param {Vector2} a
* @returns {Vector2} The component wise sign of `a`
@@ -473,6 +487,20 @@ class Vector2 {
return resampledPoints;
}
+
+ /**
+ * Rotate a vector 90 degrees
+ * @param {Vector2} p Point
+ * @param {("counterClockwise"|"clockwise")=} direction Direction to rotate the vector
+ * @returns {Vector2}
+ */
+ static rotate90(p, direction = "clockwise") {
+ if (direction === "clockwise") {
+ return { x: p.y, y: -p.x };
+ } else {
+ return { x: -p.y, y: p.x };
+ }
+ }
}
export default Vector2;
diff --git a/src/helpers/grid.js b/src/helpers/grid.js
index e8707e9..f09d5e0 100644
--- a/src/helpers/grid.js
+++ b/src/helpers/grid.js
@@ -57,7 +57,7 @@ export function getCellPixelSize(grid, gridWidth, gridHeight) {
}
/**
- * Find the location of a cell in the grid.
+ * Find the center location of a cell in the grid.
* Hex is addressed in an offset coordinate system with even numbered columns/rows offset to the right
* @param {Grid} grid
* @param {number} col X-axis coordinate of the cell
@@ -68,16 +68,19 @@ export function getCellPixelSize(grid, gridWidth, gridHeight) {
export function getCellLocation(grid, col, row, cellSize) {
switch (grid.type) {
case "square":
- return { x: col * cellSize.width, y: row * cellSize.height };
+ return {
+ x: col * cellSize.width + cellSize.width / 2,
+ y: row * cellSize.height + cellSize.height / 2,
+ };
case "hexVertical":
return {
- x: col * cellSize.width + (cellSize.width * (1 + (row & 1))) / 2,
- y: row * cellSize.height * (3 / 4) + cellSize.radius,
+ x: cellSize.radius * SQRT3 * (col - 0.5 * (row & 1)),
+ y: ((cellSize.radius * 3) / 2) * row,
};
case "hexHorizontal":
return {
- x: col * cellSize.width * (3 / 4) + cellSize.radius,
- y: row * cellSize.height + (cellSize.height * (1 + (col & 1))) / 2,
+ x: ((cellSize.radius * 3) / 2) * col,
+ y: cellSize.radius * SQRT3 * (row - 0.5 * (col & 1)),
};
default:
throw GRID_TYPE_NOT_IMPLEMENTED;
@@ -95,7 +98,7 @@ export function getCellLocation(grid, col, row, cellSize) {
export function getNearestCellCoordinates(grid, x, y, cellSize) {
switch (grid.type) {
case "square":
- return Vector2.roundTo({ x, y }, cellSize);
+ return Vector2.divide(Vector2.floorTo({ x, y }, cellSize), cellSize);
case "hexVertical":
// Find nearest cell in cube coordinates the convert to offset coordinates
const cubeXVert = ((SQRT3 / 3) * x - (1 / 3) * y) / cellSize.radius;
@@ -115,62 +118,49 @@ export function getNearestCellCoordinates(grid, x, y, cellSize) {
}
/**
- * Whether the cell located at `x, y` is out of bounds of the grid
+ * Find the corners of a grid cell
* @param {Grid} grid
- * @param {number} col X-axis coordinate of the cell
- * @param {number} row Y-axis coordinate of the cell
- * @returns {boolean}
+ * @param {number} x X location of the cell in pixels
+ * @param {number} y Y location of the cell in pixels
+ * @param {Size} cellSize Cell size in pixels
+ * @returns {Vector2[]}
*/
-export function shouldClipCell(grid, col, row) {
- if (col < 0 || row < 0) {
- return true;
- }
+export function getCellCorners(grid, x, y, cellSize) {
+ const position = new Vector2(x, y);
switch (grid.type) {
case "square":
- return false;
+ const halfSize = Vector2.multiply(cellSize, 0.5);
+ return [
+ Vector2.add(position, Vector2.multiply(halfSize, { x: -1, y: -1 })),
+ Vector2.add(position, Vector2.multiply(halfSize, { x: 1, y: -1 })),
+ Vector2.add(position, Vector2.multiply(halfSize, { x: 1, y: 1 })),
+ Vector2.add(position, Vector2.multiply(halfSize, { x: -1, y: 1 })),
+ ];
case "hexVertical":
- return col === grid.size.x - 1 && (row & 1) !== 0;
+ const up = Vector2.subtract(position, { x: 0, y: cellSize.radius });
+ return [
+ up,
+ Vector2.rotate(up, position, 60),
+ Vector2.rotate(up, position, 120),
+ Vector2.rotate(up, position, 180),
+ Vector2.rotate(up, position, 240),
+ Vector2.rotate(up, position, 300),
+ ];
case "hexHorizontal":
- return row === grid.size.y - 1 && (col & 1) !== 0;
+ const right = Vector2.add(position, { x: cellSize.radius, y: 0 });
+ return [
+ right,
+ Vector2.rotate(right, position, 60),
+ Vector2.rotate(right, position, 120),
+ Vector2.rotate(right, position, 180),
+ Vector2.rotate(right, position, 240),
+ Vector2.rotate(right, position, 300),
+ ];
default:
throw GRID_TYPE_NOT_IMPLEMENTED;
}
}
-/**
- * Canvas clip function for culling hex cells that overshoot/undershoot the grid
- * @param {CanvasRenderingContext2D} context The canvas context of the clip function
- * @param {Grid} grid
- * @param {number} col X-axis coordinate of the cell
- * @param {number} row Y-axis coordinate of the cell
- * @param {Size} cellSize Cell size in pixels
- */
-export function gridClipFunction(context, grid, col, row, cellSize) {
- // Clip the undershooting cells unless they are needed to fill out a specific grid type
- if (
- (col < 0 && grid.type !== "hexVertical") ||
- (col < 0 && (row & 1) === 0)
- ) {
- return;
- }
- if (
- (row < 0 && grid.type !== "hexHorizontal") ||
- (row < 0 && (col & 1) === 0)
- ) {
- return;
- }
- context.rect(
- col < 0 ? 0 : -cellSize.radius,
- row < 0 ? 0 : -cellSize.radius,
- col > 0 && grid.type === "hexVertical"
- ? cellSize.radius
- : cellSize.radius * 2,
- row > 0 && grid.type === "hexVertical"
- ? cellSize.radius * 2
- : cellSize.radius
- );
-}
-
/**
* Get the height of a grid based off of its width
* @param {Grid} grid
@@ -243,7 +233,7 @@ export function getGridMaxZoom(grid) {
* @param {("hexVertical"|"hexHorizontal")} type
* @returns {Vector2}
*/
-function hexCubeToOffset(cube, type) {
+export function hexCubeToOffset(cube, type) {
if (type === "hexVertical") {
const x = cube.x + (cube.z + (cube.z & 1)) / 2;
const y = cube.z;
@@ -260,7 +250,7 @@ function hexCubeToOffset(cube, type) {
* @param {("hexVertical"|"hexHorizontal")} type
* @returns {Vector3}
*/
-function hexOffsetToCube(offset, type) {
+export function hexOffsetToCube(offset, type) {
if (type === "hexVertical") {
const x = offset.x - (offset.y - (offset.y & 1)) / 2;
const z = offset.y;
diff --git a/src/helpers/konva.js b/src/helpers/konva.js
index 2eae222..39f6b98 100644
--- a/src/helpers/konva.js
+++ b/src/helpers/konva.js
@@ -223,7 +223,7 @@ export function Trail({ position, size, duration, segments, color }) {
const drawOffsetLine = (from, to, alpha) => {
const forward = Vector2.normalize(Vector2.subtract(from, to));
// Rotate the forward vector 90 degrees based off of the direction
- const side = { x: forward.y, y: -forward.x };
+ const side = Vector2.rotate90(forward);
// Offset the `to` position by the size of the point and in the side direction
const toSize = (alpha * size) / 2;
diff --git a/src/helpers/shared.js b/src/helpers/shared.js
index 7d420fb..a9ed206 100644
--- a/src/helpers/shared.js
+++ b/src/helpers/shared.js
@@ -28,6 +28,10 @@ export function roundTo(x, to) {
return Math.round(x / to) * to;
}
+export function floorTo(x, to) {
+ return Math.floor(x / to) * to;
+}
+
export function toRadians(angle) {
return angle * (Math.PI / 180);
}
diff --git a/src/hooks/useGridSnapping.js b/src/hooks/useGridSnapping.js
index 3336115..7da3cb4 100644
--- a/src/hooks/useGridSnapping.js
+++ b/src/hooks/useGridSnapping.js
@@ -1,7 +1,13 @@
+// Load Konva for auto complete
+// eslint-disable-next-line no-unused-vars
import Konva from "konva";
import Vector2 from "../helpers/Vector2";
-import { getCellLocation } from "../helpers/grid";
+import {
+ getCellLocation,
+ getNearestCellCoordinates,
+ getCellCorners,
+} from "../helpers/grid";
import { useGrid } from "../contexts/GridContext";
@@ -10,79 +16,61 @@ import { useGrid } from "../contexts/GridContext";
* @param {number} snappingThreshold 1 = Always snap, 0 = never snap
*/
function useGridSnapping(snappingThreshold) {
- const { gridOffset, gridCellPixelSize } = useGrid();
+ const { grid, gridOffset, gridCellPixelSize } = useGrid();
/**
* @param {Konva.Node} node The node to snap
*/
function snapNodeToGrid(node) {
const position = node.position();
- const halfSize = Vector2.divide({ x: node.width(), y: node.height() }, 2);
-
- // Offsets to tranform the centered position into the four corners
- const cornerOffsets = [
- { x: 0, y: 0 },
- // halfSize,
- // { x: -halfSize.x, y: -halfSize.y },
- // { x: halfSize.x, y: -halfSize.y },
- // { x: -halfSize.x, y: halfSize.y },
- ];
-
- // Minimum distance from a corner to the grid
- let minCornerGridDistance = Number.MAX_VALUE;
- // Minimum component of the difference between the min corner and the grid
- let minCornerMinComponent;
- // Closest grid value
- let minGridSnap;
-
- // Find the closest corner to the grid
- for (let cornerOffset of cornerOffsets) {
- const corner = Vector2.add(position, cornerOffset);
- // Transform into gridOffset space, round, then transform back
- const gridSnap = Vector2.add(
- Vector2.roundTo(
- Vector2.subtract(corner, gridOffset),
- gridCellPixelSize
- ),
- gridOffset
+ // Account for grid offset
+ let offsetPosition = Vector2.subtract(position, gridOffset);
+ // Move hex tiles to top left
+ if (grid.type === "hexVertical" || grid.type === "hexHorizontal") {
+ offsetPosition = Vector2.subtract(
+ offsetPosition,
+ Vector2.multiply(gridCellPixelSize, 0.5)
);
- const gridDistance = Vector2.length(Vector2.subtract(gridSnap, corner));
- const minComponent = Vector2.min(gridCellPixelSize);
- if (gridDistance < minCornerGridDistance) {
- minCornerGridDistance = gridDistance;
- minCornerMinComponent = minComponent;
- // Move the grid value back to the center
- minGridSnap = Vector2.subtract(gridSnap, cornerOffset);
- }
}
-
- // Snap to center of grid
- // Subtract gridOffset and half grid size to transform it into gridOffset half space then transform it back
- const halfGridSize = Vector2.multiply(gridCellPixelSize, 0.5);
- const centerSnap = Vector2.add(
- Vector2.add(
- Vector2.roundTo(
- Vector2.subtract(
- Vector2.subtract(position, gridOffset),
- halfGridSize
- ),
- gridCellPixelSize
- ),
- halfGridSize
- ),
- gridOffset
+ const nearsetCell = getNearestCellCoordinates(
+ grid,
+ offsetPosition.x,
+ offsetPosition.y,
+ gridCellPixelSize
);
- const centerDistance = Vector2.length(
- Vector2.subtract(centerSnap, position)
+ const cellPosition = getCellLocation(
+ grid,
+ nearsetCell.x,
+ nearsetCell.y,
+ gridCellPixelSize
+ );
+ const cellCorners = getCellCorners(
+ grid,
+ cellPosition.x,
+ cellPosition.y,
+ gridCellPixelSize
);
- if (minCornerGridDistance < minCornerMinComponent * snappingThreshold) {
- node.position(minGridSnap);
- } else if (
- centerDistance <
- Vector2.min(gridCellPixelSize) * snappingThreshold
- ) {
- node.position(centerSnap);
+ const snapPoints = [cellPosition, ...cellCorners];
+
+ for (let snapPoint of snapPoints) {
+ const distanceToSnapPoint = Vector2.distance(offsetPosition, snapPoint);
+ if (
+ distanceToSnapPoint <
+ Vector2.min(gridCellPixelSize) * snappingThreshold
+ ) {
+ // Reverse grid offset
+ let offsetSnapPoint = Vector2.add(snapPoint, gridOffset);
+ // Reverse offset for hex tiles
+ if (grid.type === "hexVertical" || grid.type === "hexHorizontal") {
+ offsetSnapPoint = Vector2.add(
+ offsetSnapPoint,
+ Vector2.multiply(gridCellPixelSize, 0.5)
+ );
+ }
+ node.position(offsetSnapPoint);
+ return;
+ }
}
}