Add basic hex functionality and clean up hooks and Vector2 class
This commit is contained in:
parent
924b3e2481
commit
de84e77e58
@ -1,71 +1,121 @@
|
||||
import React from "react";
|
||||
import { Line, Group } from "react-konva";
|
||||
import { Line, Group, RegularPolygon } from "react-konva";
|
||||
|
||||
import { getStrokeWidth } from "../helpers/drawing";
|
||||
import { getCellSize, getCellLocation, shouldClampCell } from "../helpers/grid";
|
||||
|
||||
function Grid({ gridX, gridY, gridInset, strokeWidth, width, height, stroke }) {
|
||||
if (!gridX || !gridY) {
|
||||
function Grid({ grid, strokeWidth, width, height, stroke }) {
|
||||
if (!grid?.size.x || !grid?.size.y) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const gridSizeNormalized = {
|
||||
x: (gridInset.bottomRight.x - gridInset.topLeft.x) / gridX,
|
||||
y: (gridInset.bottomRight.y - gridInset.topLeft.y) / gridY,
|
||||
x: (grid.inset.bottomRight.x - grid.inset.topLeft.x) / grid.size.x,
|
||||
y: (grid.inset.bottomRight.y - grid.inset.topLeft.y) / grid.size.y,
|
||||
};
|
||||
|
||||
const insetWidth = (gridInset.bottomRight.x - gridInset.topLeft.x) * width;
|
||||
const insetHeight = (gridInset.bottomRight.y - gridInset.topLeft.y) * height;
|
||||
const insetWidth = (grid.inset.bottomRight.x - grid.inset.topLeft.x) * width;
|
||||
const insetHeight =
|
||||
(grid.inset.bottomRight.y - grid.inset.topLeft.y) * height;
|
||||
|
||||
const lineSpacingX = insetWidth / gridX;
|
||||
const lineSpacingY = insetHeight / gridY;
|
||||
const offsetX = grid.inset.topLeft.x * width * -1;
|
||||
const offsetY = grid.inset.topLeft.y * height * -1;
|
||||
|
||||
const offsetX = gridInset.topLeft.x * width * -1;
|
||||
const offsetY = gridInset.topLeft.y * height * -1;
|
||||
const cellSize = getCellSize(grid, insetWidth, insetHeight);
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
const shapes = [];
|
||||
if (grid.type === "square") {
|
||||
for (let x = 1; x < grid.size.x; x++) {
|
||||
shapes.push(
|
||||
<Line
|
||||
key={`grid_x_${x}`}
|
||||
points={[x * cellSize.width, 0, x * cellSize.width, insetHeight]}
|
||||
stroke={stroke}
|
||||
strokeWidth={getStrokeWidth(
|
||||
strokeWidth,
|
||||
gridSizeNormalized,
|
||||
width,
|
||||
height
|
||||
)}
|
||||
opacity={0.5}
|
||||
offsetX={offsetX}
|
||||
offsetY={offsetY}
|
||||
/>
|
||||
);
|
||||
}
|
||||
for (let y = 1; y < grid.size.y; y++) {
|
||||
shapes.push(
|
||||
<Line
|
||||
key={`grid_y_${y}`}
|
||||
points={[0, y * cellSize.height, insetWidth, y * cellSize.height]}
|
||||
stroke={stroke}
|
||||
strokeWidth={getStrokeWidth(
|
||||
strokeWidth,
|
||||
gridSizeNormalized,
|
||||
width,
|
||||
height
|
||||
)}
|
||||
opacity={0.5}
|
||||
offsetX={offsetX}
|
||||
offsetY={offsetY}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (grid.type === "hexVertical" || grid.type === "hexHorizontal") {
|
||||
for (let x = 0; x < grid.size.x; x++) {
|
||||
for (let y = 0; y < grid.size.y; y++) {
|
||||
const cellLocation = getCellLocation(grid, x, y, cellSize);
|
||||
|
||||
// If our hex shape will go past the bounds of the grid
|
||||
const overshot = shouldClampCell(grid, x, y);
|
||||
shapes.push(
|
||||
<Group
|
||||
key={`grid_${x}_${y}`}
|
||||
// Clip the hex if it will overshoot
|
||||
clipFunc={
|
||||
overshot &&
|
||||
((context) => {
|
||||
context.rect(
|
||||
-cellSize.radius,
|
||||
-cellSize.radius,
|
||||
grid.type === "hexVertical"
|
||||
? cellSize.radius
|
||||
: cellSize.radius * 2,
|
||||
grid.type === "hexVertical"
|
||||
? cellSize.radius * 2
|
||||
: cellSize.radius
|
||||
);
|
||||
})
|
||||
}
|
||||
x={cellLocation.x}
|
||||
y={cellLocation.y}
|
||||
offsetX={offsetX}
|
||||
offsetY={offsetY}
|
||||
>
|
||||
<RegularPolygon
|
||||
sides={6}
|
||||
radius={cellSize.radius}
|
||||
stroke={stroke}
|
||||
strokeWidth={getStrokeWidth(
|
||||
strokeWidth,
|
||||
gridSizeNormalized,
|
||||
width,
|
||||
height
|
||||
)}
|
||||
opacity={0.5}
|
||||
rotation={grid.type === "hexVertical" ? 0 : 90}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <Group>{lines}</Group>;
|
||||
return <Group>{shapes}</Group>;
|
||||
}
|
||||
|
||||
Grid.defaultProps = {
|
||||
strokeWidth: 0.1,
|
||||
gridInset: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 } },
|
||||
stroke: "white",
|
||||
};
|
||||
|
||||
|
@ -19,7 +19,7 @@ import SelectDiceButton from "./SelectDiceButton";
|
||||
import Divider from "../Divider";
|
||||
|
||||
import { dice } from "../../dice";
|
||||
import useSetting from "../../helpers/useSetting";
|
||||
import useSetting from "../../hooks/useSetting";
|
||||
|
||||
function DiceButtons({
|
||||
diceRolls,
|
||||
|
@ -16,7 +16,7 @@ import "@babylonjs/loaders/glTF";
|
||||
|
||||
import ReactResizeDetector from "react-resize-detector";
|
||||
|
||||
import usePreventTouch from "../../helpers/usePreventTouch";
|
||||
import usePreventTouch from "../../hooks/usePreventTouch";
|
||||
|
||||
const diceThrowSpeed = 2;
|
||||
|
||||
|
@ -24,7 +24,7 @@ import DiceTray from "../../dice/diceTray/DiceTray";
|
||||
import DiceLoadingContext from "../../contexts/DiceLoadingContext";
|
||||
|
||||
import { getDiceRoll } from "../../helpers/dice";
|
||||
import useSetting from "../../helpers/useSetting";
|
||||
import useSetting from "../../hooks/useSetting";
|
||||
|
||||
function DiceTrayOverlay({
|
||||
isOpen,
|
||||
|
@ -21,7 +21,7 @@ import FullScreenIcon from "../../icons/FullScreenIcon";
|
||||
import FullScreenExitIcon from "../../icons/FullScreenExitIcon";
|
||||
import NoteToolIcon from "../../icons/NoteToolIcon";
|
||||
|
||||
import useSetting from "../../helpers/useSetting";
|
||||
import useSetting from "../../hooks/useSetting";
|
||||
|
||||
function MapContols({
|
||||
onMapChange,
|
||||
|
@ -5,7 +5,7 @@ import { Group, Line, Rect, Circle } from "react-konva";
|
||||
import MapInteractionContext from "../../contexts/MapInteractionContext";
|
||||
import MapStageContext from "../../contexts/MapStageContext";
|
||||
|
||||
import { compare as comparePoints } from "../../helpers/vector2";
|
||||
import Vector2 from "../../helpers/Vector2";
|
||||
import {
|
||||
getBrushPosition,
|
||||
getDefaultShapeData,
|
||||
@ -92,7 +92,7 @@ function MapDrawing({
|
||||
setDrawingShape((prevShape) => {
|
||||
const prevPoints = prevShape.data.points;
|
||||
if (
|
||||
comparePoints(
|
||||
Vector2.compare(
|
||||
prevPoints[prevPoints.length - 1],
|
||||
brushPosition,
|
||||
0.001
|
||||
|
@ -3,12 +3,13 @@ import { Box, IconButton } from "theme-ui";
|
||||
import { Stage, Layer, Image } from "react-konva";
|
||||
import ReactResizeDetector from "react-resize-detector";
|
||||
|
||||
import useMapImage from "../../helpers/useMapImage";
|
||||
import usePreventOverscroll from "../../helpers/usePreventOverscroll";
|
||||
import useStageInteraction from "../../helpers/useStageInteraction";
|
||||
import useImageCenter from "../../helpers/useImageCenter";
|
||||
import { getMapDefaultInset, getMapMaxZoom } from "../../helpers/map";
|
||||
import useResponsiveLayout from "../../helpers/useResponsiveLayout";
|
||||
import useMapImage from "../../hooks/useMapImage";
|
||||
import usePreventOverscroll from "../../hooks/usePreventOverscroll";
|
||||
import useStageInteraction from "../../hooks/useStageInteraction";
|
||||
import useImageCenter from "../../hooks/useImageCenter";
|
||||
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
|
||||
|
||||
import { getMapDefaultInset, getGridMaxZoom } from "../../helpers/grid";
|
||||
|
||||
import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
|
||||
import KeyboardContext from "../../contexts/KeyboardContext";
|
||||
@ -65,7 +66,7 @@ function MapEditor({ map, onSettingsChange }) {
|
||||
setStageScale,
|
||||
stageTranslateRef,
|
||||
mapLayerRef.current,
|
||||
getMapMaxZoom(map),
|
||||
getGridMaxZoom(map.grid),
|
||||
"pan",
|
||||
preventMapInteraction
|
||||
);
|
||||
|
@ -14,7 +14,7 @@ import diagonalPattern from "../../images/DiagonalPattern.png";
|
||||
import MapInteractionContext from "../../contexts/MapInteractionContext";
|
||||
import MapStageContext from "../../contexts/MapStageContext";
|
||||
|
||||
import { compare as comparePoints } from "../../helpers/vector2";
|
||||
import Vector2 from "../../helpers/Vector2";
|
||||
import {
|
||||
getFogBrushPosition,
|
||||
simplifyPoints,
|
||||
@ -23,8 +23,9 @@ import {
|
||||
} from "../../helpers/drawing";
|
||||
import colors from "../../helpers/colors";
|
||||
import { HoleyLine, Tick } from "../../helpers/konva";
|
||||
import useKeyboard from "../../helpers/useKeyboard";
|
||||
import useDebounce from "../../helpers/useDebounce";
|
||||
|
||||
import useKeyboard from "../../hooks/useKeyboard";
|
||||
import useDebounce from "../../hooks/useDebounce";
|
||||
|
||||
function MapFog({
|
||||
map,
|
||||
@ -120,7 +121,7 @@ function MapFog({
|
||||
setDrawingShape((prevShape) => {
|
||||
const prevPoints = prevShape.data.points;
|
||||
if (
|
||||
comparePoints(
|
||||
Vector2.compare(
|
||||
prevPoints[prevPoints.length - 1],
|
||||
brushPosition,
|
||||
0.001
|
||||
|
@ -3,7 +3,7 @@ import useImage from "use-image";
|
||||
|
||||
import MapInteractionContext from "../../contexts/MapInteractionContext";
|
||||
|
||||
import useDataSource from "../../helpers/useDataSource";
|
||||
import useDataSource from "../../hooks/useDataSource";
|
||||
import { mapSources as defaultMapSources } from "../../maps";
|
||||
|
||||
import { getImageLightness } from "../../helpers/image";
|
||||
@ -33,16 +33,9 @@ function MapGrid({ map, strokeWidth }) {
|
||||
}
|
||||
}, [mapImage, mapLoadingStatus]);
|
||||
|
||||
const gridX = map && map.grid.size.x;
|
||||
const gridY = map && map.grid.size.y;
|
||||
|
||||
const gridInset = map && map.grid.inset;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
gridX={gridX}
|
||||
gridY={gridY}
|
||||
gridInset={gridInset}
|
||||
grid={map?.grid}
|
||||
strokeWidth={strokeWidth}
|
||||
width={mapWidth}
|
||||
height={mapHeight}
|
||||
|
@ -3,8 +3,9 @@ import { Group, Circle, Rect } from "react-konva";
|
||||
|
||||
import MapInteractionContext from "../../contexts/MapInteractionContext";
|
||||
|
||||
import * as Vector2 from "../../helpers/vector2";
|
||||
import useKeyboard from "../../helpers/useKeyboard";
|
||||
import Vector2 from "../../helpers/Vector2";
|
||||
|
||||
import useKeyboard from "../../hooks/useKeyboard";
|
||||
|
||||
function MapGridEditor({ map, onGridChange }) {
|
||||
const {
|
||||
|
@ -4,12 +4,13 @@ import ReactResizeDetector from "react-resize-detector";
|
||||
import { Stage, Layer, Image } from "react-konva";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import useMapImage from "../../helpers/useMapImage";
|
||||
import usePreventOverscroll from "../../helpers/usePreventOverscroll";
|
||||
import useKeyboard from "../../helpers/useKeyboard";
|
||||
import useStageInteraction from "../../helpers/useStageInteraction";
|
||||
import useImageCenter from "../../helpers/useImageCenter";
|
||||
import { getMapMaxZoom } from "../../helpers/map";
|
||||
import useMapImage from "../../hooks/useMapImage";
|
||||
import usePreventOverscroll from "../../hooks/usePreventOverscroll";
|
||||
import useKeyboard from "../../hooks/useKeyboard";
|
||||
import useStageInteraction from "../../hooks/useStageInteraction";
|
||||
import useImageCenter from "../../hooks/useImageCenter";
|
||||
|
||||
import { getGridMaxZoom } from "../../helpers/grid";
|
||||
|
||||
import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
|
||||
import MapStageContext, {
|
||||
@ -87,7 +88,7 @@ function MapInteraction({
|
||||
setStageScale,
|
||||
stageTranslateRef,
|
||||
mapLayerRef.current,
|
||||
getMapMaxZoom(map),
|
||||
getGridMaxZoom(map?.grid),
|
||||
selectedToolId,
|
||||
preventMapInteraction,
|
||||
{
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
getUpdatedShapeData,
|
||||
getStrokeWidth,
|
||||
} from "../../helpers/drawing";
|
||||
import * as Vector2 from "../../helpers/vector2";
|
||||
import Vector2 from "../../helpers/Vector2";
|
||||
|
||||
function MapMeasure({ map, selectedToolSettings, active, gridSize }) {
|
||||
const { stageScale, mapWidth, mapHeight, interactionEmitter } = useContext(
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
getRelativePointerPositionNormalized,
|
||||
Trail,
|
||||
} from "../../helpers/konva";
|
||||
import { multiply } from "../../helpers/vector2";
|
||||
import Vector2 from "../../helpers/Vector2";
|
||||
|
||||
import colors from "../../helpers/colors";
|
||||
|
||||
@ -69,7 +69,7 @@ function MapPointer({
|
||||
<Group>
|
||||
{visible && (
|
||||
<Trail
|
||||
position={multiply(position, { x: mapWidth, y: mapHeight })}
|
||||
position={Vector2.multiply(position, { x: mapWidth, y: mapHeight })}
|
||||
color={colors[color]}
|
||||
size={size}
|
||||
duration={200}
|
||||
|
@ -16,6 +16,12 @@ const qualitySettings = [
|
||||
{ value: "original", label: "Original" },
|
||||
];
|
||||
|
||||
const gridTypeSettings = [
|
||||
{ value: "square", label: "Square" },
|
||||
{ value: "hexVertical", label: "Hex Vertical" },
|
||||
{ value: "hexHorizontal", label: "Hex Horizontal" },
|
||||
];
|
||||
|
||||
function MapSettings({
|
||||
map,
|
||||
mapState,
|
||||
@ -141,13 +147,15 @@ function MapSettings({
|
||||
<Box mb={1} sx={{ width: "50%" }}>
|
||||
<Label mb={1}>Grid Type</Label>
|
||||
<Select
|
||||
defaultValue={{ value: "square", label: "Square" }}
|
||||
isDisabled={mapEmpty || map.type === "default"}
|
||||
options={[
|
||||
{ value: "square", label: "Square" },
|
||||
{ value: "hex", label: "Hex (Coming Soon)" },
|
||||
]}
|
||||
isOptionDisabled={(option) => option.value === "hex"}
|
||||
options={gridTypeSettings}
|
||||
value={
|
||||
!mapEmpty &&
|
||||
gridTypeSettings.find((s) => s.value === map.grid.type)
|
||||
}
|
||||
onChange={(option) =>
|
||||
onSettingsChange("grid", { ...map.grid, type: option.value })
|
||||
}
|
||||
isSearchable={false}
|
||||
/>
|
||||
</Box>
|
||||
|
@ -2,7 +2,7 @@ import React from "react";
|
||||
|
||||
import Tile from "../Tile";
|
||||
|
||||
import useDataSource from "../../helpers/useDataSource";
|
||||
import useDataSource from "../../hooks/useDataSource";
|
||||
import { mapSources as defaultMapSources, unknownSource } from "../../maps";
|
||||
|
||||
function MapTile({
|
||||
|
@ -13,7 +13,7 @@ import FilterBar from "../FilterBar";
|
||||
|
||||
import DatabaseContext from "../../contexts/DatabaseContext";
|
||||
|
||||
import useResponsiveLayout from "../../helpers/useResponsiveLayout";
|
||||
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
|
||||
|
||||
function MapTiles({
|
||||
maps,
|
||||
|
@ -4,10 +4,11 @@ import { useSpring, animated } from "react-spring/konva";
|
||||
import useImage from "use-image";
|
||||
import Konva from "konva";
|
||||
|
||||
import useDataSource from "../../helpers/useDataSource";
|
||||
import useDebounce from "../../helpers/useDebounce";
|
||||
import usePrevious from "../../helpers/usePrevious";
|
||||
import { snapNodeToMap } from "../../helpers/map";
|
||||
import useDataSource from "../../hooks/useDataSource";
|
||||
import useDebounce from "../../hooks/useDebounce";
|
||||
import usePrevious from "../../hooks/usePrevious";
|
||||
|
||||
import { snapNodeToGrid } from "../../helpers/grid";
|
||||
|
||||
import AuthContext from "../../contexts/AuthContext";
|
||||
import MapInteractionContext from "../../contexts/MapInteractionContext";
|
||||
@ -86,7 +87,13 @@ function MapToken({
|
||||
const tokenGroup = event.target;
|
||||
// Snap to corners of grid
|
||||
if (map.snapToGrid) {
|
||||
snapNodeToMap(map, mapWidth, mapHeight, tokenGroup, snappingThreshold);
|
||||
snapNodeToGrid(
|
||||
map.grid,
|
||||
mapWidth,
|
||||
mapHeight,
|
||||
tokenGroup,
|
||||
snappingThreshold
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ import RedoButton from "./RedoButton";
|
||||
|
||||
import Divider from "../../Divider";
|
||||
|
||||
import useKeyboard from "../../../helpers/useKeyboard";
|
||||
import useKeyboard from "../../../hooks/useKeyboard";
|
||||
|
||||
function DrawingToolSettings({
|
||||
settings,
|
||||
|
@ -20,7 +20,7 @@ import ToolSection from "./ToolSection";
|
||||
|
||||
import Divider from "../../Divider";
|
||||
|
||||
import useKeyboard from "../../../helpers/useKeyboard";
|
||||
import useKeyboard from "../../../hooks/useKeyboard";
|
||||
|
||||
function BrushToolSettings({
|
||||
settings,
|
||||
|
@ -9,7 +9,7 @@ import MeasureAlternatingIcon from "../../../icons/MeasureAlternatingIcon";
|
||||
|
||||
import Divider from "../../Divider";
|
||||
|
||||
import useKeyboard from "../../../helpers/useKeyboard";
|
||||
import useKeyboard from "../../../hooks/useKeyboard";
|
||||
|
||||
function MeasureToolSettings({ settings, onSettingChange }) {
|
||||
// Keyboard shortcuts
|
||||
|
@ -5,9 +5,10 @@ import { useSpring, animated } from "react-spring/konva";
|
||||
import AuthContext from "../../contexts/AuthContext";
|
||||
import MapInteractionContext from "../../contexts/MapInteractionContext";
|
||||
|
||||
import { snapNodeToMap } from "../../helpers/map";
|
||||
import { snapNodeToGrid } from "../../helpers/grid";
|
||||
import colors from "../../helpers/colors";
|
||||
import usePrevious from "../../helpers/usePrevious";
|
||||
|
||||
import usePrevious from "../../hooks/usePrevious";
|
||||
|
||||
const snappingThreshold = 1 / 5;
|
||||
|
||||
@ -38,7 +39,13 @@ function Note({
|
||||
const noteGroup = event.target;
|
||||
// Snap to corners of grid
|
||||
if (map.snapToGrid) {
|
||||
snapNodeToMap(map, mapWidth, mapHeight, noteGroup, snappingThreshold);
|
||||
snapNodeToGrid(
|
||||
map.grid,
|
||||
mapWidth,
|
||||
mapHeight,
|
||||
noteGroup,
|
||||
snappingThreshold
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ import MapMenu from "../map/MapMenu";
|
||||
|
||||
import colors, { colorOptions } from "../../helpers/colors";
|
||||
|
||||
import usePrevious from "../../helpers/usePrevious";
|
||||
import usePrevious from "../../hooks/usePrevious";
|
||||
|
||||
import LockIcon from "../../icons/TokenLockIcon";
|
||||
import UnlockIcon from "../../icons/TokenUnlockIcon";
|
||||
|
@ -5,7 +5,7 @@ import ExpandMoreDiceIcon from "../../icons/ExpandMoreDiceIcon";
|
||||
|
||||
import { DiceLoadingProvider } from "../../contexts/DiceLoadingContext";
|
||||
|
||||
import useSetting from "../../helpers/useSetting";
|
||||
import useSetting from "../../hooks/useSetting";
|
||||
|
||||
import LoadingOverlay from "../LoadingOverlay";
|
||||
|
||||
|
@ -11,7 +11,7 @@ import StartTimerButton from "./StartTimerButton";
|
||||
import Timer from "./Timer";
|
||||
import DiceTrayButton from "./DiceTrayButton";
|
||||
|
||||
import useSetting from "../../helpers/useSetting";
|
||||
import useSetting from "../../hooks/useSetting";
|
||||
|
||||
import PartyContext from "../../contexts/PartyContext";
|
||||
import {
|
||||
|
@ -2,7 +2,7 @@ import React, { useEffect, useRef } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Box, Progress } from "theme-ui";
|
||||
|
||||
import usePortal from "../../helpers/usePortal";
|
||||
import usePortal from "../../hooks/usePortal";
|
||||
|
||||
function Timer({ timer, index }) {
|
||||
const progressBarRef = useRef();
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { useRef } from "react";
|
||||
import { Box, Image } from "theme-ui";
|
||||
|
||||
import usePreventTouch from "../../helpers/usePreventTouch";
|
||||
import useDataSource from "../../helpers/useDataSource";
|
||||
import usePreventTouch from "../../hooks/usePreventTouch";
|
||||
import useDataSource from "../../hooks/useDataSource";
|
||||
|
||||
import { tokenSources, unknownSource } from "../../tokens";
|
||||
|
||||
|
@ -3,7 +3,7 @@ import ReactDOM from "react-dom";
|
||||
import { Image, Box } from "theme-ui";
|
||||
import interact from "interactjs";
|
||||
|
||||
import usePortal from "../../helpers/usePortal";
|
||||
import usePortal from "../../hooks/usePortal";
|
||||
|
||||
import MapStageContext from "../../contexts/MapStageContext";
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import { Rect, Text, Group } from "react-konva";
|
||||
|
||||
import useSetting from "../../helpers/useSetting";
|
||||
import useSetting from "../../hooks/useSetting";
|
||||
|
||||
const maxTokenSize = 3;
|
||||
|
||||
|
@ -7,7 +7,7 @@ import MapMenu from "../map/MapMenu";
|
||||
|
||||
import colors, { colorOptions } from "../../helpers/colors";
|
||||
|
||||
import usePrevious from "../../helpers/usePrevious";
|
||||
import usePrevious from "../../hooks/usePrevious";
|
||||
|
||||
import LockIcon from "../../icons/TokenLockIcon";
|
||||
import UnlockIcon from "../../icons/TokenUnlockIcon";
|
||||
|
@ -4,11 +4,11 @@ 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 useImageCenter from "../../helpers/useImageCenter";
|
||||
import useResponsiveLayout from "../../helpers/useResponsiveLayout";
|
||||
import usePreventOverscroll from "../../hooks/usePreventOverscroll";
|
||||
import useStageInteraction from "../../hooks/useStageInteraction";
|
||||
import useDataSource from "../../hooks/useDataSource";
|
||||
import useImageCenter from "../../hooks/useImageCenter";
|
||||
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
|
||||
|
||||
import GridOnIcon from "../../icons/GridOnIcon";
|
||||
import GridOffIcon from "../../icons/GridOffIcon";
|
||||
@ -111,8 +111,14 @@ function TokenPreview({ token }) {
|
||||
{showGridPreview && (
|
||||
<Group offsetY={gridHeight - tokenHeight}>
|
||||
<Grid
|
||||
gridX={gridX}
|
||||
gridY={gridY}
|
||||
grid={{
|
||||
size: { x: gridX, y: gridY },
|
||||
inset: {
|
||||
topLeft: { x: 0, y: 0 },
|
||||
bottomRight: { x: 1, y: 1 },
|
||||
},
|
||||
type: "square",
|
||||
}}
|
||||
width={gridWidth}
|
||||
height={gridHeight}
|
||||
/>
|
||||
|
@ -2,7 +2,7 @@ import React from "react";
|
||||
|
||||
import Tile from "../Tile";
|
||||
|
||||
import useDataSource from "../../helpers/useDataSource";
|
||||
import useDataSource from "../../hooks/useDataSource";
|
||||
import {
|
||||
tokenSources as defaultTokenSources,
|
||||
unknownSource,
|
||||
|
@ -14,7 +14,7 @@ import FilterBar from "../FilterBar";
|
||||
|
||||
import DatabaseContext from "../../contexts/DatabaseContext";
|
||||
|
||||
import useResponsiveLayout from "../../helpers/useResponsiveLayout";
|
||||
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
|
||||
|
||||
function TokenTiles({
|
||||
tokens,
|
||||
|
@ -9,7 +9,8 @@ import ProxyToken from "./ProxyToken";
|
||||
import SelectTokensButton from "./SelectTokensButton";
|
||||
|
||||
import { fromEntries } from "../../helpers/shared";
|
||||
import useSetting from "../../helpers/useSetting";
|
||||
|
||||
import useSetting from "../../hooks/useSetting";
|
||||
|
||||
import AuthContext from "../../contexts/AuthContext";
|
||||
import TokenDataContext from "../../contexts/TokenDataContext";
|
||||
|
@ -1,11 +1,12 @@
|
||||
import React, { useEffect, useContext } from "react";
|
||||
|
||||
import useNetworkedState from "../helpers/useNetworkedState";
|
||||
import DatabaseContext from "./DatabaseContext";
|
||||
import AuthContext from "./AuthContext";
|
||||
|
||||
import { getRandomMonster } from "../helpers/monsters";
|
||||
|
||||
import useNetworkedState from "../hooks/useNetworkedState";
|
||||
|
||||
export const PlayerStateContext = React.createContext();
|
||||
export const PlayerUpdaterContext = React.createContext(() => {});
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Dexie from "dexie";
|
||||
|
||||
import blobToBuffer from "./helpers/blobToBuffer";
|
||||
import { getMapDefaultInset } from "./helpers/map";
|
||||
import { getMapDefaultInset } from "./helpers/grid";
|
||||
import { convertOldActionsToShapes } from "./actions";
|
||||
|
||||
function loadVersions(db) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import simplify from "simplify-js";
|
||||
import polygonClipping from "polygon-clipping";
|
||||
|
||||
import * as Vector2 from "./vector2";
|
||||
import Vector2 from "./Vector2";
|
||||
import { toDegrees } from "./shared";
|
||||
import { getRelativePointerPositionNormalized } from "./konva";
|
||||
import { logError } from "./logging";
|
||||
|
@ -1,8 +1,100 @@
|
||||
import GridSizeModel from "../ml/gridSize/GridSizeModel";
|
||||
import * as Vector2 from "./vector2";
|
||||
import Vector2 from "./Vector2";
|
||||
|
||||
import { logError } from "./logging";
|
||||
|
||||
const SQRT3 = 1.73205;
|
||||
|
||||
/**
|
||||
* @typedef GridInset
|
||||
* @property {Vector2} topLeft
|
||||
* @property {Vector2} bottomRight
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef Grid
|
||||
* @property {GridInset} inset
|
||||
* @property {Vector2} size
|
||||
* @property {("square"|"hexVertical"|"hexHorizontal")} type
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef CellSize
|
||||
* @property {number} width
|
||||
* @property {number} height
|
||||
* @property {number?} radius - Used for hex cell sizes
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gets the cell size for a grid taking into account inset and grid type
|
||||
* @param {Grid} grid
|
||||
* @param {number} insetWidth
|
||||
* @param {number} insetHeight
|
||||
* @returns {CellSize}
|
||||
*/
|
||||
export function getCellSize(grid, insetWidth, insetHeight) {
|
||||
if (grid.type === "square") {
|
||||
return {
|
||||
width: insetWidth / grid.size.x,
|
||||
height: insetHeight / grid.size.y,
|
||||
};
|
||||
} else if (grid.type === "hexVertical") {
|
||||
const radius = insetWidth / grid.size.x / SQRT3;
|
||||
return { width: radius * SQRT3, height: radius + radius / 2, radius };
|
||||
} else if (grid.type === "hexHorizontal") {
|
||||
const radius = insetHeight / grid.size.y / SQRT3;
|
||||
return { width: radius + radius / 2, height: radius * SQRT3, radius };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the location of cell in the grid
|
||||
* @param {Grid} grid
|
||||
* @param {number} x X-axis location of the cell
|
||||
* @param {number} y Y-axis location of the cell
|
||||
* @param {CellSize} cellSize
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
export function getCellLocation(grid, x, y, cellSize) {
|
||||
if (grid.type === "square") {
|
||||
return { x: x * cellSize.width, y: y * cellSize.height };
|
||||
} else if (grid.type === "hexVertical") {
|
||||
return {
|
||||
x: x * cellSize.width + (cellSize.width * (1 + (y % 2))) / 2,
|
||||
y: y * cellSize.height + cellSize.radius,
|
||||
};
|
||||
} else if (grid.type === "hexHorizontal") {
|
||||
return {
|
||||
x: x * cellSize.width + cellSize.radius,
|
||||
y: y * cellSize.height + (cellSize.height * (1 + (x % 2))) / 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the cell located at `x, y` is out of bounds of the grid
|
||||
* @param {Grid} grid
|
||||
* @param {number} x X-axis location of the cell
|
||||
* @param {number} y Y-axis location of the cell
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function shouldClampCell(grid, x, y) {
|
||||
if (grid.type === "hexVertical") {
|
||||
return x === grid.size.x - 1 && y % 2 !== 0;
|
||||
} else if (grid.type === "hexHorizontal") {
|
||||
return y === grid.size.y - 1 && x % 2 !== 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default inset for a map
|
||||
* @param {number} width Hidth of the map
|
||||
* @param {number} height Height of the map
|
||||
* @param {number} gridX Number of grid cells in the horizontal direction
|
||||
* @param {number} gridY Number of grid cells in the vertical direction
|
||||
* @returns {GridInset}
|
||||
*/
|
||||
export function getMapDefaultInset(width, height, gridX, gridY) {
|
||||
// Max the width
|
||||
const gridScale = width / gridX;
|
||||
@ -11,14 +103,23 @@ export function getMapDefaultInset(width, height, gridX, gridY) {
|
||||
return { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: yNorm } };
|
||||
}
|
||||
|
||||
// Get all factors of a number
|
||||
/**
|
||||
* Get all factors of a number
|
||||
* @param {number} n
|
||||
* @returns {number[]}
|
||||
*/
|
||||
function factors(n) {
|
||||
const numbers = Array.from(Array(n + 1), (_, i) => i);
|
||||
return numbers.filter((i) => n % i === 0);
|
||||
}
|
||||
|
||||
// Greatest common divisor
|
||||
// Euclidean algorithm https://en.wikipedia.org/wiki/Euclidean_algorithm
|
||||
/**
|
||||
* Greatest common divisor
|
||||
* Uses the Euclidean algorithm https://en.wikipedia.org/wiki/Euclidean_algorithm
|
||||
* @param {number} a
|
||||
* @param {number} b
|
||||
* @returns {number}
|
||||
*/
|
||||
function gcd(a, b) {
|
||||
while (b !== 0) {
|
||||
const t = b;
|
||||
@ -28,7 +129,12 @@ function gcd(a, b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
// Find all dividers that fit into two numbers
|
||||
/**
|
||||
* Find all dividers that fit into two numbers
|
||||
* @param {number} a
|
||||
* @param {number} b
|
||||
* @returns {number[]}
|
||||
*/
|
||||
function dividers(a, b) {
|
||||
const d = gcd(a, b);
|
||||
return factors(d);
|
||||
@ -42,12 +148,24 @@ const gridSizeStd = { x: 14.438842, y: 15.582376 };
|
||||
const minGridSize = 10;
|
||||
const maxGridSize = 200;
|
||||
|
||||
/**
|
||||
* Get whether the grid size is likely valid by checking whether it exceeds a bounds
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function gridSizeVaild(x, y) {
|
||||
return (
|
||||
x > minGridSize && y > minGridSize && x < maxGridSize && y < maxGridSize
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a grid size for an image by finding the closest size to the average grid size
|
||||
* @param {Image} image
|
||||
* @param {number[]} candidates
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
function gridSizeHeuristic(image, candidates) {
|
||||
const width = image.width;
|
||||
const height = image.height;
|
||||
@ -74,6 +192,12 @@ function gridSizeHeuristic(image, candidates) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the grid size of an image by running the image through a machine learning model
|
||||
* @param {Image} image
|
||||
* @param {number[]} candidates
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
async function gridSizeML(image, candidates) {
|
||||
const width = image.width;
|
||||
const height = image.height;
|
||||
@ -134,6 +258,11 @@ async function gridSizeML(image, candidates) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the grid size of an image by either using a ML model or falling back to a heuristic
|
||||
* @param {Image} image
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
export async function getGridSize(image) {
|
||||
const candidates = dividers(image.width, image.height);
|
||||
let prediction;
|
||||
@ -155,32 +284,45 @@ export async function getGridSize(image) {
|
||||
return prediction;
|
||||
}
|
||||
|
||||
export function getMapMaxZoom(map) {
|
||||
if (!map) {
|
||||
/**
|
||||
* Get the max zoom for a grid
|
||||
* @param {Grid} grid
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getGridMaxZoom(grid) {
|
||||
if (!grid) {
|
||||
return 10;
|
||||
}
|
||||
// Return max grid size / 2
|
||||
return Math.max(Math.max(map.grid.size.x, map.grid.size.y) / 2, 5);
|
||||
return Math.max(Math.max(grid.size.x, grid.size.y) / 2, 5);
|
||||
}
|
||||
|
||||
export function snapNodeToMap(
|
||||
map,
|
||||
/**
|
||||
* Snap a Konva Node to a the closest grid cell
|
||||
* @param {Grid} grid
|
||||
* @param {number} mapWidth
|
||||
* @param {number} mapHeight
|
||||
* @param {Konva.node} node
|
||||
* @param {number} snappingThreshold 1 = Always snap, 0 = never snap
|
||||
*/
|
||||
export function snapNodeToGrid(
|
||||
grid,
|
||||
mapWidth,
|
||||
mapHeight,
|
||||
node,
|
||||
snappingThreshold
|
||||
) {
|
||||
const offset = Vector2.multiply(map.grid.inset.topLeft, {
|
||||
const offset = Vector2.multiply(grid.inset.topLeft, {
|
||||
x: mapWidth,
|
||||
y: mapHeight,
|
||||
});
|
||||
const gridSize = {
|
||||
x:
|
||||
(mapWidth * (map.grid.inset.bottomRight.x - map.grid.inset.topLeft.x)) /
|
||||
map.grid.size.x,
|
||||
(mapWidth * (grid.inset.bottomRight.x - grid.inset.topLeft.x)) /
|
||||
grid.size.x,
|
||||
y:
|
||||
(mapHeight * (map.grid.inset.bottomRight.y - map.grid.inset.topLeft.y)) /
|
||||
map.grid.size.y,
|
||||
(mapHeight * (grid.inset.bottomRight.y - grid.inset.topLeft.y)) /
|
||||
grid.size.y,
|
||||
};
|
||||
|
||||
const position = node.position();
|
@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Line, Group, Path, Circle } from "react-konva";
|
||||
import Color from "color";
|
||||
import * as Vector2 from "./vector2";
|
||||
import Vector2 from "./Vector2";
|
||||
|
||||
// Holes should be wound in the opposite direction as the containing points array
|
||||
export function HoleyLine({ holes, ...props }) {
|
||||
|
@ -5,440 +5,465 @@ import {
|
||||
} from "./shared";
|
||||
|
||||
/**
|
||||
* Vector class with x and y
|
||||
* @typedef {Object} Vector2
|
||||
* @property {number} x - X component of the vector
|
||||
* @property {number} y - Y component of the vector
|
||||
* Vector class with x, y and static helper methods
|
||||
*/
|
||||
class Vector2 {
|
||||
/**
|
||||
* @type {number} x - X component of the vector
|
||||
*/
|
||||
x;
|
||||
/**
|
||||
* @type {number} y - Y component of the vector
|
||||
*/
|
||||
y;
|
||||
|
||||
/**
|
||||
* @param {Vector2} p
|
||||
* @returns {number} Length squared of `p`
|
||||
*/
|
||||
export function lengthSquared(p) {
|
||||
return p.x * p.x + p.y * p.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Vector2} p
|
||||
* @returns {number} Length of `p`
|
||||
*/
|
||||
export function length(p) {
|
||||
return Math.sqrt(lengthSquared(p));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Vector2} p
|
||||
* @returns {Vector2} `p` normalized, if length of `p` is 0 `{x: 0, y: 0}` is returned
|
||||
*/
|
||||
export function normalize(p) {
|
||||
const l = length(p);
|
||||
if (l === 0) {
|
||||
return { x: 0, y: 0 };
|
||||
/**
|
||||
* @param {Vector2} p
|
||||
* @returns {number} Length squared of `p`
|
||||
*/
|
||||
static lengthSquared(p) {
|
||||
return p.x * p.x + p.y * p.y;
|
||||
}
|
||||
return divide(p, l);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Vector2} a
|
||||
* @param {Vector2} b
|
||||
* @returns {number} Dot product between `a` and `b`
|
||||
*/
|
||||
export function dot(a, b) {
|
||||
return a.x * b.x + a.y * b.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Vector2} a
|
||||
* @param {(Vector2 | number)} b
|
||||
* @returns {Vector2} a - b
|
||||
*/
|
||||
export function subtract(a, b) {
|
||||
if (typeof b === "number") {
|
||||
return { x: a.x - b, y: a.y - b };
|
||||
} else {
|
||||
return { x: a.x - b.x, y: a.y - b.y };
|
||||
/**
|
||||
* @param {Vector2} p
|
||||
* @returns {number} Length of `p`
|
||||
*/
|
||||
static length(p) {
|
||||
return Math.sqrt(this.lengthSquared(p));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Vector2} a
|
||||
* @param {(Vector2 | number)} b
|
||||
* @returns {Vector2} a + b
|
||||
*/
|
||||
export function add(a, b) {
|
||||
if (typeof b === "number") {
|
||||
return { x: a.x + b, y: a.y + b };
|
||||
} else {
|
||||
return { x: a.x + b.x, y: a.y + b.y };
|
||||
/**
|
||||
* @param {Vector2} p
|
||||
* @returns {Vector2} `p` normalized, if length of `p` is 0 `{x: 0, y: 0}` is returned
|
||||
*/
|
||||
static normalize(p) {
|
||||
const l = this.length(p);
|
||||
if (l === 0) {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
return this.divide(p, l);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Vector2} a
|
||||
* @param {(Vector2 | number)} b
|
||||
* @returns {Vector2} a * b
|
||||
*/
|
||||
export function multiply(a, b) {
|
||||
if (typeof b === "number") {
|
||||
return { x: a.x * b, y: a.y * b };
|
||||
} else {
|
||||
return { x: a.x * b.x, y: a.y * b.y };
|
||||
/**
|
||||
* @param {Vector2} a
|
||||
* @param {Vector2} b
|
||||
* @returns {number} Dot product between `a` and `b`
|
||||
*/
|
||||
static dot(a, b) {
|
||||
return a.x * b.x + a.y * b.y;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Vector2} a
|
||||
* @param {(Vector2 | number)} b
|
||||
* @returns {Vector2} a / b
|
||||
*/
|
||||
export function divide(a, b) {
|
||||
if (typeof b === "number") {
|
||||
return { x: a.x / b, y: a.y / b };
|
||||
} else {
|
||||
return { x: a.x / b.x, y: a.y / b.y };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates a point around a given origin by an angle in degrees
|
||||
* @param {Vector2} point Point to rotate
|
||||
* @param {Vector2} origin Origin of the rotation
|
||||
* @param {number} angle Angle of rotation in degrees
|
||||
* @returns {Vector2} Rotated point
|
||||
*/
|
||||
export function rotate(point, origin, angle) {
|
||||
const cos = Math.cos(toRadians(angle));
|
||||
const sin = Math.sin(toRadians(angle));
|
||||
const dif = subtract(point, origin);
|
||||
return {
|
||||
x: origin.x + cos * dif.x - sin * dif.y,
|
||||
y: origin.y + sin * dif.x + cos * dif.y,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates a direction by a given angle in degrees
|
||||
* @param {Vector2} direction Direction to rotate
|
||||
* @param {number} angle Angle of rotation in degrees
|
||||
* @returns {Vector2} Rotated direction
|
||||
*/
|
||||
export function rotateDirection(direction, angle) {
|
||||
return rotate(direction, { x: 0, y: 0 }, angle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the min of `value` and `minimum`, if `minimum` is undefined component wise min is returned instead
|
||||
* @param {Vector2} a
|
||||
* @param {(Vector2 | number)} [minimum] Value to compare
|
||||
* @returns {(Vector2 | number)}
|
||||
*/
|
||||
export function min(a, minimum) {
|
||||
if (minimum === undefined) {
|
||||
return a.x < a.y ? a.x : a.y;
|
||||
} else if (typeof minimum === "number") {
|
||||
return { x: Math.min(a.x, minimum), y: Math.min(a.y, minimum) };
|
||||
} else {
|
||||
return { x: Math.min(a.x, minimum.x), y: Math.min(a.y, minimum.y) };
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns the max of `a` and `maximum`, if `maximum` is undefined component wise max is returned instead
|
||||
* @param {Vector2} a
|
||||
* @param {(Vector2 | number)} [maximum] Value to compare
|
||||
* @returns {(Vector2 | number)}
|
||||
*/
|
||||
export function max(a, maximum) {
|
||||
if (maximum === undefined) {
|
||||
return a.x > a.y ? a.x : a.y;
|
||||
} else if (typeof maximum === "number") {
|
||||
return { x: Math.max(a.x, maximum), y: Math.max(a.y, maximum) };
|
||||
} else {
|
||||
return { x: Math.max(a.x, maximum.x), y: Math.max(a.y, maximum.y) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds `p` to the nearest value of `to`
|
||||
* @param {Vector2} p
|
||||
* @param {Vector2} to
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
export function roundTo(p, to) {
|
||||
return {
|
||||
x: roundToNumber(p.x, to.x),
|
||||
y: roundToNumber(p.y, to.y),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Vector2} a
|
||||
* @returns {Vector2} The component wise sign of `a`
|
||||
*/
|
||||
export function sign(a) {
|
||||
return { x: Math.sign(a.x), y: Math.sign(a.y) };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Vector2} a
|
||||
* @returns {Vector2} The component wise absolute of `a`
|
||||
*/
|
||||
export function abs(a) {
|
||||
return { x: Math.abs(a.x), y: Math.abs(a.y) };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Vector2} a
|
||||
* @param {(Vector2 | number)} b
|
||||
* @returns {Vector2} `a` to the power of `b`
|
||||
*/
|
||||
export function pow(a, b) {
|
||||
if (typeof b === "number") {
|
||||
return { x: Math.pow(a.x, b), y: Math.pow(a.y, b) };
|
||||
} else {
|
||||
return { x: Math.pow(a.x, b.x), y: Math.pow(a.y, b.y) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Vector2} a
|
||||
* @returns {number} The dot product between `a` and `a`
|
||||
*/
|
||||
export function dot2(a) {
|
||||
return dot(a, a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamps `a` between `min` and `max`
|
||||
* @param {Vector2} a
|
||||
* @param {number} min
|
||||
* @param {number} max
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
export function clamp(a, min, max) {
|
||||
return {
|
||||
x: Math.min(Math.max(a.x, min), max),
|
||||
y: Math.min(Math.max(a.y, min), max),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the distance between a point and a line segment
|
||||
* See more at {@link https://www.iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm}
|
||||
* @param {Vector2} p Point
|
||||
* @param {Vector2} a Start of the line
|
||||
* @param {Vector2} b End of the line
|
||||
* @returns {Object} The distance to and the closest point on the line segment
|
||||
*/
|
||||
export function distanceToLine(p, a, b) {
|
||||
const pa = subtract(p, a);
|
||||
const ba = subtract(b, a);
|
||||
const h = Math.min(Math.max(dot(pa, ba) / dot(ba, ba), 0), 1);
|
||||
const distance = length(subtract(pa, multiply(ba, h)));
|
||||
const point = add(a, multiply(ba, h));
|
||||
return { distance, point };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the distance between a point and a quadratic bezier curve
|
||||
* See more at {@link https://www.shadertoy.com/view/MlKcDD}
|
||||
* @todo Fix the robustness of this to allow smoothing on fog layers
|
||||
* @param {Vector2} pos Position
|
||||
* @param {Vector2} A Start of the curve
|
||||
* @param {Vector2} B Control point of the curve
|
||||
* @param {Vector2} C End of the curve
|
||||
* @returns {Object} The distance to and the closest point on the curve
|
||||
*/
|
||||
export function distanceToQuadraticBezier(pos, A, B, C) {
|
||||
let distance = 0;
|
||||
let point = { x: pos.x, y: pos.y };
|
||||
|
||||
const a = subtract(B, A);
|
||||
const b = add(subtract(A, multiply(B, 2)), C);
|
||||
const c = multiply(a, 2);
|
||||
const d = subtract(A, pos);
|
||||
|
||||
// Solve cubic roots to find closest points
|
||||
const kk = 1 / dot(b, b);
|
||||
const kx = kk * dot(a, b);
|
||||
const ky = (kk * (2 * dot(a, a) + dot(d, b))) / 3;
|
||||
const kz = kk * dot(d, a);
|
||||
|
||||
const p = ky - kx * kx;
|
||||
const p3 = p * p * p;
|
||||
const q = kx * (2 * kx * kx - 3 * ky) + kz;
|
||||
let h = q * q + 4 * p3;
|
||||
|
||||
if (h >= 0) {
|
||||
// 1 root
|
||||
h = Math.sqrt(h);
|
||||
const x = divide(subtract({ x: h, y: -h }, q), 2);
|
||||
const uv = multiply(sign(x), pow(abs(x), 1 / 3));
|
||||
const t = Math.min(Math.max(uv.x + uv.y - kx, 0), 1);
|
||||
point = add(A, multiply(add(c, multiply(b, t)), t));
|
||||
distance = dot2(add(d, multiply(add(c, multiply(b, t)), t)));
|
||||
} else {
|
||||
// 3 roots but ignore the 3rd one as it will never be closest
|
||||
// https://www.shadertoy.com/view/MdXBzB
|
||||
const z = Math.sqrt(-p);
|
||||
const v = Math.acos(q / (p * z * 2)) / 3;
|
||||
const m = Math.cos(v);
|
||||
const n = Math.sin(v) * 1.732050808;
|
||||
|
||||
const t = clamp(subtract(multiply({ x: m + m, y: -n - m }, z), kx), 0, 1);
|
||||
const d1 = dot2(add(d, multiply(add(c, multiply(b, t.x)), t.x)));
|
||||
const d2 = dot2(add(d, multiply(add(c, multiply(b, t.y)), t.y)));
|
||||
distance = Math.min(d1, d2);
|
||||
if (d1 < d2) {
|
||||
point = add(d, multiply(add(c, multiply(b, t.x)), t.x));
|
||||
/**
|
||||
* @param {Vector2} a
|
||||
* @param {(Vector2 | number)} b
|
||||
* @returns {Vector2} a - b
|
||||
*/
|
||||
static subtract(a, b) {
|
||||
if (typeof b === "number") {
|
||||
return { x: a.x - b, y: a.y - b };
|
||||
} else {
|
||||
point = add(d, multiply(add(c, multiply(b, t.y)), t.y));
|
||||
return { x: a.x - b.x, y: a.y - b.y };
|
||||
}
|
||||
}
|
||||
|
||||
return { distance: Math.sqrt(distance), point: point };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates an axis-aligned bounding box around an array of point
|
||||
* @param {Vector2[]} points
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getBounds(points) {
|
||||
let minX = Number.MAX_VALUE;
|
||||
let maxX = Number.MIN_VALUE;
|
||||
let minY = Number.MAX_VALUE;
|
||||
let maxY = Number.MIN_VALUE;
|
||||
for (let point of points) {
|
||||
minX = point.x < minX ? point.x : minX;
|
||||
maxX = point.x > maxX ? point.x : maxX;
|
||||
minY = point.y < minY ? point.y : minY;
|
||||
maxY = point.y > maxY ? point.y : maxY;
|
||||
}
|
||||
return { minX, maxX, minY, maxY };
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if a point is in a polygon using ray casting
|
||||
* See more at {@link https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm}
|
||||
* and {@link https://stackoverflow.com/questions/217578/how-can-i-determine-whether-a-2d-point-is-within-a-polygon/2922778}
|
||||
* @param {Vector2} p
|
||||
* @param {Vector2[]} points
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function pointInPolygon(p, points) {
|
||||
const { minX, maxX, minY, maxY } = getBounds(points);
|
||||
if (p.x < minX || p.x > maxX || p.y < minY || p.y > maxY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let isInside = false;
|
||||
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
|
||||
const a = points[i].y > p.y;
|
||||
const b = points[j].y > p.y;
|
||||
if (
|
||||
a !== b &&
|
||||
p.x <
|
||||
((points[j].x - points[i].x) * (p.y - points[i].y)) /
|
||||
(points[j].y - points[i].y) +
|
||||
points[i].x
|
||||
) {
|
||||
isInside = !isInside;
|
||||
/**
|
||||
* @param {Vector2} a
|
||||
* @param {(Vector2 | number)} b
|
||||
* @returns {Vector2} a + b
|
||||
*/
|
||||
static add(a, b) {
|
||||
if (typeof b === "number") {
|
||||
return { x: a.x + b, y: a.y + b };
|
||||
} else {
|
||||
return { x: a.x + b.x, y: a.y + b.y };
|
||||
}
|
||||
}
|
||||
return isInside;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a the distance between `a` and `b` is under `threshold`
|
||||
* @param {Vector2} a
|
||||
* @param {Vector2} b
|
||||
* @param {number} threshold
|
||||
*/
|
||||
export function compare(a, b, threshold) {
|
||||
return lengthSquared(subtract(a, b)) < threshold * threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the distance between two vectors
|
||||
* @param {Vector2} a
|
||||
* @param {Vector2} b
|
||||
* @param {string} type - `chebyshev | euclidean | manhattan | alternating`
|
||||
*/
|
||||
export function distance(a, b, type) {
|
||||
switch (type) {
|
||||
case "chebyshev":
|
||||
return Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y));
|
||||
case "euclidean":
|
||||
return length(subtract(a, b));
|
||||
case "manhattan":
|
||||
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
||||
case "alternating":
|
||||
// Alternating diagonal distance like D&D 3.5 and Pathfinder
|
||||
const delta = abs(subtract(a, b));
|
||||
const ma = max(delta);
|
||||
const mi = min(delta);
|
||||
return ma - mi + Math.floor(1.5 * mi);
|
||||
default:
|
||||
return length(subtract(a, b));
|
||||
/**
|
||||
* @param {Vector2} a
|
||||
* @param {(Vector2 | number)} b
|
||||
* @returns {Vector2} a * b
|
||||
*/
|
||||
static multiply(a, b) {
|
||||
if (typeof b === "number") {
|
||||
return { x: a.x * b, y: a.y * b };
|
||||
} else {
|
||||
return { x: a.x * b.x, y: a.y * b.y };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Linear interpolate between `a` and `b` by `alpha`
|
||||
* @param {Vector2} a
|
||||
* @param {Vector2} b
|
||||
* @param {number} alpha
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
export function lerp(a, b, alpha) {
|
||||
return { x: lerpNumber(a.x, b.x, alpha), y: lerpNumber(a.y, b.y, alpha) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns total length of a an array of points treated as a path
|
||||
* @param {Vector2[]} points the array of points in the path
|
||||
*/
|
||||
export function pathLength(points) {
|
||||
let l = 0;
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
l += distance(points[i - 1], points[i], "euclidean");
|
||||
/**
|
||||
* @param {Vector2} a
|
||||
* @param {(Vector2 | number)} b
|
||||
* @returns {Vector2} a / b
|
||||
*/
|
||||
static divide(a, b) {
|
||||
if (typeof b === "number") {
|
||||
return { x: a.x / b, y: a.y / b };
|
||||
} else {
|
||||
return { x: a.x / b.x, y: a.y / b.y };
|
||||
}
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resample a path to n number of evenly distributed points
|
||||
* based off of http://depts.washington.edu/acelab/proj/dollar/index.html
|
||||
* @param {Vector2[]} points the points to resample
|
||||
* @param {number} n the number of new points
|
||||
*/
|
||||
export function resample(points, n) {
|
||||
if (points.length === 0 || n <= 0) {
|
||||
return [];
|
||||
/**
|
||||
* Rotates a point around a given origin by an angle in degrees
|
||||
* @param {Vector2} point Point to rotate
|
||||
* @param {Vector2} origin Origin of the rotation
|
||||
* @param {number} angle Angle of rotation in degrees
|
||||
* @returns {Vector2} Rotated point
|
||||
*/
|
||||
static rotate(point, origin, angle) {
|
||||
const cos = Math.cos(toRadians(angle));
|
||||
const sin = Math.sin(toRadians(angle));
|
||||
const dif = this.subtract(point, origin);
|
||||
return {
|
||||
x: origin.x + cos * dif.x - sin * dif.y,
|
||||
y: origin.y + sin * dif.x + cos * dif.y,
|
||||
};
|
||||
}
|
||||
let localPoints = [...points];
|
||||
const intervalLength = pathLength(localPoints) / (n - 1);
|
||||
let resampledPoints = [localPoints[0]];
|
||||
let currentDistance = 0;
|
||||
for (let i = 1; i < localPoints.length; i++) {
|
||||
let d = distance(localPoints[i - 1], localPoints[i], "euclidean");
|
||||
if (currentDistance + d >= intervalLength) {
|
||||
let newPoint = lerp(
|
||||
localPoints[i - 1],
|
||||
localPoints[i],
|
||||
(intervalLength - currentDistance) / d
|
||||
|
||||
/**
|
||||
* Rotates a direction by a given angle in degrees
|
||||
* @param {Vector2} direction Direction to rotate
|
||||
* @param {number} angle Angle of rotation in degrees
|
||||
* @returns {Vector2} Rotated direction
|
||||
*/
|
||||
static rotateDirection(direction, angle) {
|
||||
return this.rotate(direction, { x: 0, y: 0 }, angle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the min of `value` and `minimum`, if `minimum` is undefined component wise min is returned instead
|
||||
* @param {Vector2} a
|
||||
* @param {(Vector2 | number)} [minimum] Value to compare
|
||||
* @returns {(Vector2 | number)}
|
||||
*/
|
||||
static min(a, minimum) {
|
||||
if (minimum === undefined) {
|
||||
return a.x < a.y ? a.x : a.y;
|
||||
} else if (typeof minimum === "number") {
|
||||
return { x: Math.min(a.x, minimum), y: Math.min(a.y, minimum) };
|
||||
} else {
|
||||
return { x: Math.min(a.x, minimum.x), y: Math.min(a.y, minimum.y) };
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns the max of `a` and `maximum`, if `maximum` is undefined component wise max is returned instead
|
||||
* @param {Vector2} a
|
||||
* @param {(Vector2 | number)} [maximum] Value to compare
|
||||
* @returns {(Vector2 | number)}
|
||||
*/
|
||||
static max(a, maximum) {
|
||||
if (maximum === undefined) {
|
||||
return a.x > a.y ? a.x : a.y;
|
||||
} else if (typeof maximum === "number") {
|
||||
return { x: Math.max(a.x, maximum), y: Math.max(a.y, maximum) };
|
||||
} else {
|
||||
return { x: Math.max(a.x, maximum.x), y: Math.max(a.y, maximum.y) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds `p` to the nearest value of `to`
|
||||
* @param {Vector2} p
|
||||
* @param {Vector2} to
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
static roundTo(p, to) {
|
||||
return {
|
||||
x: roundToNumber(p.x, to.x),
|
||||
y: roundToNumber(p.y, to.y),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Vector2} a
|
||||
* @returns {Vector2} The component wise sign of `a`
|
||||
*/
|
||||
static sign(a) {
|
||||
return { x: Math.sign(a.x), y: Math.sign(a.y) };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Vector2} a
|
||||
* @returns {Vector2} The component wise absolute of `a`
|
||||
*/
|
||||
static abs(a) {
|
||||
return { x: Math.abs(a.x), y: Math.abs(a.y) };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Vector2} a
|
||||
* @param {(Vector2 | number)} b
|
||||
* @returns {Vector2} `a` to the power of `b`
|
||||
*/
|
||||
static pow(a, b) {
|
||||
if (typeof b === "number") {
|
||||
return { x: Math.pow(a.x, b), y: Math.pow(a.y, b) };
|
||||
} else {
|
||||
return { x: Math.pow(a.x, b.x), y: Math.pow(a.y, b.y) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Vector2} a
|
||||
* @returns {number} The dot product between `a` and `a`
|
||||
*/
|
||||
static dot2(a) {
|
||||
return this.dot(a, a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamps `a` between `min` and `max`
|
||||
* @param {Vector2} a
|
||||
* @param {number} min
|
||||
* @param {number} max
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
static clamp(a, min, max) {
|
||||
return {
|
||||
x: Math.min(Math.max(a.x, min), max),
|
||||
y: Math.min(Math.max(a.y, min), max),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the distance between a point and a line segment
|
||||
* See more at {@link https://www.iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm}
|
||||
* @param {Vector2} p Point
|
||||
* @param {Vector2} a Start of the line
|
||||
* @param {Vector2} b End of the line
|
||||
* @returns {Object} The distance to and the closest point on the line segment
|
||||
*/
|
||||
static distanceToLine(p, a, b) {
|
||||
const pa = this.subtract(p, a);
|
||||
const ba = this.subtract(b, a);
|
||||
const h = Math.min(Math.max(this.dot(pa, ba) / this.dot(ba, ba), 0), 1);
|
||||
const distance = this.length(this.subtract(pa, this.multiply(ba, h)));
|
||||
const point = this.add(a, this.multiply(ba, h));
|
||||
return { distance, point };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the distance between a point and a quadratic bezier curve
|
||||
* See more at {@link https://www.shadertoy.com/view/MlKcDD}
|
||||
* @todo Fix the robustness of this to allow smoothing on fog layers
|
||||
* @param {Vector2} pos Position
|
||||
* @param {Vector2} A Start of the curve
|
||||
* @param {Vector2} B Control point of the curve
|
||||
* @param {Vector2} C End of the curve
|
||||
* @returns {Object} The distance to and the closest point on the curve
|
||||
*/
|
||||
static distanceToQuadraticBezier(pos, A, B, C) {
|
||||
let distance = 0;
|
||||
let point = { x: pos.x, y: pos.y };
|
||||
|
||||
const a = this.subtract(B, A);
|
||||
const b = this.add(this.subtract(A, this.multiply(B, 2)), C);
|
||||
const c = this.multiply(a, 2);
|
||||
const d = this.subtract(A, pos);
|
||||
|
||||
// Solve cubic roots to find closest points
|
||||
const kk = 1 / this.dot(b, b);
|
||||
const kx = kk * this.dot(a, b);
|
||||
const ky = (kk * (2 * this.dot(a, a) + this.dot(d, b))) / 3;
|
||||
const kz = kk * this.dot(d, a);
|
||||
|
||||
const p = ky - kx * kx;
|
||||
const p3 = p * p * p;
|
||||
const q = kx * (2 * kx * kx - 3 * ky) + kz;
|
||||
let h = q * q + 4 * p3;
|
||||
|
||||
if (h >= 0) {
|
||||
// 1 root
|
||||
h = Math.sqrt(h);
|
||||
const x = this.divide(this.subtract({ x: h, y: -h }, q), 2);
|
||||
const uv = this.multiply(this.sign(x), this.pow(this.abs(x), 1 / 3));
|
||||
const t = Math.min(Math.max(uv.x + uv.y - kx, 0), 1);
|
||||
point = this.add(A, this.multiply(this.add(c, this.multiply(b, t)), t));
|
||||
distance = this.dot2(
|
||||
this.add(d, this.multiply(this.add(c, this.multiply(b, t)), t))
|
||||
);
|
||||
resampledPoints.push(newPoint);
|
||||
localPoints.splice(i, 0, newPoint);
|
||||
currentDistance = 0;
|
||||
} else {
|
||||
currentDistance += d;
|
||||
// 3 roots but ignore the 3rd one as it will never be closest
|
||||
// https://www.shadertoy.com/view/MdXBzB
|
||||
const z = Math.sqrt(-p);
|
||||
const v = Math.acos(q / (p * z * 2)) / 3;
|
||||
const m = Math.cos(v);
|
||||
const n = Math.sin(v) * 1.732050808;
|
||||
|
||||
const t = this.clamp(
|
||||
this.subtract(this.multiply({ x: m + m, y: -n - m }, z), kx),
|
||||
0,
|
||||
1
|
||||
);
|
||||
const d1 = this.dot2(
|
||||
this.add(d, this.multiply(this.add(c, this.multiply(b, t.x)), t.x))
|
||||
);
|
||||
const d2 = this.dot2(
|
||||
this.add(d, this.multiply(this.add(c, this.multiply(b, t.y)), t.y))
|
||||
);
|
||||
distance = Math.min(d1, d2);
|
||||
if (d1 < d2) {
|
||||
point = this.add(
|
||||
d,
|
||||
this.multiply(this.add(c, this.multiply(b, t.x)), t.x)
|
||||
);
|
||||
} else {
|
||||
point = this.add(
|
||||
d,
|
||||
this.multiply(this.add(c, this.multiply(b, t.y)), t.y)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resampledPoints.length === n - 1) {
|
||||
resampledPoints.push(localPoints[localPoints.length - 1]);
|
||||
|
||||
return { distance: Math.sqrt(distance), point: point };
|
||||
}
|
||||
|
||||
return resampledPoints;
|
||||
/**
|
||||
* Calculates an axis-aligned bounding box around an array of point
|
||||
* @param {Vector2[]} points
|
||||
* @returns {Object}
|
||||
*/
|
||||
static getBounds(points) {
|
||||
let minX = Number.MAX_VALUE;
|
||||
let maxX = Number.MIN_VALUE;
|
||||
let minY = Number.MAX_VALUE;
|
||||
let maxY = Number.MIN_VALUE;
|
||||
for (let point of points) {
|
||||
minX = point.x < minX ? point.x : minX;
|
||||
maxX = point.x > maxX ? point.x : maxX;
|
||||
minY = point.y < minY ? point.y : minY;
|
||||
maxY = point.y > maxY ? point.y : maxY;
|
||||
}
|
||||
return { minX, maxX, minY, maxY };
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if a point is in a polygon using ray casting
|
||||
* See more at {@link https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm}
|
||||
* and {@link https://stackoverflow.com/questions/217578/how-can-i-determine-whether-a-2d-point-is-within-a-polygon/2922778}
|
||||
* @param {Vector2} p
|
||||
* @param {Vector2[]} points
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static pointInPolygon(p, points) {
|
||||
const { minX, maxX, minY, maxY } = this.getBounds(points);
|
||||
if (p.x < minX || p.x > maxX || p.y < minY || p.y > maxY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let isInside = false;
|
||||
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
|
||||
const a = points[i].y > p.y;
|
||||
const b = points[j].y > p.y;
|
||||
if (
|
||||
a !== b &&
|
||||
p.x <
|
||||
((points[j].x - points[i].x) * (p.y - points[i].y)) /
|
||||
(points[j].y - points[i].y) +
|
||||
points[i].x
|
||||
) {
|
||||
isInside = !isInside;
|
||||
}
|
||||
}
|
||||
return isInside;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a the distance between `a` and `b` is under `threshold`
|
||||
* @param {Vector2} a
|
||||
* @param {Vector2} b
|
||||
* @param {number} threshold
|
||||
*/
|
||||
static compare(a, b, threshold) {
|
||||
return this.lengthSquared(this.subtract(a, b)) < threshold * threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the distance between two vectors
|
||||
* @param {Vector2} a
|
||||
* @param {Vector2} b
|
||||
* @param {string?} type - `chebyshev | euclidean | manhattan | alternating`
|
||||
*/
|
||||
static distance(a, b, type = "euclidean") {
|
||||
switch (type) {
|
||||
case "chebyshev":
|
||||
return Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y));
|
||||
case "euclidean":
|
||||
return this.length(this.subtract(a, b));
|
||||
case "manhattan":
|
||||
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
||||
case "alternating":
|
||||
// Alternating diagonal distance like D&D 3.5 and Pathfinder
|
||||
const delta = this.abs(this.subtract(a, b));
|
||||
const ma = this.max(delta);
|
||||
const mi = this.min(delta);
|
||||
return ma - mi + Math.floor(1.5 * mi);
|
||||
default:
|
||||
return this.length(this.subtract(a, b));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Linear interpolate between `a` and `b` by `alpha`
|
||||
* @param {Vector2} a
|
||||
* @param {Vector2} b
|
||||
* @param {number} alpha
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
static lerp(a, b, alpha) {
|
||||
return { x: lerpNumber(a.x, b.x, alpha), y: lerpNumber(a.y, b.y, alpha) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns total length of a an array of points treated as a path
|
||||
* @param {Vector2[]} points the array of points in the path
|
||||
*/
|
||||
static pathLength(points) {
|
||||
let l = 0;
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
l += this.distance(points[i - 1], points[i]);
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resample a path to n number of evenly distributed points
|
||||
* based off of http://depts.washington.edu/acelab/proj/dollar/index.html
|
||||
* @param {Vector2[]} points the points to resample
|
||||
* @param {number} n the number of new points
|
||||
*/
|
||||
static resample(points, n) {
|
||||
if (points.length === 0 || n <= 0) {
|
||||
return [];
|
||||
}
|
||||
let localPoints = [...points];
|
||||
const intervalLength = this.pathLength(localPoints) / (n - 1);
|
||||
let resampledPoints = [localPoints[0]];
|
||||
let currentDistance = 0;
|
||||
for (let i = 1; i < localPoints.length; i++) {
|
||||
let d = this.distance(localPoints[i - 1], localPoints[i]);
|
||||
if (currentDistance + d >= intervalLength) {
|
||||
let newPoint = this.lerp(
|
||||
localPoints[i - 1],
|
||||
localPoints[i],
|
||||
(intervalLength - currentDistance) / d
|
||||
);
|
||||
resampledPoints.push(newPoint);
|
||||
localPoints.splice(i, 0, newPoint);
|
||||
currentDistance = 0;
|
||||
} else {
|
||||
currentDistance += d;
|
||||
}
|
||||
}
|
||||
if (resampledPoints.length === n - 1) {
|
||||
resampledPoints.push(localPoints[localPoints.length - 1]);
|
||||
}
|
||||
|
||||
return resampledPoints;
|
||||
}
|
||||
}
|
||||
|
||||
export default Vector2;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
|
||||
import useDebounce from "./useDebounce";
|
||||
import { diff, applyChanges } from "./diff";
|
||||
import { diff, applyChanges } from "../helpers/diff";
|
||||
|
||||
/**
|
||||
* @callback setNetworkedState
|
@ -8,8 +8,9 @@ import MapEditor from "../components/map/MapEditor";
|
||||
import MapDataContext from "../contexts/MapDataContext";
|
||||
|
||||
import { isEmpty } from "../helpers/shared";
|
||||
import { getMapDefaultInset } from "../helpers/map";
|
||||
import useResponsiveLayout from "../helpers/useResponsiveLayout";
|
||||
import { getMapDefaultInset } from "../helpers/grid";
|
||||
|
||||
import useResponsiveLayout from "../hooks/useResponsiveLayout";
|
||||
|
||||
function EditMapModal({ isOpen, onDone, map, mapState }) {
|
||||
const { updateMap, updateMapState } = useContext(MapDataContext);
|
||||
|
@ -8,7 +8,8 @@ import TokenPreview from "../components/token/TokenPreview";
|
||||
import TokenDataContext from "../contexts/TokenDataContext";
|
||||
|
||||
import { isEmpty } from "../helpers/shared";
|
||||
import useResponsiveLayout from "../helpers/useResponsiveLayout";
|
||||
|
||||
import useResponsiveLayout from "../hooks/useResponsiveLayout";
|
||||
|
||||
function EditTokenModal({ isOpen, onDone, token }) {
|
||||
const { updateToken } = useContext(TokenDataContext);
|
||||
|
@ -13,18 +13,22 @@ import ImageDrop from "../components/ImageDrop";
|
||||
import LoadingOverlay from "../components/LoadingOverlay";
|
||||
|
||||
import blobToBuffer from "../helpers/blobToBuffer";
|
||||
import useKeyboard from "../helpers/useKeyboard";
|
||||
import { resizeImage } from "../helpers/image";
|
||||
import { useSearch, useGroup, handleItemSelect } from "../helpers/select";
|
||||
import { getMapDefaultInset, getGridSize, gridSizeVaild } from "../helpers/map";
|
||||
import useResponsiveLayout from "../helpers/useResponsiveLayout";
|
||||
import * as Vector2 from "../helpers/vector2";
|
||||
import {
|
||||
getMapDefaultInset,
|
||||
getGridSize,
|
||||
gridSizeVaild,
|
||||
} from "../helpers/grid";
|
||||
import Vector2 from "../helpers/Vector2";
|
||||
|
||||
import useKeyboard from "../hooks/useKeyboard";
|
||||
import useResponsiveLayout from "../hooks/useResponsiveLayout";
|
||||
|
||||
import MapDataContext from "../contexts/MapDataContext";
|
||||
import AuthContext from "../contexts/AuthContext";
|
||||
|
||||
const defaultMapProps = {
|
||||
// Grid type
|
||||
showGrid: false,
|
||||
snapToGrid: true,
|
||||
quality: "original",
|
||||
|
@ -13,9 +13,10 @@ import TokenTiles from "../components/token/TokenTiles";
|
||||
import LoadingOverlay from "../components/LoadingOverlay";
|
||||
|
||||
import blobToBuffer from "../helpers/blobToBuffer";
|
||||
import useKeyboard from "../helpers/useKeyboard";
|
||||
import { useSearch, useGroup, handleItemSelect } from "../helpers/select";
|
||||
import useResponsiveLayout from "../helpers/useResponsiveLayout";
|
||||
|
||||
import useKeyboard from "../hooks/useKeyboard";
|
||||
import useResponsiveLayout from "../hooks/useResponsiveLayout";
|
||||
|
||||
import TokenDataContext from "../contexts/TokenDataContext";
|
||||
import AuthContext from "../contexts/AuthContext";
|
||||
|
@ -16,7 +16,7 @@ import Slider from "../components/Slider";
|
||||
import AuthContext from "../contexts/AuthContext";
|
||||
import DatabaseContext from "../contexts/DatabaseContext";
|
||||
|
||||
import useSetting from "../helpers/useSetting";
|
||||
import useSetting from "../hooks/useSetting";
|
||||
|
||||
import ConfirmModal from "./ConfirmModal";
|
||||
import ImportExportModal from "./ImportExportModal";
|
||||
|
@ -5,7 +5,7 @@ import shortid from "shortid";
|
||||
|
||||
import AuthContext from "../contexts/AuthContext";
|
||||
|
||||
import useSetting from "../helpers/useSetting";
|
||||
import useSetting from "../hooks/useSetting";
|
||||
|
||||
import Modal from "../components/Modal";
|
||||
|
||||
|
@ -5,7 +5,7 @@ import Modal from "../components/Modal";
|
||||
|
||||
import { getHMSDuration, getDurationHMS } from "../helpers/timer";
|
||||
|
||||
import useSetting from "../helpers/useSetting";
|
||||
import useSetting from "../hooks/useSetting";
|
||||
|
||||
function StartTimerModal({
|
||||
isOpen,
|
||||
|
@ -8,8 +8,10 @@ import DatabaseContext from "../contexts/DatabaseContext";
|
||||
import PartyContext from "../contexts/PartyContext";
|
||||
|
||||
import { omit } from "../helpers/shared";
|
||||
import useDebounce from "../helpers/useDebounce";
|
||||
import useNetworkedState from "../helpers/useNetworkedState";
|
||||
|
||||
import useDebounce from "../hooks/useDebounce";
|
||||
import useNetworkedState from "../hooks/useNetworkedState";
|
||||
|
||||
// Load session for auto complete
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Session from "./Session";
|
||||
|
@ -5,8 +5,9 @@ import AuthContext from "../contexts/AuthContext";
|
||||
|
||||
import MapPointer from "../components/map/MapPointer";
|
||||
import { isEmpty } from "../helpers/shared";
|
||||
import { lerp, compare } from "../helpers/vector2";
|
||||
import useSetting from "../helpers/useSetting";
|
||||
import Vector2 from "../helpers/Vector2";
|
||||
|
||||
import useSetting from "../hooks/useSetting";
|
||||
|
||||
// Send pointer updates every 50ms (20fps)
|
||||
const sendTickRate = 50;
|
||||
@ -108,7 +109,11 @@ function NetworkedMapPointer({ session, active, gridSize }) {
|
||||
to: { ...pointer, time: performance.now() + sendTickRate },
|
||||
};
|
||||
} else if (
|
||||
!compare(interpolations[id].to.position, pointer.position, 0.0001) ||
|
||||
!Vector2.compare(
|
||||
interpolations[id].to.position,
|
||||
pointer.position,
|
||||
0.0001
|
||||
) ||
|
||||
interpolations[id].to.visible !== pointer.visible
|
||||
) {
|
||||
const from = interpolations[id].to;
|
||||
@ -153,7 +158,11 @@ function NetworkedMapPointer({ session, active, gridSize }) {
|
||||
interpolatedPointerState[interp.id] = {
|
||||
id: interp.id,
|
||||
visible: interp.from.visible,
|
||||
position: lerp(interp.from.position, interp.to.position, alpha),
|
||||
position: Vector2.lerp(
|
||||
interp.from.position,
|
||||
interp.to.position,
|
||||
alpha
|
||||
),
|
||||
color: interp.from.color,
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user