diff --git a/package.json b/package.json
index 02725be..353b2ff 100644
--- a/package.json
+++ b/package.json
@@ -1,21 +1,21 @@
{
"name": "owlbear-rodeo",
- "version": "1.1.0",
+ "version": "1.2.0",
"private": true,
"dependencies": {
"@stripe/stripe-js": "^1.3.2",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
- "blob-to-buffer": "^1.2.8",
+ "dexie": "^2.0.4",
"interactjs": "^1.9.7",
"js-binarypack": "^0.0.9",
+ "normalize-wheel": "^1.0.1",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"react-modal": "^3.11.2",
"react-router-dom": "^5.1.2",
"react-scripts": "3.4.0",
- "shape-detector": "^0.2.1",
"shortid": "^2.2.15",
"simple-peer": "^9.6.2",
"simplebar-react": "^2.1.0",
diff --git a/src/components/AddMapButton.js b/src/components/AddMapButton.js
deleted file mode 100644
index 16baa75..0000000
--- a/src/components/AddMapButton.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import React, { useRef, useState, useEffect } from "react";
-import { IconButton } from "theme-ui";
-
-import AddMapModal from "../modals/AddMapModal";
-import AddMapIcon from "../icons/AddMapIcon";
-
-const defaultMapSize = 22;
-
-function AddMapButton({ onMapChange }) {
- const [isAddModalOpen, setIsAddModalOpen] = useState(false);
- function openModal() {
- setIsAddModalOpen(true);
- }
- function closeModal() {
- setIsAddModalOpen(false);
- }
-
- const [imageLoaded, setImageLoaded] = useState(false);
-
- const mapDataRef = useRef(null);
- const [mapSource, setMapSource] = useState(null);
- function handleImageUpload(file, fileGridX, fileGridY) {
- const url = URL.createObjectURL(file);
- let image = new Image();
- image.onload = function () {
- mapDataRef.current = {
- file,
- gridX: fileGridX || gridX,
- gridY: fileGridY || gridY,
- width: image.width,
- height: image.height,
- };
- setImageLoaded(true);
- };
- image.src = url;
- setMapSource(url);
- if (fileGridX) {
- setGridX(fileGridX);
- }
- if (fileGridY) {
- setGridY(fileGridY);
- }
- }
-
- function handleDone() {
- if (mapDataRef.current && mapSource) {
- onMapChange(mapDataRef.current, mapSource);
- }
- closeModal();
- }
-
- const [gridX, setGridX] = useState(defaultMapSize);
- const [gridY, setGridY] = useState(defaultMapSize);
- useEffect(() => {
- if (mapDataRef.current) {
- mapDataRef.current.gridX = gridX;
- mapDataRef.current.gridY = gridY;
- }
- }, [gridX, gridY]);
-
- return (
- <>
-
-
-
-
- >
- );
-}
-
-export default AddMapButton;
diff --git a/src/components/ListToken.js b/src/components/ListToken.js
deleted file mode 100644
index f90d3d0..0000000
--- a/src/components/ListToken.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import React, { useRef } from "react";
-import { Image } from "theme-ui";
-
-import usePreventTouch from "../helpers/usePreventTouch";
-
-function ListToken({ image, className }) {
- const imageRef = useRef();
- // Stop touch to prevent 3d touch gesutre on iOS
- usePreventTouch(imageRef);
-
- return (
-
- );
-}
-
-export default ListToken;
diff --git a/src/components/Map.js b/src/components/Map.js
deleted file mode 100644
index bb4b961..0000000
--- a/src/components/Map.js
+++ /dev/null
@@ -1,328 +0,0 @@
-import React, { useRef, useEffect, useState } from "react";
-import { Box, Image } from "theme-ui";
-import interact from "interactjs";
-
-import ProxyToken from "./ProxyToken";
-import TokenMenu from "./TokenMenu";
-import MapToken from "./MapToken";
-import MapDrawing from "./MapDrawing";
-import MapControls from "./MapControls";
-
-import { omit } from "../helpers/shared";
-
-const mapTokenProxyClassName = "map-token__proxy";
-const mapTokenMenuClassName = "map-token__menu";
-const zoomSpeed = -0.005;
-const minZoom = 0.1;
-const maxZoom = 5;
-
-function Map({
- mapSource,
- mapData,
- tokens,
- onMapTokenChange,
- onMapTokenRemove,
- onMapChange,
- onMapDraw,
- onMapDrawUndo,
- onMapDrawRedo,
- drawActions,
- drawActionIndex,
-}) {
- function handleProxyDragEnd(isOnMap, token) {
- if (isOnMap && onMapTokenChange) {
- onMapTokenChange(token);
- }
-
- if (!isOnMap && onMapTokenRemove) {
- onMapTokenRemove(token);
- }
- }
-
- /**
- * Map drawing
- */
-
- const [selectedTool, setSelectedTool] = useState("pan");
- const [brushColor, setBrushColor] = useState("black");
- const [useBrushGridSnapping, setUseBrushGridSnapping] = useState(false);
- const [useBrushBlending, setUseBrushBlending] = useState(false);
- const [useBrushGesture, setUseBrushGesture] = useState(false);
-
- const [drawnShapes, setDrawnShapes] = useState([]);
- function handleShapeAdd(shape) {
- onMapDraw({ type: "add", shapes: [shape] });
- }
-
- function handleShapeRemove(shapeId) {
- onMapDraw({ type: "remove", shapeIds: [shapeId] });
- }
-
- function handleShapeRemoveAll() {
- onMapDraw({ type: "remove", shapeIds: drawnShapes.map((s) => s.id) });
- }
-
- // Replay the draw actions and convert them to shapes for the map drawing
- useEffect(() => {
- let shapesById = {};
- for (let i = 0; i <= drawActionIndex; i++) {
- const action = drawActions[i];
- if (action.type === "add") {
- for (let shape of action.shapes) {
- shapesById[shape.id] = shape;
- }
- }
- if (action.type === "remove") {
- shapesById = omit(shapesById, action.shapeIds);
- }
- }
- setDrawnShapes(Object.values(shapesById));
- }, [drawActions, drawActionIndex]);
-
- const disabledTools = [];
- if (!mapData) {
- disabledTools.push("pan");
- disabledTools.push("brush");
- }
- if (drawnShapes.length === 0) {
- disabledTools.push("erase");
- }
-
- /**
- * Map movement
- */
-
- const mapTranslateRef = useRef({ x: 0, y: 0 });
- const mapScaleRef = useRef(1);
- const mapMoveContainerRef = useRef();
- function setTranslateAndScale(newTranslate, newScale) {
- const moveContainer = mapMoveContainerRef.current;
- moveContainer.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px) scale(${newScale})`;
- mapScaleRef.current = newScale;
- mapTranslateRef.current = newTranslate;
- }
-
- useEffect(() => {
- function handleMove(event, isGesture) {
- const scale = mapScaleRef.current;
- const translate = mapTranslateRef.current;
-
- let newScale = scale;
- let newTranslate = translate;
-
- if (isGesture) {
- newScale = Math.max(Math.min(scale + event.ds, maxZoom), minZoom);
- }
-
- if (selectedTool === "pan" || isGesture) {
- newTranslate = {
- x: translate.x + event.dx,
- y: translate.y + event.dy,
- };
- }
- setTranslateAndScale(newTranslate, newScale);
- }
- const mapInteract = interact(".map")
- .gesturable({
- listeners: {
- move: (e) => handleMove(e, true),
- },
- })
- .draggable({
- inertia: true,
- listeners: {
- move: (e) => handleMove(e, false),
- },
- cursorChecker: () => {
- return selectedTool === "pan" && mapData ? "move" : "default";
- },
- })
- .on("doubletap", (event) => {
- event.preventDefault();
- if (selectedTool === "pan") {
- setTranslateAndScale({ x: 0, y: 0 }, 1);
- }
- });
-
- return () => {
- mapInteract.unset();
- };
- }, [selectedTool, mapData]);
-
- // Reset map transform when map changes
- useEffect(() => {
- setTranslateAndScale({ x: 0, y: 0 }, 1);
- }, [mapSource]);
-
- // Bind the wheel event of the map via a ref
- // in order to support non-passive event listening
- // to allow the track pad zoom to be interrupted
- // see https://github.com/facebook/react/issues/14856
- useEffect(() => {
- const mapContainer = mapContainerRef.current;
-
- function handleZoom(event) {
- // Stop overscroll on chrome and safari
- // also stop pinch to zoom on chrome
- event.preventDefault();
-
- const scale = mapScaleRef.current;
- const translate = mapTranslateRef.current;
-
- const deltaY = event.deltaY * zoomSpeed;
- const newScale = Math.max(Math.min(scale + deltaY, maxZoom), minZoom);
-
- setTranslateAndScale(translate, newScale);
- }
-
- if (mapContainer) {
- mapContainer.addEventListener("wheel", handleZoom, {
- passive: false,
- });
- }
-
- return () => {
- if (mapContainer) {
- mapContainer.removeEventListener("wheel", handleZoom);
- }
- };
- }, []);
-
- /**
- * Member setup
- */
-
- const mapRef = useRef(null);
- const mapContainerRef = useRef();
- const gridX = mapData && mapData.gridX;
- const gridY = mapData && mapData.gridY;
- const gridSizeNormalized = { x: 1 / gridX || 0, y: 1 / gridY || 0 };
- const tokenSizePercent = gridSizeNormalized.x * 100;
- const aspectRatio = (mapData && mapData.width / mapData.height) || 1;
-
- const mapImage = (
-
-
-
- );
-
- const mapTokens = (
-
- {Object.values(tokens).map((token) => (
-
- ))}
-
- );
-
- return (
- <>
-
-
-
-
- {mapImage}
-
- {mapTokens}
-
-
-
-
-
-
- >
- );
-}
-
-export default Map;
diff --git a/src/components/MapControls.js b/src/components/MapControls.js
deleted file mode 100644
index 2e9b33f..0000000
--- a/src/components/MapControls.js
+++ /dev/null
@@ -1,298 +0,0 @@
-import React, { useState, useEffect, useRef } from "react";
-import { Flex, Box, IconButton, Label } from "theme-ui";
-
-import AddMapButton from "./AddMapButton";
-import ExpandMoreIcon from "../icons/ExpandMoreIcon";
-import PanToolIcon from "../icons/PanToolIcon";
-import BrushToolIcon from "../icons/BrushToolIcon";
-import EraseToolIcon from "../icons/EraseToolIcon";
-import UndoIcon from "../icons/UndoIcon";
-import RedoIcon from "../icons/RedoIcon";
-import GridOnIcon from "../icons/GridOnIcon";
-import GridOffIcon from "../icons/GridOffIcon";
-import BlendOnIcon from "../icons/BlendOnIcon";
-import BlendOffIcon from "../icons/BlendOffIcon";
-import GestureOnIcon from "../icons/GestureOnIcon";
-import GestureOffIcon from "../icons/GestureOffIcon";
-
-import colors, { colorOptions } from "../helpers/colors";
-
-import MapMenu from "./MapMenu";
-import EraseAllIcon from "../icons/EraseAllIcon";
-
-function MapControls({
- onMapChange,
- onToolChange,
- selectedTool,
- disabledTools,
- onUndo,
- onRedo,
- undoDisabled,
- redoDisabled,
- brushColor,
- onBrushColorChange,
- onEraseAll,
- useBrushGridSnapping,
- onBrushGridSnappingChange,
- useBrushBlending,
- onBrushBlendingChange,
- useBrushGesture,
- onBrushGestureChange,
-}) {
- const [isExpanded, setIsExpanded] = useState(false);
-
- const subMenus = {
- brush: (
-
-
- {colorOptions.map((color) => (
- onBrushColorChange(color)}
- aria-label={`Brush Color ${color}`}
- >
- {brushColor === color && (
-
- )}
-
- ))}
-
-
- onBrushGridSnappingChange(!useBrushGridSnapping)}
- >
- {useBrushGridSnapping ? : }
-
- onBrushBlendingChange(!useBrushBlending)}
- >
- {useBrushBlending ? : }
-
- onBrushGestureChange(!useBrushGesture)}
- >
- {useBrushGesture ? : }
-
-
-
- ),
- erase: (
-
-
-
- ),
- };
-
- const [currentSubmenu, setCurrentSubmenu] = useState(null);
- const [currentSubmenuOptions, setCurrentSubmenuOptions] = useState({});
-
- function handleToolClick(event, tool) {
- if (tool !== selectedTool) {
- onToolChange(tool);
- } else if (currentSubmenu) {
- setCurrentSubmenu(null);
- setCurrentSubmenuOptions({});
- } else if (subMenus[tool]) {
- const toolRect = event.target.getBoundingClientRect();
- setCurrentSubmenu(tool);
- setCurrentSubmenuOptions({
- // Align the right of the submenu to the left of the tool and center vertically
- left: `${toolRect.left - 16}px`,
- top: `${toolRect.bottom - toolRect.height / 2}px`,
- style: { transform: "translate(-100%, -50%)" },
- // Exclude this node from the sub menus auto close
- excludeNode: event.target,
- });
- }
- }
-
- // Detect when a tool becomes disabled and switch to to the pan tool
- useEffect(() => {
- if (disabledTools.includes(selectedTool)) {
- onToolChange("pan");
- }
- }, [selectedTool, disabledTools, onToolChange]);
-
- const divider = (
-
- );
-
- const expanedMenuRef = useRef();
-
- return (
- <>
-
- setIsExpanded(!isExpanded)}
- sx={{
- transform: `rotate(${isExpanded ? "0" : "180deg"})`,
- display: "block",
- backgroundColor: "overlay",
- borderRadius: "50%",
- }}
- m={2}
- >
-
-
-
-
- {divider}
- handleToolClick(e, "pan")}
- sx={{ color: selectedTool === "pan" ? "primary" : "text" }}
- disabled={disabledTools.includes("pan")}
- >
-
-
- handleToolClick(e, "brush")}
- sx={{ color: selectedTool === "brush" ? "primary" : "text" }}
- disabled={disabledTools.includes("brush")}
- >
-
-
- handleToolClick(e, "erase")}
- sx={{ color: selectedTool === "erase" ? "primary" : "text" }}
- disabled={disabledTools.includes("erase")}
- >
-
-
- {divider}
- onUndo()}
- disabled={undoDisabled}
- >
-
-
- onRedo()}
- disabled={redoDisabled}
- >
-
-
-
-
- {
- setCurrentSubmenu(null);
- setCurrentSubmenuOptions({});
- }}
- {...currentSubmenuOptions}
- >
- {currentSubmenu && subMenus[currentSubmenu]}
-
- >
- );
-}
-
-export default MapControls;
diff --git a/src/components/MapDrawing.js b/src/components/MapDrawing.js
deleted file mode 100644
index 2387eef..0000000
--- a/src/components/MapDrawing.js
+++ /dev/null
@@ -1,233 +0,0 @@
-import React, { useRef, useEffect, useState } from "react";
-import simplify from "simplify-js";
-import shortid from "shortid";
-
-import colors from "../helpers/colors";
-import { snapPositionToGrid } from "../helpers/shared";
-
-import { pointsToGesture, gestureToData } from "../helpers/gestures";
-
-function MapDrawing({
- width,
- height,
- selectedTool,
- shapes,
- onShapeAdd,
- onShapeRemove,
- brushColor,
- useGridSnapping,
- gridSize,
- useBrushBlending,
- useBrushGesture,
-}) {
- const canvasRef = useRef();
- const containerRef = useRef();
-
- const [brushPoints, setBrushPoints] = useState([]);
- const [isDrawing, setIsDrawing] = useState(false);
- const [pointerPosition, setPointerPosition] = useState({ x: -1, y: -1 });
-
- // Reset pointer position when tool changes
- useEffect(() => {
- setPointerPosition({ x: -1, y: -1 });
- }, [selectedTool]);
-
- function getRelativePointerPosition(event) {
- const container = containerRef.current;
- if (container) {
- const containerRect = container.getBoundingClientRect();
- const x = (event.clientX - containerRect.x) / containerRect.width;
- const y = (event.clientY - containerRect.y) / containerRect.height;
- return { x, y };
- }
- }
-
- function handleStart(event) {
- if (event.touches && event.touches.length !== 1) {
- setIsDrawing(false);
- setBrushPoints([]);
- return;
- }
- const pointer = event.touches ? event.touches[0] : event;
- const position = getRelativePointerPosition(pointer);
- setPointerPosition(position);
- setIsDrawing(true);
- if (selectedTool === "brush") {
- const brushPosition = useGridSnapping
- ? snapPositionToGrid(position, gridSize)
- : position;
- setBrushPoints([brushPosition]);
- }
- }
-
- function handleMove(event) {
- if (event.touches && event.touches.length !== 1) {
- return;
- }
- const pointer = event.touches ? event.touches[0] : event;
- const position = getRelativePointerPosition(pointer);
- if (selectedTool === "erase") {
- setPointerPosition(position);
- }
- if (isDrawing && selectedTool === "brush") {
- setPointerPosition(position);
- const brushPosition = useGridSnapping
- ? snapPositionToGrid(position, gridSize)
- : position;
- setBrushPoints((prevPoints) => {
- if (prevPoints[prevPoints.length - 1] === brushPosition) {
- return prevPoints;
- }
- return [...prevPoints, brushPosition];
- });
- }
- }
-
- function handleStop(event) {
- if (event.touches && event.touches.length !== 0) {
- return;
- }
- setIsDrawing(false);
- if (selectedTool === "brush") {
- if (brushPoints.length > 1) {
- const simplifiedPoints = simplify(brushPoints, 0.001);
- const type = useBrushGesture
- ? pointsToGesture(simplifiedPoints)
- : "path";
-
- if (type !== null) {
- const data =
- type === "path"
- ? { points: simplifiedPoints }
- : gestureToData(simplifiedPoints, type);
-
- onShapeAdd({
- type,
- data,
- id: shortid.generate(),
- color: brushColor,
- blend: useBrushBlending,
- });
- }
-
- setBrushPoints([]);
- }
- }
- if (selectedTool === "erase" && hoveredShapeRef.current) {
- onShapeRemove(hoveredShapeRef.current.id);
- }
- }
-
- const hoveredShapeRef = useRef(null);
- useEffect(() => {
- function pointsToPath(points) {
- const path = new Path2D();
- path.moveTo(points[0].x * width, points[0].y * height);
- for (let point of points.slice(1)) {
- path.lineTo(point.x * width, point.y * height);
- }
- path.closePath();
- return path;
- }
-
- function circleToPath(x, y, radius) {
- const path = new Path2D();
- const minSide = width < height ? width : height;
- path.arc(x * width, y * height, radius * minSide, 0, 2 * Math.PI, true);
- return path;
- }
-
- function rectangleToPath(x, y, w, h) {
- const path = new Path2D();
- path.rect(x * width, y * height, w * width, h * height);
- return path;
- }
-
- function shapeToPath(shape) {
- const data = shape.data;
- if (shape.type === "path") {
- return pointsToPath(data.points);
- } else if (shape.type === "circle") {
- return circleToPath(data.x, data.y, data.radius);
- } else if (shape.type === "rectangle") {
- return rectangleToPath(data.x, data.y, data.width, data.height);
- } else if (shape.type === "triangle") {
- return pointsToPath(data.points);
- }
- }
-
- function drawPath(path, color, blend, context) {
- context.globalAlpha = blend ? 0.5 : 1.0;
- context.fillStyle = color;
- context.strokeStyle = color;
- context.stroke(path);
- context.fill(path);
- }
-
- const canvas = canvasRef.current;
- if (canvas) {
- const context = canvas.getContext("2d");
-
- context.clearRect(0, 0, width, height);
- let hoveredShape = null;
- for (let shape of shapes) {
- const path = shapeToPath(shape);
- // Detect hover
- if (selectedTool === "erase") {
- if (
- context.isPointInPath(
- path,
- pointerPosition.x * width,
- pointerPosition.y * height
- )
- ) {
- hoveredShape = shape;
- }
- }
- drawPath(path, colors[shape.color], shape.blend, context);
- }
- if (selectedTool === "brush" && brushPoints.length > 0) {
- const path = pointsToPath(brushPoints);
- drawPath(path, colors[brushColor], useBrushBlending, context);
- }
- if (hoveredShape) {
- const path = shapeToPath(hoveredShape);
- drawPath(path, "#BB99FF", true, context);
- }
- hoveredShapeRef.current = hoveredShape;
- }
- }, [
- shapes,
- width,
- height,
- pointerPosition,
- isDrawing,
- selectedTool,
- brushPoints,
- brushColor,
- useBrushGesture,
- useBrushBlending,
- ]);
-
- return (
-
-
-
- );
-}
-
-export default MapDrawing;
diff --git a/src/components/SettingsButton.js b/src/components/SettingsButton.js
new file mode 100644
index 0000000..750978c
--- /dev/null
+++ b/src/components/SettingsButton.js
@@ -0,0 +1,31 @@
+import React, { useState } from "react";
+import { IconButton } from "theme-ui";
+
+import SettingsIcon from "../icons/SettingsIcon";
+import SettingsModal from "../modals/SettingsModal";
+
+function SettingsButton() {
+ const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
+ function openModal() {
+ setIsSettingsModalOpen(true);
+ }
+ function closeModal() {
+ setIsSettingsModalOpen(false);
+ }
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default SettingsButton;
diff --git a/src/components/map/Map.js b/src/components/map/Map.js
new file mode 100644
index 0000000..965afef
--- /dev/null
+++ b/src/components/map/Map.js
@@ -0,0 +1,323 @@
+import React, { useRef, useEffect, useState } from "react";
+import { Box, Image } from "theme-ui";
+
+import ProxyToken from "../token/ProxyToken";
+import TokenMenu from "../token/TokenMenu";
+import MapToken from "./MapToken";
+import MapDrawing from "./MapDrawing";
+import MapFog from "./MapFog";
+import MapControls from "./MapControls";
+
+import { omit } from "../../helpers/shared";
+import useDataSource from "../../helpers/useDataSource";
+import MapInteraction from "./MapInteraction";
+
+import { mapSources as defaultMapSources } from "../../maps";
+
+const mapTokenProxyClassName = "map-token__proxy";
+const mapTokenMenuClassName = "map-token__menu";
+
+function Map({
+ map,
+ mapState,
+ tokens,
+ onMapTokenStateChange,
+ onMapTokenStateRemove,
+ onMapChange,
+ onMapStateChange,
+ onMapDraw,
+ onMapDrawUndo,
+ onMapDrawRedo,
+ onFogDraw,
+ onFogDrawUndo,
+ onFogDrawRedo,
+ allowMapDrawing,
+ allowFogDrawing,
+ disabledTokens,
+ loading,
+}) {
+ const mapSource = useDataSource(map, defaultMapSources);
+
+ function handleProxyDragEnd(isOnMap, tokenState) {
+ if (isOnMap && onMapTokenStateChange) {
+ onMapTokenStateChange(tokenState);
+ }
+
+ if (!isOnMap && onMapTokenStateRemove) {
+ onMapTokenStateRemove(tokenState);
+ }
+ }
+
+ /**
+ * Map drawing
+ */
+
+ const [selectedToolId, setSelectedToolId] = useState("pan");
+ const [toolSettings, setToolSettings] = useState({
+ fog: { type: "add", useEdgeSnapping: true, useGridSnapping: false },
+ brush: {
+ color: "darkGray",
+ type: "stroke",
+ useBlending: false,
+ },
+ shape: {
+ color: "red",
+ type: "rectangle",
+ useBlending: true,
+ },
+ });
+
+ function handleToolSettingChange(tool, change) {
+ setToolSettings((prevSettings) => ({
+ ...prevSettings,
+ [tool]: {
+ ...prevSettings[tool],
+ ...change,
+ },
+ }));
+ }
+
+ function handleToolAction(action) {
+ if (action === "eraseAll") {
+ onMapDraw({
+ type: "remove",
+ shapeIds: mapShapes.map((s) => s.id),
+ timestamp: Date.now(),
+ });
+ }
+ if (action === "mapUndo") {
+ onMapDrawUndo();
+ }
+ if (action === "mapRedo") {
+ onMapDrawRedo();
+ }
+ if (action === "fogUndo") {
+ onFogDrawUndo();
+ }
+ if (action === "fogRedo") {
+ onFogDrawRedo();
+ }
+ }
+
+ const [mapShapes, setMapShapes] = useState([]);
+ function handleMapShapeAdd(shape) {
+ onMapDraw({ type: "add", shapes: [shape] });
+ }
+
+ function handleMapShapeRemove(shapeId) {
+ onMapDraw({ type: "remove", shapeIds: [shapeId] });
+ }
+
+ const [fogShapes, setFogShapes] = useState([]);
+ function handleFogShapeAdd(shape) {
+ onFogDraw({ type: "add", shapes: [shape] });
+ }
+
+ function handleFogShapeRemove(shapeId) {
+ onFogDraw({ type: "remove", shapeIds: [shapeId] });
+ }
+
+ function handleFogShapeEdit(shape) {
+ onFogDraw({ type: "edit", shapes: [shape] });
+ }
+
+ // Replay the draw actions and convert them to shapes for the map drawing
+ useEffect(() => {
+ if (!mapState) {
+ return;
+ }
+ function actionsToShapes(actions, actionIndex) {
+ let shapesById = {};
+ for (let i = 0; i <= actionIndex; i++) {
+ const action = actions[i];
+ if (action.type === "add" || action.type === "edit") {
+ for (let shape of action.shapes) {
+ shapesById[shape.id] = shape;
+ }
+ }
+ if (action.type === "remove") {
+ shapesById = omit(shapesById, action.shapeIds);
+ }
+ }
+ return Object.values(shapesById);
+ }
+
+ setMapShapes(
+ actionsToShapes(mapState.mapDrawActions, mapState.mapDrawActionIndex)
+ );
+ setFogShapes(
+ actionsToShapes(mapState.fogDrawActions, mapState.fogDrawActionIndex)
+ );
+ }, [mapState]);
+
+ const disabledControls = [];
+ if (!allowMapDrawing) {
+ disabledControls.push("brush");
+ disabledControls.push("shape");
+ disabledControls.push("erase");
+ }
+ if (!map) {
+ disabledControls.push("pan");
+ }
+ if (mapShapes.length === 0) {
+ disabledControls.push("erase");
+ }
+ if (!allowFogDrawing) {
+ disabledControls.push("fog");
+ }
+
+ const disabledSettings = { fog: [], brush: [], shape: [], erase: [] };
+ if (!mapState || mapState.mapDrawActionIndex < 0) {
+ disabledSettings.brush.push("undo");
+ disabledSettings.shape.push("undo");
+ disabledSettings.erase.push("undo");
+ }
+ if (
+ !mapState ||
+ mapState.mapDrawActionIndex === mapState.mapDrawActions.length - 1
+ ) {
+ disabledSettings.brush.push("redo");
+ disabledSettings.shape.push("redo");
+ disabledSettings.erase.push("redo");
+ }
+ if (fogShapes.length === 0) {
+ disabledSettings.fog.push("undo");
+ }
+ if (
+ !mapState ||
+ mapState.fogDrawActionIndex === mapState.fogDrawActions.length - 1
+ ) {
+ disabledSettings.fog.push("redo");
+ }
+
+ /**
+ * Member setup
+ */
+
+ const mapRef = useRef(null);
+
+ const gridX = map && map.gridX;
+ const gridY = map && map.gridY;
+ const gridSizeNormalized = { x: 1 / gridX || 0, y: 1 / gridY || 0 };
+ const tokenSizePercent = gridSizeNormalized.x * 100;
+ const aspectRatio = (map && map.width / map.height) || 1;
+
+ const mapImage = (
+
+
+
+ );
+
+ const mapTokens = (
+
+ {mapState &&
+ Object.values(mapState.tokens).map((tokenState) => (
+ token.id === tokenState.tokenId)}
+ tokenState={tokenState}
+ tokenSizePercent={tokenSizePercent}
+ className={`${mapTokenProxyClassName} ${mapTokenMenuClassName}`}
+ />
+ ))}
+
+ );
+
+ const mapDrawing = (
+
+ );
+
+ const mapFog = (
+
+ );
+
+ const mapControls = (
+
+ );
+ return (
+ <>
+
+ {map && mapImage}
+ {map && mapDrawing}
+ {map && mapFog}
+ {map && mapTokens}
+
+
+
+ >
+ );
+}
+
+export default Map;
diff --git a/src/components/map/MapControls.js b/src/components/map/MapControls.js
new file mode 100644
index 0000000..7399798
--- /dev/null
+++ b/src/components/map/MapControls.js
@@ -0,0 +1,230 @@
+import React, { useState, Fragment, useEffect, useRef } from "react";
+import { IconButton, Flex, Box } from "theme-ui";
+
+import RadioIconButton from "./controls/RadioIconButton";
+import Divider from "./controls/Divider";
+
+import SelectMapButton from "./SelectMapButton";
+
+import FogToolSettings from "./controls/FogToolSettings";
+import BrushToolSettings from "./controls/BrushToolSettings";
+import ShapeToolSettings from "./controls/ShapeToolSettings";
+import EraseToolSettings from "./controls/EraseToolSettings";
+
+import PanToolIcon from "../../icons/PanToolIcon";
+import FogToolIcon from "../../icons/FogToolIcon";
+import BrushToolIcon from "../../icons/BrushToolIcon";
+import ShapeToolIcon from "../../icons/ShapeToolIcon";
+import EraseToolIcon from "../../icons/EraseToolIcon";
+import ExpandMoreIcon from "../../icons/ExpandMoreIcon";
+
+function MapContols({
+ onMapChange,
+ onMapStateChange,
+ currentMap,
+ selectedToolId,
+ onSelectedToolChange,
+ toolSettings,
+ onToolSettingChange,
+ onToolAction,
+ disabledControls,
+ disabledSettings,
+}) {
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ const toolsById = {
+ pan: {
+ id: "pan",
+ icon: ,
+ title: "Pan Tool",
+ },
+ fog: {
+ id: "fog",
+ icon: ,
+ title: "Fog Tool",
+ SettingsComponent: FogToolSettings,
+ },
+ brush: {
+ id: "brush",
+ icon: ,
+ title: "Brush Tool",
+ SettingsComponent: BrushToolSettings,
+ },
+ shape: {
+ id: "shape",
+ icon: ,
+ title: "Shape Tool",
+ SettingsComponent: ShapeToolSettings,
+ },
+ erase: {
+ id: "erase",
+ icon: ,
+ title: "Erase tool",
+ SettingsComponent: EraseToolSettings,
+ },
+ };
+ const tools = ["pan", "fog", "brush", "shape", "erase"];
+
+ const sections = [
+ {
+ id: "map",
+ component: (
+
+ ),
+ },
+ {
+ id: "drawing",
+ component: tools.map((tool) => (
+ onSelectedToolChange(tool)}
+ isSelected={selectedToolId === tool}
+ disabled={disabledControls.includes(tool)}
+ >
+ {toolsById[tool].icon}
+
+ )),
+ },
+ ];
+
+ let controls = null;
+ if (sections.length === 1 && sections[0].id === "map") {
+ controls = (
+
+ {sections[0].component}
+
+ );
+ } else if (sections.length > 0) {
+ controls = (
+ <>
+ setIsExpanded(!isExpanded)}
+ sx={{
+ transform: `rotate(${isExpanded ? "0" : "180deg"})`,
+ display: "block",
+ backgroundColor: "overlay",
+ borderRadius: "50%",
+ }}
+ m={2}
+ >
+
+
+
+ {sections.map((section, index) => (
+
+ {section.component}
+ {index !== sections.length - 1 && }
+
+ ))}
+
+ >
+ );
+ }
+
+ const controlsRef = useRef();
+ const settingsRef = useRef();
+
+ function getToolSettings() {
+ const Settings = toolsById[selectedToolId].SettingsComponent;
+ if (Settings) {
+ return (
+
+
+ onToolSettingChange(selectedToolId, change)
+ }
+ onToolAction={onToolAction}
+ disabledActions={disabledSettings[selectedToolId]}
+ />
+
+ );
+ } else {
+ return null;
+ }
+ }
+
+ // Stop map drawing from happening when selecting controls
+ // Not using react events as they seem to trigger after dom events
+ useEffect(() => {
+ function stopPropagation(e) {
+ e.stopPropagation();
+ }
+ const controls = controlsRef.current;
+ if (controls) {
+ controls.addEventListener("mousedown", stopPropagation);
+ controls.addEventListener("touchstart", stopPropagation);
+ }
+ const settings = settingsRef.current;
+ if (settings) {
+ settings.addEventListener("mousedown", stopPropagation);
+ settings.addEventListener("touchstart", stopPropagation);
+ }
+
+ return () => {
+ if (controls) {
+ controls.removeEventListener("mousedown", stopPropagation);
+ controls.removeEventListener("touchstart", stopPropagation);
+ }
+ if (settings) {
+ settings.removeEventListener("mousedown", stopPropagation);
+ settings.removeEventListener("touchstart", stopPropagation);
+ }
+ };
+ });
+
+ return (
+ <>
+
+ {controls}
+
+ {getToolSettings()}
+ >
+ );
+}
+
+export default MapContols;
diff --git a/src/components/map/MapDrawing.js b/src/components/map/MapDrawing.js
new file mode 100644
index 0000000..fbc5416
--- /dev/null
+++ b/src/components/map/MapDrawing.js
@@ -0,0 +1,261 @@
+import React, { useRef, useEffect, useState, useContext } from "react";
+import shortid from "shortid";
+
+import { compare as comparePoints } from "../../helpers/vector2";
+import {
+ getBrushPositionForTool,
+ getDefaultShapeData,
+ getUpdatedShapeData,
+ isShapeHovered,
+ drawShape,
+ simplifyPoints,
+ getRelativePointerPosition,
+} from "../../helpers/drawing";
+
+import MapInteractionContext from "../../contexts/MapInteractionContext";
+
+function MapDrawing({
+ width,
+ height,
+ selectedTool,
+ toolSettings,
+ shapes,
+ onShapeAdd,
+ onShapeRemove,
+ gridSize,
+}) {
+ const canvasRef = useRef();
+ const containerRef = useRef();
+
+ const [isPointerDown, setIsPointerDown] = useState(false);
+ const [drawingShape, setDrawingShape] = useState(null);
+ const [pointerPosition, setPointerPosition] = useState({ x: -1, y: -1 });
+
+ const shouldHover = selectedTool === "erase";
+ const isEditing =
+ selectedTool === "brush" ||
+ selectedTool === "shape" ||
+ selectedTool === "erase";
+
+ const { scaleRef } = useContext(MapInteractionContext);
+
+ // Reset pointer position when tool changes
+ useEffect(() => {
+ setPointerPosition({ x: -1, y: -1 });
+ }, [selectedTool]);
+
+ function handleStart(event) {
+ if (!isEditing) {
+ return;
+ }
+ if (event.touches && event.touches.length !== 1) {
+ setIsPointerDown(false);
+ setDrawingShape(null);
+ return;
+ }
+ const pointer = event.touches ? event.touches[0] : event;
+ const position = getRelativePointerPosition(pointer, containerRef.current);
+ setPointerPosition(position);
+ setIsPointerDown(true);
+ const brushPosition = getBrushPositionForTool(
+ position,
+ selectedTool,
+ toolSettings,
+ gridSize,
+ shapes
+ );
+ const commonShapeData = {
+ color: toolSettings && toolSettings.color,
+ blend: toolSettings && toolSettings.useBlending,
+ id: shortid.generate(),
+ };
+ if (selectedTool === "brush") {
+ setDrawingShape({
+ type: "path",
+ pathType: toolSettings.type,
+ data: { points: [brushPosition] },
+ strokeWidth: toolSettings.type === "stroke" ? 1 : 0,
+ ...commonShapeData,
+ });
+ } else if (selectedTool === "shape") {
+ setDrawingShape({
+ type: "shape",
+ shapeType: toolSettings.type,
+ data: getDefaultShapeData(toolSettings.type, brushPosition),
+ strokeWidth: 0,
+ ...commonShapeData,
+ });
+ }
+ }
+
+ function handleMove(event) {
+ if (!isEditing) {
+ return;
+ }
+ if (event.touches && event.touches.length !== 1) {
+ return;
+ }
+ const pointer = event.touches ? event.touches[0] : event;
+ // Set pointer position every frame for erase tool and fog
+ if (shouldHover) {
+ const position = getRelativePointerPosition(
+ pointer,
+ containerRef.current
+ );
+ setPointerPosition(position);
+ }
+ if (isPointerDown) {
+ const position = getRelativePointerPosition(
+ pointer,
+ containerRef.current
+ );
+ setPointerPosition(position);
+ const brushPosition = getBrushPositionForTool(
+ position,
+ selectedTool,
+ toolSettings,
+ gridSize,
+ shapes
+ );
+ if (selectedTool === "brush") {
+ setDrawingShape((prevShape) => {
+ const prevPoints = prevShape.data.points;
+ if (
+ comparePoints(
+ prevPoints[prevPoints.length - 1],
+ brushPosition,
+ 0.001
+ )
+ ) {
+ return prevShape;
+ }
+ const simplified = simplifyPoints(
+ [...prevPoints, brushPosition],
+ gridSize,
+ scaleRef.current
+ );
+ return {
+ ...prevShape,
+ data: { points: simplified },
+ };
+ });
+ } else if (selectedTool === "shape") {
+ setDrawingShape((prevShape) => ({
+ ...prevShape,
+ data: getUpdatedShapeData(
+ prevShape.shapeType,
+ prevShape.data,
+ brushPosition,
+ gridSize
+ ),
+ }));
+ }
+ }
+ }
+
+ function handleStop(event) {
+ if (!isEditing) {
+ return;
+ }
+ if (event.touches && event.touches.length !== 0) {
+ return;
+ }
+ if (selectedTool === "brush" && drawingShape) {
+ if (drawingShape.data.points.length > 1) {
+ onShapeAdd(drawingShape);
+ }
+ } else if (selectedTool === "shape" && drawingShape) {
+ onShapeAdd(drawingShape);
+ }
+
+ if (selectedTool === "erase" && hoveredShapeRef.current && isPointerDown) {
+ onShapeRemove(hoveredShapeRef.current.id);
+ }
+ setIsPointerDown(false);
+ setDrawingShape(null);
+ }
+
+ // Add listeners for draw events on map to allow drawing past the bounds
+ // of the container
+ useEffect(() => {
+ const map = document.querySelector(".map");
+ map.addEventListener("mousedown", handleStart);
+ map.addEventListener("mousemove", handleMove);
+ map.addEventListener("mouseup", handleStop);
+ map.addEventListener("touchstart", handleStart);
+ map.addEventListener("touchmove", handleMove);
+ map.addEventListener("touchend", handleStop);
+
+ return () => {
+ map.removeEventListener("mousedown", handleStart);
+ map.removeEventListener("mousemove", handleMove);
+ map.removeEventListener("mouseup", handleStop);
+ map.removeEventListener("touchstart", handleStart);
+ map.removeEventListener("touchmove", handleMove);
+ map.removeEventListener("touchend", handleStop);
+ };
+ });
+
+ /**
+ * Rendering
+ */
+ const hoveredShapeRef = useRef(null);
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (canvas) {
+ const context = canvas.getContext("2d");
+
+ context.clearRect(0, 0, width, height);
+ let hoveredShape = null;
+ for (let shape of shapes) {
+ if (shouldHover) {
+ if (isShapeHovered(shape, context, pointerPosition, width, height)) {
+ hoveredShape = shape;
+ }
+ }
+ drawShape(shape, context, gridSize, width, height);
+ }
+ if (drawingShape) {
+ drawShape(drawingShape, context, gridSize, width, height);
+ }
+ if (hoveredShape) {
+ const shape = { ...hoveredShape, color: "#BB99FF", blend: true };
+ drawShape(shape, context, gridSize, width, height);
+ }
+ hoveredShapeRef.current = hoveredShape;
+ }
+ }, [
+ shapes,
+ width,
+ height,
+ pointerPosition,
+ isPointerDown,
+ selectedTool,
+ drawingShape,
+ gridSize,
+ shouldHover,
+ ]);
+
+ return (
+
+
+
+ );
+}
+
+export default MapDrawing;
diff --git a/src/components/map/MapFog.js b/src/components/map/MapFog.js
new file mode 100644
index 0000000..c0cbfba
--- /dev/null
+++ b/src/components/map/MapFog.js
@@ -0,0 +1,276 @@
+import React, { useRef, useEffect, useState, useContext } from "react";
+import shortid from "shortid";
+
+import { compare as comparePoints } from "../../helpers/vector2";
+import {
+ getBrushPositionForTool,
+ isShapeHovered,
+ drawShape,
+ simplifyPoints,
+ getRelativePointerPosition,
+} from "../../helpers/drawing";
+
+import MapInteractionContext from "../../contexts/MapInteractionContext";
+
+import diagonalPattern from "../../images/DiagonalPattern.png";
+
+function MapFog({
+ width,
+ height,
+ isEditing,
+ toolSettings,
+ shapes,
+ onShapeAdd,
+ onShapeRemove,
+ onShapeEdit,
+ gridSize,
+}) {
+ const canvasRef = useRef();
+ const containerRef = useRef();
+
+ const [isPointerDown, setIsPointerDown] = useState(false);
+ const [drawingShape, setDrawingShape] = useState(null);
+ const [pointerPosition, setPointerPosition] = useState({ x: -1, y: -1 });
+
+ const shouldHover =
+ isEditing &&
+ (toolSettings.type === "toggle" || toolSettings.type === "remove");
+
+ const { scaleRef } = useContext(MapInteractionContext);
+
+ // Reset pointer position when tool changes
+ useEffect(() => {
+ setPointerPosition({ x: -1, y: -1 });
+ }, [isEditing, toolSettings]);
+
+ function handleStart(event) {
+ if (!isEditing) {
+ return;
+ }
+ if (event.touches && event.touches.length !== 1) {
+ setIsPointerDown(false);
+ setDrawingShape(null);
+ return;
+ }
+ const pointer = event.touches ? event.touches[0] : event;
+ const position = getRelativePointerPosition(pointer, containerRef.current);
+ setPointerPosition(position);
+ setIsPointerDown(true);
+ const brushPosition = getBrushPositionForTool(
+ position,
+ "fog",
+ toolSettings,
+ gridSize,
+ shapes
+ );
+ if (isEditing && toolSettings.type === "add") {
+ setDrawingShape({
+ type: "fog",
+ data: { points: [brushPosition] },
+ strokeWidth: 0.5,
+ color: "black",
+ blend: true, // Blend while drawing
+ id: shortid.generate(),
+ visible: true,
+ });
+ }
+ }
+
+ function handleMove(event) {
+ if (!isEditing) {
+ return;
+ }
+ if (event.touches && event.touches.length !== 1) {
+ return;
+ }
+ const pointer = event.touches ? event.touches[0] : event;
+ const position = getRelativePointerPosition(pointer, containerRef.current);
+ // Set pointer position every frame for erase tool and fog
+ if (shouldHover) {
+ setPointerPosition(position);
+ }
+ if (isPointerDown) {
+ setPointerPosition(position);
+ const brushPosition = getBrushPositionForTool(
+ position,
+ "fog",
+ toolSettings,
+ gridSize,
+ shapes
+ );
+ if (isEditing && toolSettings.type === "add" && drawingShape) {
+ setDrawingShape((prevShape) => {
+ const prevPoints = prevShape.data.points;
+ if (
+ comparePoints(
+ prevPoints[prevPoints.length - 1],
+ brushPosition,
+ 0.001
+ )
+ ) {
+ return prevShape;
+ }
+ return {
+ ...prevShape,
+ data: { points: [...prevPoints, brushPosition] },
+ };
+ });
+ }
+ }
+ }
+
+ function handleStop(event) {
+ if (!isEditing) {
+ return;
+ }
+ if (event.touches && event.touches.length !== 0) {
+ return;
+ }
+ if (isEditing && toolSettings.type === "add" && drawingShape) {
+ if (drawingShape.data.points.length > 1) {
+ const shape = {
+ ...drawingShape,
+ data: {
+ points: simplifyPoints(
+ drawingShape.data.points,
+ gridSize,
+ // Downscale fog as smoothing doesn't currently work with edge snapping
+ scaleRef.current / 2
+ ),
+ },
+ blend: false,
+ };
+ onShapeAdd(shape);
+ }
+ }
+
+ if (hoveredShapeRef.current && isPointerDown) {
+ if (toolSettings.type === "remove") {
+ onShapeRemove(hoveredShapeRef.current.id);
+ } else if (toolSettings.type === "toggle") {
+ onShapeEdit({
+ ...hoveredShapeRef.current,
+ visible: !hoveredShapeRef.current.visible,
+ });
+ }
+ }
+ setDrawingShape(null);
+ setIsPointerDown(false);
+ }
+
+ // Add listeners for draw events on map to allow drawing past the bounds
+ // of the container
+ useEffect(() => {
+ const map = document.querySelector(".map");
+ map.addEventListener("mousedown", handleStart);
+ map.addEventListener("mousemove", handleMove);
+ map.addEventListener("mouseup", handleStop);
+ map.addEventListener("touchstart", handleStart);
+ map.addEventListener("touchmove", handleMove);
+ map.addEventListener("touchend", handleStop);
+
+ return () => {
+ map.removeEventListener("mousedown", handleStart);
+ map.removeEventListener("mousemove", handleMove);
+ map.removeEventListener("mouseup", handleStop);
+ map.removeEventListener("touchstart", handleStart);
+ map.removeEventListener("touchmove", handleMove);
+ map.removeEventListener("touchend", handleStop);
+ };
+ });
+
+ /**
+ * Rendering
+ */
+ const hoveredShapeRef = useRef(null);
+ const diagonalPatternRef = useRef();
+
+ useEffect(() => {
+ let image = new Image();
+ image.src = diagonalPattern;
+ diagonalPatternRef.current = image;
+ }, []);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (canvas) {
+ const context = canvas.getContext("2d");
+
+ context.clearRect(0, 0, width, height);
+ let hoveredShape = null;
+ if (isEditing) {
+ const editPattern = context.createPattern(
+ diagonalPatternRef.current,
+ "repeat"
+ );
+ for (let shape of shapes) {
+ if (shouldHover) {
+ if (
+ isShapeHovered(shape, context, pointerPosition, width, height)
+ ) {
+ hoveredShape = shape;
+ }
+ }
+ drawShape(
+ {
+ ...shape,
+ blend: true,
+ color: shape.visible ? "black" : editPattern,
+ },
+ context,
+ gridSize,
+ width,
+ height
+ );
+ }
+ if (drawingShape) {
+ drawShape(drawingShape, context, gridSize, width, height);
+ }
+ if (hoveredShape) {
+ const shape = { ...hoveredShape, color: "#BB99FF", blend: true };
+ drawShape(shape, context, gridSize, width, height);
+ }
+ } else {
+ // Not editing
+ for (let shape of shapes) {
+ if (shape.visible) {
+ drawShape(shape, context, gridSize, width, height);
+ }
+ }
+ }
+ hoveredShapeRef.current = hoveredShape;
+ }
+ }, [
+ shapes,
+ width,
+ height,
+ pointerPosition,
+ isEditing,
+ drawingShape,
+ gridSize,
+ shouldHover,
+ ]);
+
+ return (
+
+
+
+ );
+}
+
+export default MapFog;
diff --git a/src/components/map/MapInteraction.js b/src/components/map/MapInteraction.js
new file mode 100644
index 0000000..06e0cd0
--- /dev/null
+++ b/src/components/map/MapInteraction.js
@@ -0,0 +1,168 @@
+import React, { useRef, useEffect } from "react";
+import { Box } from "theme-ui";
+import interact from "interactjs";
+import normalizeWheel from "normalize-wheel";
+
+import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
+
+import LoadingOverlay from "../LoadingOverlay";
+
+const zoomSpeed = -0.001;
+const minZoom = 0.1;
+const maxZoom = 5;
+
+function MapInteraction({
+ map,
+ aspectRatio,
+ isEnabled,
+ children,
+ controls,
+ loading,
+}) {
+ const mapContainerRef = useRef();
+ const mapMoveContainerRef = useRef();
+ const mapTranslateRef = useRef({ x: 0, y: 0 });
+ const mapScaleRef = useRef(1);
+ function setTranslateAndScale(newTranslate, newScale) {
+ const moveContainer = mapMoveContainerRef.current;
+ moveContainer.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px) scale(${newScale})`;
+ mapScaleRef.current = newScale;
+ mapTranslateRef.current = newTranslate;
+ }
+
+ useEffect(() => {
+ function handleMove(event, isGesture) {
+ const scale = mapScaleRef.current;
+ const translate = mapTranslateRef.current;
+
+ let newScale = scale;
+ let newTranslate = translate;
+
+ if (isGesture) {
+ newScale = Math.max(Math.min(scale + event.ds, maxZoom), minZoom);
+ }
+
+ if (isEnabled || isGesture) {
+ newTranslate = {
+ x: translate.x + event.dx,
+ y: translate.y + event.dy,
+ };
+ }
+ setTranslateAndScale(newTranslate, newScale);
+ }
+ const mapInteract = interact(".map")
+ .gesturable({
+ listeners: {
+ move: (e) => handleMove(e, true),
+ },
+ })
+ .draggable({
+ inertia: true,
+ listeners: {
+ move: (e) => handleMove(e, false),
+ },
+ cursorChecker: () => {
+ return isEnabled && map ? "move" : "default";
+ },
+ })
+ .on("doubletap", (event) => {
+ event.preventDefault();
+ if (isEnabled) {
+ setTranslateAndScale({ x: 0, y: 0 }, 1);
+ }
+ });
+
+ return () => {
+ mapInteract.unset();
+ };
+ }, [isEnabled, map]);
+
+ // Reset map transform when map changes
+ useEffect(() => {
+ setTranslateAndScale({ x: 0, y: 0 }, 1);
+ }, [map]);
+
+ // Bind the wheel event of the map via a ref
+ // in order to support non-passive event listening
+ // to allow the track pad zoom to be interrupted
+ // see https://github.com/facebook/react/issues/14856
+ useEffect(() => {
+ const mapContainer = mapContainerRef.current;
+
+ function handleZoom(event) {
+ // Stop overscroll on chrome and safari
+ // also stop pinch to zoom on chrome
+ event.preventDefault();
+
+ // Try and normalize the wheel event to prevent OS differences for zoom speed
+ const normalized = normalizeWheel(event);
+
+ const scale = mapScaleRef.current;
+ const translate = mapTranslateRef.current;
+
+ const deltaY = normalized.pixelY * zoomSpeed;
+ const newScale = Math.max(Math.min(scale + deltaY, maxZoom), minZoom);
+
+ setTranslateAndScale(translate, newScale);
+ }
+
+ if (mapContainer) {
+ mapContainer.addEventListener("wheel", handleZoom, {
+ passive: false,
+ });
+ }
+
+ return () => {
+ if (mapContainer) {
+ mapContainer.removeEventListener("wheel", handleZoom);
+ }
+ };
+ }, []);
+
+ return (
+
+
+
+
+
+ {children}
+
+
+
+ {controls}
+ {loading && }
+
+ );
+}
+
+export default MapInteraction;
diff --git a/src/components/MapMenu.js b/src/components/map/MapMenu.js
similarity index 100%
rename from src/components/MapMenu.js
rename to src/components/map/MapMenu.js
diff --git a/src/components/map/MapSettings.js b/src/components/map/MapSettings.js
new file mode 100644
index 0000000..230d327
--- /dev/null
+++ b/src/components/map/MapSettings.js
@@ -0,0 +1,126 @@
+import React from "react";
+import { Flex, Box, Label, Input, Checkbox, IconButton } from "theme-ui";
+
+import ExpandMoreIcon from "../../icons/ExpandMoreIcon";
+
+function MapSettings({
+ map,
+ mapState,
+ onSettingsChange,
+ onStateSettingsChange,
+ showMore,
+ onShowMoreChange,
+}) {
+ function handleFlagChange(event, flag) {
+ if (event.target.checked) {
+ onStateSettingsChange("editFlags", [...mapState.editFlags, flag]);
+ } else {
+ onStateSettingsChange(
+ "editFlags",
+ mapState.editFlags.filter((f) => f !== flag)
+ );
+ }
+ }
+
+ return (
+
+
+
+
+
+ onSettingsChange("gridX", parseInt(e.target.value))
+ }
+ disabled={map === null || map.type === "default"}
+ min={1}
+ my={1}
+ />
+
+
+
+
+ onSettingsChange("gridY", parseInt(e.target.value))
+ }
+ disabled={map === null || map.type === "default"}
+ min={1}
+ my={1}
+ />
+
+
+ {showMore && (
+ <>
+
+
+
+
+
+
+
+
+
+
+ onSettingsChange("name", e.target.value)}
+ disabled={map === null || map.type === "default"}
+ my={1}
+ />
+
+ >
+ )}
+ {
+ e.stopPropagation();
+ e.preventDefault();
+ onShowMoreChange(!showMore);
+ }}
+ sx={{
+ transform: `rotate(${showMore ? "180deg" : "0"})`,
+ alignSelf: "center",
+ }}
+ aria-label={showMore ? "Show Less" : "Show More"}
+ title={showMore ? "Show Less" : "Show More"}
+ disabled={map === null}
+ >
+
+
+
+ );
+}
+
+export default MapSettings;
diff --git a/src/components/map/MapTile.js b/src/components/map/MapTile.js
new file mode 100644
index 0000000..38c4989
--- /dev/null
+++ b/src/components/map/MapTile.js
@@ -0,0 +1,173 @@
+import React, { useState } from "react";
+import { Flex, Image as UIImage, IconButton, Box, Text } from "theme-ui";
+
+import RemoveMapIcon from "../../icons/RemoveMapIcon";
+import ResetMapIcon from "../../icons/ResetMapIcon";
+import ExpandMoreDotIcon from "../../icons/ExpandMoreDotIcon";
+
+import useDataSource from "../../helpers/useDataSource";
+import { mapSources as defaultMapSources } from "../../maps";
+
+function MapTile({
+ map,
+ mapState,
+ isSelected,
+ onMapSelect,
+ onMapRemove,
+ onMapReset,
+ onSubmit,
+}) {
+ const mapSource = useDataSource(map, defaultMapSources);
+ const [isMapTileMenuOpen, setIsTileMenuOpen] = useState(false);
+ const isDefault = map.type === "default";
+ const hasMapState =
+ mapState &&
+ (Object.values(mapState.tokens).length > 0 ||
+ mapState.mapDrawActions.length > 0 ||
+ mapState.fogDrawActions.length > 0);
+
+ const expandButton = (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ setIsTileMenuOpen(true);
+ }}
+ bg="overlay"
+ sx={{ borderRadius: "50%" }}
+ m={1}
+ >
+
+
+ );
+
+ function removeButton(map) {
+ return (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ setIsTileMenuOpen(false);
+ onMapRemove(map.id);
+ }}
+ bg="overlay"
+ sx={{ borderRadius: "50%" }}
+ m={1}
+ >
+
+
+ );
+ }
+
+ function resetButton(map) {
+ return (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ setIsTileMenuOpen(false);
+ onMapReset(map.id);
+ }}
+ bg="overlay"
+ sx={{ borderRadius: "50%" }}
+ m={1}
+ >
+
+
+ );
+ }
+
+ return (
+ {
+ setIsTileMenuOpen(false);
+ if (!isSelected) {
+ onMapSelect(map);
+ }
+ }}
+ onDoubleClick={(e) => {
+ if (!isMapTileMenuOpen) {
+ onSubmit(e);
+ }
+ }}
+ >
+
+
+
+ {map.name}
+
+
+ {/* Show expand button only if both reset and remove is available */}
+ {isSelected && (
+
+ {isDefault && hasMapState && resetButton(map)}
+ {!isDefault && hasMapState && !isMapTileMenuOpen && expandButton}
+ {!isDefault && !hasMapState && removeButton(map)}
+
+ )}
+ {/* Tile menu for two actions */}
+ {!isDefault && isMapTileMenuOpen && isSelected && (
+ setIsTileMenuOpen(false)}
+ >
+ {!isDefault && removeButton(map)}
+ {hasMapState && resetButton(map)}
+
+ )}
+
+ );
+}
+
+export default MapTile;
diff --git a/src/components/map/MapTiles.js b/src/components/map/MapTiles.js
new file mode 100644
index 0000000..5babba3
--- /dev/null
+++ b/src/components/map/MapTiles.js
@@ -0,0 +1,75 @@
+import React from "react";
+import { Flex } from "theme-ui";
+import SimpleBar from "simplebar-react";
+
+import AddIcon from "../../icons/AddIcon";
+
+import MapTile from "./MapTile";
+
+function MapTiles({
+ maps,
+ selectedMap,
+ selectedMapState,
+ onMapSelect,
+ onMapAdd,
+ onMapRemove,
+ onMapReset,
+ onSubmit,
+}) {
+ return (
+
+
+
+
+
+ {maps.map((map) => (
+
+ ))}
+
+
+ );
+}
+
+export default MapTiles;
diff --git a/src/components/MapToken.js b/src/components/map/MapToken.js
similarity index 54%
rename from src/components/MapToken.js
rename to src/components/map/MapToken.js
index 0923478..6d8309a 100644
--- a/src/components/MapToken.js
+++ b/src/components/map/MapToken.js
@@ -1,12 +1,17 @@
import React, { useRef } from "react";
import { Box, Image } from "theme-ui";
-import TokenLabel from "./TokenLabel";
-import TokenStatus from "./TokenStatus";
+import TokenLabel from "../token/TokenLabel";
+import TokenStatus from "../token/TokenStatus";
-import usePreventTouch from "../helpers/usePreventTouch";
+import usePreventTouch from "../../helpers/usePreventTouch";
+import useDataSource from "../../helpers/useDataSource";
+
+import { tokenSources } from "../../tokens";
+
+function MapToken({ token, tokenState, tokenSizePercent, className }) {
+ const imageSource = useDataSource(token, tokenSources);
-function MapToken({ token, tokenSizePercent, className }) {
const imageRef = useRef();
// Stop touch to prevent 3d touch gesutre on iOS
usePreventTouch(imageRef);
@@ -14,7 +19,7 @@ function MapToken({ token, tokenSizePercent, className }) {
return (
- {token.status && }
- {token.label && }
+ {tokenState.statuses && (
+
+ )}
+ {tokenState.label && }
diff --git a/src/components/map/SelectMapButton.js b/src/components/map/SelectMapButton.js
new file mode 100644
index 0000000..2e9dc87
--- /dev/null
+++ b/src/components/map/SelectMapButton.js
@@ -0,0 +1,41 @@
+import React, { useState } from "react";
+import { IconButton } from "theme-ui";
+
+import SelectMapModal from "../../modals/SelectMapModal";
+import SelectMapIcon from "../../icons/SelectMapIcon";
+
+function SelectMapButton({ onMapChange, onMapStateChange, currentMap }) {
+ const [isAddModalOpen, setIsAddModalOpen] = useState(false);
+ function openModal() {
+ setIsAddModalOpen(true);
+ }
+ function closeModal() {
+ setIsAddModalOpen(false);
+ }
+
+ function handleDone() {
+ closeModal();
+ }
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default SelectMapButton;
diff --git a/src/components/map/controls/AlphaBlendToggle.js b/src/components/map/controls/AlphaBlendToggle.js
new file mode 100644
index 0000000..ccc2065
--- /dev/null
+++ b/src/components/map/controls/AlphaBlendToggle.js
@@ -0,0 +1,19 @@
+import React from "react";
+import { IconButton } from "theme-ui";
+
+import BlendOnIcon from "../../../icons/BlendOnIcon";
+import BlendOffIcon from "../../../icons/BlendOffIcon";
+
+function AlphaBlendToggle({ useBlending, onBlendingChange }) {
+ return (
+ onBlendingChange(!useBlending)}
+ >
+ {useBlending ? : }
+
+ );
+}
+
+export default AlphaBlendToggle;
diff --git a/src/components/map/controls/BrushToolSettings.js b/src/components/map/controls/BrushToolSettings.js
new file mode 100644
index 0000000..df24bb1
--- /dev/null
+++ b/src/components/map/controls/BrushToolSettings.js
@@ -0,0 +1,61 @@
+import React from "react";
+import { Flex } from "theme-ui";
+
+import ColorControl from "./ColorControl";
+import AlphaBlendToggle from "./AlphaBlendToggle";
+import RadioIconButton from "./RadioIconButton";
+
+import BrushStrokeIcon from "../../../icons/BrushStrokeIcon";
+import BrushFillIcon from "../../../icons/BrushFillIcon";
+
+import UndoButton from "./UndoButton";
+import RedoButton from "./RedoButton";
+
+import Divider from "./Divider";
+
+function BrushToolSettings({
+ settings,
+ onSettingChange,
+ onToolAction,
+ disabledActions,
+}) {
+ return (
+
+ onSettingChange({ color })}
+ />
+
+ onSettingChange({ type: "stroke" })}
+ isSelected={settings.type === "stroke"}
+ >
+
+
+ onSettingChange({ type: "fill" })}
+ isSelected={settings.type === "fill"}
+ >
+
+
+
+ onSettingChange({ useBlending })}
+ />
+
+ onToolAction("mapUndo")}
+ disabled={disabledActions.includes("undo")}
+ />
+ onToolAction("mapRedo")}
+ disabled={disabledActions.includes("redo")}
+ />
+
+ );
+}
+
+export default BrushToolSettings;
diff --git a/src/components/map/controls/ColorControl.js b/src/components/map/controls/ColorControl.js
new file mode 100644
index 0000000..d9264f1
--- /dev/null
+++ b/src/components/map/controls/ColorControl.js
@@ -0,0 +1,107 @@
+import React, { useState } from "react";
+import { Box } from "theme-ui";
+
+import colors, { colorOptions } from "../../../helpers/colors";
+import MapMenu from "../MapMenu";
+
+function ColorCircle({ color, selected, onClick, sx }) {
+ return (
+
+ {selected && (
+
+ )}
+
+ );
+}
+
+function ColorControl({ color, onColorChange }) {
+ const [showColorMenu, setShowColorMenu] = useState(false);
+ const [colorMenuOptions, setColorMenuOptions] = useState({});
+
+ function handleControlClick(event) {
+ if (showColorMenu) {
+ setShowColorMenu(false);
+ setColorMenuOptions({});
+ } else {
+ setShowColorMenu(true);
+ const rect = event.target.getBoundingClientRect();
+ setColorMenuOptions({
+ // Align the right of the submenu to the left of the tool and center vertically
+ left: `${rect.left + rect.width / 2}px`,
+ top: `${rect.bottom + 16}px`,
+ style: { transform: "translateX(-50%)" },
+ // Exclude this node from the sub menus auto close
+ excludeNode: event.target,
+ });
+ }
+ }
+
+ const colorMenu = (
+ {
+ setShowColorMenu(false);
+ setColorMenuOptions({});
+ }}
+ {...colorMenuOptions}
+ >
+
+ {colorOptions.map((c) => (
+ {
+ onColorChange(c);
+ setShowColorMenu(false);
+ setColorMenuOptions({});
+ }}
+ sx={{ width: "25%", paddingTop: "25%" }}
+ />
+ ))}
+
+
+ );
+
+ return (
+ <>
+
+ {colorMenu}
+ >
+ );
+}
+
+export default ColorControl;
diff --git a/src/components/map/controls/Divider.js b/src/components/map/controls/Divider.js
new file mode 100644
index 0000000..485e2d6
--- /dev/null
+++ b/src/components/map/controls/Divider.js
@@ -0,0 +1,24 @@
+import React from "react";
+import { Divider } from "theme-ui";
+
+function StyledDivider({ vertical }) {
+ return (
+
+ );
+}
+
+StyledDivider.defaultProps = {
+ vertical: false,
+};
+
+export default StyledDivider;
diff --git a/src/components/map/controls/EdgeSnappingToggle.js b/src/components/map/controls/EdgeSnappingToggle.js
new file mode 100644
index 0000000..addd219
--- /dev/null
+++ b/src/components/map/controls/EdgeSnappingToggle.js
@@ -0,0 +1,21 @@
+import React from "react";
+import { IconButton } from "theme-ui";
+
+import SnappingOnIcon from "../../../icons/SnappingOnIcon";
+import SnappingOffIcon from "../../../icons/SnappingOffIcon";
+
+function EdgeSnappingToggle({ useEdgeSnapping, onEdgeSnappingChange }) {
+ return (
+ onEdgeSnappingChange(!useEdgeSnapping)}
+ >
+ {useEdgeSnapping ? : }
+
+ );
+}
+
+export default EdgeSnappingToggle;
diff --git a/src/components/map/controls/EraseToolSettings.js b/src/components/map/controls/EraseToolSettings.js
new file mode 100644
index 0000000..0660570
--- /dev/null
+++ b/src/components/map/controls/EraseToolSettings.js
@@ -0,0 +1,34 @@
+import React from "react";
+import { Flex, IconButton } from "theme-ui";
+
+import EraseAllIcon from "../../../icons/EraseAllIcon";
+
+import UndoButton from "./UndoButton";
+import RedoButton from "./RedoButton";
+
+import Divider from "./Divider";
+
+function EraseToolSettings({ onToolAction, disabledActions }) {
+ return (
+
+ onToolAction("eraseAll")}
+ >
+
+
+
+ onToolAction("mapUndo")}
+ disabled={disabledActions.includes("undo")}
+ />
+ onToolAction("mapRedo")}
+ disabled={disabledActions.includes("redo")}
+ />
+
+ );
+}
+
+export default EraseToolSettings;
diff --git a/src/components/map/controls/FogToolSettings.js b/src/components/map/controls/FogToolSettings.js
new file mode 100644
index 0000000..04183d3
--- /dev/null
+++ b/src/components/map/controls/FogToolSettings.js
@@ -0,0 +1,72 @@
+import React from "react";
+import { Flex } from "theme-ui";
+
+import EdgeSnappingToggle from "./EdgeSnappingToggle";
+import RadioIconButton from "./RadioIconButton";
+import GridSnappingToggle from "./GridSnappingToggle";
+
+import FogAddIcon from "../../../icons/FogAddIcon";
+import FogRemoveIcon from "../../../icons/FogRemoveIcon";
+import FogToggleIcon from "../../../icons/FogToggleIcon";
+
+import UndoButton from "./UndoButton";
+import RedoButton from "./RedoButton";
+
+import Divider from "./Divider";
+
+function BrushToolSettings({
+ settings,
+ onSettingChange,
+ onToolAction,
+ disabledActions,
+}) {
+ return (
+
+ onSettingChange({ type: "add" })}
+ isSelected={settings.type === "add"}
+ >
+
+
+ onSettingChange({ type: "remove" })}
+ isSelected={settings.type === "remove"}
+ >
+
+
+ onSettingChange({ type: "toggle" })}
+ isSelected={settings.type === "toggle"}
+ >
+
+
+
+
+ onSettingChange({ useEdgeSnapping })
+ }
+ />
+
+ onSettingChange({ useGridSnapping })
+ }
+ />
+
+ onToolAction("fogUndo")}
+ disabled={disabledActions.includes("undo")}
+ />
+ onToolAction("fogRedo")}
+ disabled={disabledActions.includes("redo")}
+ />
+
+ );
+}
+
+export default BrushToolSettings;
diff --git a/src/components/map/controls/GridSnappingToggle.js b/src/components/map/controls/GridSnappingToggle.js
new file mode 100644
index 0000000..b09232a
--- /dev/null
+++ b/src/components/map/controls/GridSnappingToggle.js
@@ -0,0 +1,21 @@
+import React from "react";
+import { IconButton } from "theme-ui";
+
+import GridOnIcon from "../../../icons/GridOnIcon";
+import GridOffIcon from "../../../icons/GridOffIcon";
+
+function GridSnappingToggle({ useGridSnapping, onGridSnappingChange }) {
+ return (
+ onGridSnappingChange(!useGridSnapping)}
+ >
+ {useGridSnapping ? : }
+
+ );
+}
+
+export default GridSnappingToggle;
diff --git a/src/components/map/controls/RadioIconButton.js b/src/components/map/controls/RadioIconButton.js
new file mode 100644
index 0000000..14579bc
--- /dev/null
+++ b/src/components/map/controls/RadioIconButton.js
@@ -0,0 +1,22 @@
+import React from "react";
+import { IconButton } from "theme-ui";
+
+function RadioIconButton({ title, onClick, isSelected, disabled, children }) {
+ return (
+
+ {children}
+
+ );
+}
+
+RadioIconButton.defaultProps = {
+ disabled: false,
+};
+
+export default RadioIconButton;
diff --git a/src/components/map/controls/RedoButton.js b/src/components/map/controls/RedoButton.js
new file mode 100644
index 0000000..e233143
--- /dev/null
+++ b/src/components/map/controls/RedoButton.js
@@ -0,0 +1,14 @@
+import React from "react";
+import { IconButton } from "theme-ui";
+
+import RedoIcon from "../../../icons/RedoIcon";
+
+function RedoButton({ onClick, disabled }) {
+ return (
+
+
+
+ );
+}
+
+export default RedoButton;
diff --git a/src/components/map/controls/ShapeToolSettings.js b/src/components/map/controls/ShapeToolSettings.js
new file mode 100644
index 0000000..e4a8328
--- /dev/null
+++ b/src/components/map/controls/ShapeToolSettings.js
@@ -0,0 +1,69 @@
+import React from "react";
+import { Flex } from "theme-ui";
+
+import ColorControl from "./ColorControl";
+import AlphaBlendToggle from "./AlphaBlendToggle";
+import RadioIconButton from "./RadioIconButton";
+
+import ShapeRectangleIcon from "../../../icons/ShapeRectangleIcon";
+import ShapeCircleIcon from "../../../icons/ShapeCircleIcon";
+import ShapeTriangleIcon from "../../../icons/ShapeTriangleIcon";
+
+import UndoButton from "./UndoButton";
+import RedoButton from "./RedoButton";
+
+import Divider from "./Divider";
+
+function ShapeToolSettings({
+ settings,
+ onSettingChange,
+ onToolAction,
+ disabledActions,
+}) {
+ return (
+
+ onSettingChange({ color })}
+ />
+
+ onSettingChange({ type: "rectangle" })}
+ isSelected={settings.type === "rectangle"}
+ >
+
+
+ onSettingChange({ type: "circle" })}
+ isSelected={settings.type === "circle"}
+ >
+
+
+ onSettingChange({ type: "triangle" })}
+ isSelected={settings.type === "triangle"}
+ >
+
+
+
+ onSettingChange({ useBlending })}
+ />
+
+ onToolAction("mapUndo")}
+ disabled={disabledActions.includes("undo")}
+ />
+ onToolAction("mapRedo")}
+ disabled={disabledActions.includes("redo")}
+ />
+
+ );
+}
+
+export default ShapeToolSettings;
diff --git a/src/components/map/controls/UndoButton.js b/src/components/map/controls/UndoButton.js
new file mode 100644
index 0000000..88d0d26
--- /dev/null
+++ b/src/components/map/controls/UndoButton.js
@@ -0,0 +1,14 @@
+import React from "react";
+import { IconButton } from "theme-ui";
+
+import UndoIcon from "../../../icons/UndoIcon";
+
+function UndoButton({ onClick, disabled }) {
+ return (
+
+
+
+ );
+}
+
+export default UndoButton;
diff --git a/src/components/AddPartyMemberButton.js b/src/components/party/AddPartyMemberButton.js
similarity index 84%
rename from src/components/AddPartyMemberButton.js
rename to src/components/party/AddPartyMemberButton.js
index b26dd7f..b2932b1 100644
--- a/src/components/AddPartyMemberButton.js
+++ b/src/components/party/AddPartyMemberButton.js
@@ -1,8 +1,8 @@
import React, { useState } from "react";
import { IconButton } from "theme-ui";
-import AddPartyMemberModal from "../modals/AddPartyMemberModal";
-import AddPartyMemberIcon from "../icons/AddPartyMemberIcon";
+import AddPartyMemberModal from "../../modals/AddPartyMemberModal";
+import AddPartyMemberIcon from "../../icons/AddPartyMemberIcon";
function AddPartyMemberButton({ gameId }) {
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
diff --git a/src/components/ChangeNicknameButton.js b/src/components/party/ChangeNicknameButton.js
similarity index 75%
rename from src/components/ChangeNicknameButton.js
rename to src/components/party/ChangeNicknameButton.js
index 4b4194b..20231af 100644
--- a/src/components/ChangeNicknameButton.js
+++ b/src/components/party/ChangeNicknameButton.js
@@ -1,8 +1,8 @@
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
import { IconButton } from "theme-ui";
-import ChangeNicknameModal from "../modals/ChangeNicknameModal";
-import ChangeNicknameIcon from "../icons/ChangeNicknameIcon";
+import ChangeNicknameModal from "../../modals/ChangeNicknameModal";
+import ChangeNicknameIcon from "../../icons/ChangeNicknameIcon";
function ChangeNicknameButton({ nickname, onChange }) {
const [isChangeModalOpen, setIsChangeModalOpen] = useState(false);
@@ -13,7 +13,12 @@ function ChangeNicknameButton({ nickname, onChange }) {
setIsChangeModalOpen(false);
}
- const [changedNickname, setChangedNickname] = useState(nickname);
+ const [changedNickname, setChangedNickname] = useState("");
+
+ useEffect(() => {
+ setChangedNickname(nickname);
+ }, [nickname]);
+
function handleChangeSubmit(event) {
event.preventDefault();
onChange(changedNickname);
diff --git a/src/components/Nickname.js b/src/components/party/Nickname.js
similarity index 100%
rename from src/components/Nickname.js
rename to src/components/party/Nickname.js
diff --git a/src/components/Party.js b/src/components/party/Party.js
similarity index 95%
rename from src/components/Party.js
rename to src/components/party/Party.js
index 59329d1..e2c436d 100644
--- a/src/components/Party.js
+++ b/src/components/party/Party.js
@@ -5,6 +5,7 @@ import AddPartyMemberButton from "./AddPartyMemberButton";
import Nickname from "./Nickname";
import ChangeNicknameButton from "./ChangeNicknameButton";
import StartStreamButton from "./StartStreamButton";
+import SettingsButton from "../SettingsButton";
function Party({
nickname,
@@ -54,12 +55,13 @@ function Party({
+
-
+
);
diff --git a/src/components/StartStreamButton.js b/src/components/party/StartStreamButton.js
similarity index 96%
rename from src/components/StartStreamButton.js
rename to src/components/party/StartStreamButton.js
index 12e4b15..b55020f 100644
--- a/src/components/StartStreamButton.js
+++ b/src/components/party/StartStreamButton.js
@@ -2,9 +2,9 @@ import React, { useState } from "react";
import { IconButton, Box, Text } from "theme-ui";
import adapter from "webrtc-adapter";
-import Link from "../components/Link";
+import Link from "../Link";
-import StartStreamModal from "../modals/StartStreamModal";
+import StartStreamModal from "../../modals/StartStreamModal";
function StartStreamButton({ onStreamStart, onStreamEnd, stream }) {
const [isStreamModalOpoen, setIsStreamModalOpen] = useState(false);
diff --git a/src/components/Stream.js b/src/components/party/Stream.js
similarity index 99%
rename from src/components/Stream.js
rename to src/components/party/Stream.js
index 4583fea..beccd0a 100644
--- a/src/components/Stream.js
+++ b/src/components/party/Stream.js
@@ -1,7 +1,7 @@
import React, { useState, useRef, useEffect } from "react";
import { Text, IconButton, Box } from "theme-ui";
-import Banner from "./Banner";
+import Banner from "../Banner";
function Stream({ stream, nickname }) {
const [streamMuted, setStreamMuted] = useState(false);
diff --git a/src/components/token/ListToken.js b/src/components/token/ListToken.js
new file mode 100644
index 0000000..bbdb3a2
--- /dev/null
+++ b/src/components/token/ListToken.js
@@ -0,0 +1,30 @@
+import React, { useRef } from "react";
+import { Box, Image } from "theme-ui";
+
+import usePreventTouch from "../../helpers/usePreventTouch";
+import useDataSource from "../../helpers/useDataSource";
+
+import { tokenSources } from "../../tokens";
+
+function ListToken({ token, className }) {
+ const imageSource = useDataSource(token, tokenSources);
+
+ const imageRef = useRef();
+ // Stop touch to prevent 3d touch gesutre on iOS
+ usePreventTouch(imageRef);
+
+ return (
+
+
+
+ );
+}
+
+export default ListToken;
diff --git a/src/components/ProxyToken.js b/src/components/token/ProxyToken.js
similarity index 68%
rename from src/components/ProxyToken.js
rename to src/components/token/ProxyToken.js
index f10c81c..0ed984d 100644
--- a/src/components/ProxyToken.js
+++ b/src/components/token/ProxyToken.js
@@ -3,19 +3,46 @@ import ReactDOM from "react-dom";
import { Image, Box } from "theme-ui";
import interact from "interactjs";
-import usePortal from "../helpers/usePortal";
+import usePortal from "../../helpers/usePortal";
import TokenLabel from "./TokenLabel";
import TokenStatus from "./TokenStatus";
-function ProxyToken({ tokenClassName, onProxyDragEnd }) {
+/**
+ * @callback onProxyDragEnd
+ * @param {boolean} isOnMap whether the token was dropped on the map
+ * @param {Object} token the token that was dropped
+ */
+
+/**
+ *
+ * @param {string} tokenClassName The class name to attach the interactjs handler to
+ * @param {onProxyDragEnd} onProxyDragEnd Called when the proxy token is dropped
+ * @param {Object} tokens An optional mapping of tokens to use as a base when calling OnProxyDragEnd
+ * @param {Object} disabledTokens An optional mapping of tokens that shouldn't allow movement
+
+ */
+function ProxyToken({
+ tokenClassName,
+ onProxyDragEnd,
+ tokens,
+ disabledTokens,
+}) {
const proxyContainer = usePortal("root");
const [imageSource, setImageSource] = useState("");
- const [label, setLabel] = useState("");
- const [status, setStatus] = useState("");
+ const [tokenId, setTokenId] = useState(null);
const proxyRef = useRef();
+ // Store the tokens in a ref and access in the interactjs loop
+ // This is needed to stop interactjs from creating multiple listeners
+ const tokensRef = useRef(tokens);
+ const disabledTokensRef = useRef(disabledTokens);
+ useEffect(() => {
+ tokensRef.current = tokens;
+ disabledTokensRef.current = disabledTokens;
+ }, [tokens, disabledTokens]);
+
const proxyOnMap = useRef(false);
useEffect(() => {
@@ -23,11 +50,15 @@ function ProxyToken({ tokenClassName, onProxyDragEnd }) {
listeners: {
start: (event) => {
let target = event.target;
+ const id = target.dataset.id;
+ if (id in disabledTokensRef.current) {
+ return;
+ }
+
// Hide the token and copy it's image to the proxy
target.parentElement.style.opacity = "0.25";
setImageSource(target.src);
- setLabel(target.dataset.label || "");
- setStatus(target.dataset.status || "");
+ setTokenId(id);
let proxy = proxyRef.current;
if (proxy) {
@@ -73,10 +104,14 @@ function ProxyToken({ tokenClassName, onProxyDragEnd }) {
end: (event) => {
let target = event.target;
+ const id = target.dataset.id;
+ if (id in disabledTokensRef.current) {
+ return;
+ }
let proxy = proxyRef.current;
if (proxy) {
- if (onProxyDragEnd) {
- const mapImage = document.querySelector(".mapImage");
+ const mapImage = document.querySelector(".mapImage");
+ if (onProxyDragEnd && mapImage) {
const mapImageRect = mapImage.getBoundingClientRect();
let x = parseFloat(proxy.getAttribute("data-x")) || 0;
@@ -88,13 +123,13 @@ function ProxyToken({ tokenClassName, onProxyDragEnd }) {
x = x / (mapImageRect.right - mapImageRect.left);
y = y / (mapImageRect.bottom - mapImageRect.top);
- target.setAttribute("data-x", x);
- target.setAttribute("data-y", y);
+ // Get the token from the supplied tokens if it exists
+ const token = tokensRef.current[id] || {};
onProxyDragEnd(proxyOnMap.current, {
- image: target.src,
- // Pass in props stored as data- in the dom node
- ...target.dataset,
+ ...token,
+ x,
+ y,
});
}
@@ -140,12 +175,21 @@ function ProxyToken({ tokenClassName, onProxyDragEnd }) {
width: "100%",
}}
/>
- {status && }
- {label && }
+ {tokens[tokenId] && tokens[tokenId].statuses && (
+
+ )}
+ {tokens[tokenId] && tokens[tokenId].label && (
+
+ )}
,
proxyContainer
);
}
+ProxyToken.defaultProps = {
+ tokens: {},
+ disabledTokens: {},
+};
+
export default ProxyToken;
diff --git a/src/components/TokenLabel.js b/src/components/token/TokenLabel.js
similarity index 92%
rename from src/components/TokenLabel.js
rename to src/components/token/TokenLabel.js
index 91ac687..b5d90fb 100644
--- a/src/components/TokenLabel.js
+++ b/src/components/token/TokenLabel.js
@@ -1,7 +1,7 @@
import React from "react";
import { Image, Box, Text } from "theme-ui";
-import tokenLabel from "../images/TokenLabel.png";
+import tokenLabel from "../../images/TokenLabel.png";
function TokenLabel({ label }) {
return (
@@ -39,6 +39,7 @@ function TokenLabel({ label }) {
verticalAlign: "middle",
lineHeight: 1.4,
}}
+ color="hsl(210, 50%, 96%)"
>
{label}
diff --git a/src/components/TokenMenu.js b/src/components/token/TokenMenu.js
similarity index 75%
rename from src/components/TokenMenu.js
rename to src/components/token/TokenMenu.js
index dc8f1c1..d64063a 100644
--- a/src/components/TokenMenu.js
+++ b/src/components/token/TokenMenu.js
@@ -1,18 +1,39 @@
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useState, useRef } from "react";
import interact from "interactjs";
import { Box, Input } from "theme-ui";
-import MapMenu from "./MapMenu";
+import MapMenu from "../map/MapMenu";
-import colors, { colorOptions } from "../helpers/colors";
+import colors, { colorOptions } from "../../helpers/colors";
-function TokenMenu({ tokenClassName, onTokenChange }) {
+/**
+ * @callback onTokenChange
+ * @param {Object} token the token that was changed
+ */
+
+/**
+ *
+ * @param {string} tokenClassName The class name to attach the interactjs handler to
+ * @param {onProxyDragEnd} onTokenChange Called when the the token data is changed
+ * @param {Object} tokens An mapping of tokens to use as a base when calling onTokenChange
+ * @param {Object} disabledTokens An optional mapping of tokens that shouldn't allow interaction
+ */
+function TokenMenu({ tokenClassName, onTokenChange, tokens, disabledTokens }) {
const [isOpen, setIsOpen] = useState(false);
function handleRequestClose() {
setIsOpen(false);
}
+ // Store the tokens in a ref and access in the interactjs loop
+ // This is needed to stop interactjs from creating multiple listeners
+ const tokensRef = useRef(tokens);
+ const disabledTokensRef = useRef(disabledTokens);
+ useEffect(() => {
+ tokensRef.current = tokens;
+ disabledTokensRef.current = disabledTokens;
+ }, [tokens, disabledTokens]);
+
const [currentToken, setCurrentToken] = useState({});
const [menuLeft, setMenuLeft] = useState(0);
const [menuTop, setMenuTop] = useState(0);
@@ -31,30 +52,29 @@ function TokenMenu({ tokenClassName, onTokenChange }) {
}
function handleStatusChange(status) {
- const statuses =
- currentToken.status.split(" ").filter((s) => s !== "") || [];
+ const statuses = currentToken.statuses;
let newStatuses = [];
if (statuses.includes(status)) {
newStatuses = statuses.filter((s) => s !== status);
} else {
newStatuses = [...statuses, status];
}
- const newStatus = newStatuses.join(" ");
setCurrentToken((prevToken) => ({
...prevToken,
- status: newStatus,
+ statuses: newStatuses,
}));
- onTokenChange({ ...currentToken, status: newStatus });
+ onTokenChange({ ...currentToken, statuses: newStatuses });
}
useEffect(() => {
function handleTokenMenuOpen(event) {
const target = event.target;
- const dataset = (target && target.dataset) || {};
- setCurrentToken({
- image: target.src,
- ...dataset,
- });
+ const id = target.getAttribute("data-id");
+ if (id in disabledTokensRef.current) {
+ return;
+ }
+ const token = tokensRef.current[id] || {};
+ setCurrentToken(token);
const targetRect = target.getBoundingClientRect();
setMenuLeft(targetRect.left);
@@ -162,7 +182,7 @@ function TokenMenu({ tokenClassName, onTokenChange }) {
onClick={() => handleStatusChange(color)}
aria-label={`Token label Color ${color}`}
>
- {currentToken.status && currentToken.status.includes(color) && (
+ {currentToken.statuses && currentToken.statuses.includes(color) && (
- {Object.entries(tokens).map(([id, image]) => (
-
-
-
+ {tokens.map((token) => (
+
))}
@@ -57,6 +65,7 @@ function Tokens({ onCreateMapToken }) {
[token.id, token]))}
/>
>
);
diff --git a/src/contexts/AuthContext.js b/src/contexts/AuthContext.js
index ace0b95..0d71b2c 100644
--- a/src/contexts/AuthContext.js
+++ b/src/contexts/AuthContext.js
@@ -1,4 +1,9 @@
import React, { useState, useEffect } from "react";
+import shortid from "shortid";
+
+import { getRandomMonster } from "../helpers/monsters";
+
+import db from "../database";
const AuthContext = React.createContext();
@@ -13,7 +18,48 @@ export function AuthProvider({ children }) {
const [authenticationStatus, setAuthenticationStatus] = useState("unknown");
+ const [userId, setUserId] = useState();
+ useEffect(() => {
+ async function loadUserId() {
+ const storedUserId = await db.table("user").get("userId");
+ if (storedUserId) {
+ setUserId(storedUserId.value);
+ } else {
+ const id = shortid.generate();
+ setUserId(id);
+ db.table("user").add({ key: "userId", value: id });
+ }
+ }
+
+ loadUserId();
+ }, []);
+
+ const [nickname, setNickname] = useState("");
+ useEffect(() => {
+ async function loadNickname() {
+ const storedNickname = await db.table("user").get("nickname");
+ if (storedNickname) {
+ setNickname(storedNickname.value);
+ } else {
+ const name = getRandomMonster();
+ setNickname(name);
+ db.table("user").add({ key: "nickname", value: name });
+ }
+ }
+
+ loadNickname();
+ }, []);
+
+ useEffect(() => {
+ if (nickname !== undefined) {
+ db.table("user").update("nickname", { value: nickname });
+ }
+ }, [nickname]);
+
const value = {
+ userId,
+ nickname,
+ setNickname,
password,
setPassword,
authenticationStatus,
diff --git a/src/contexts/MapInteractionContext.js b/src/contexts/MapInteractionContext.js
new file mode 100644
index 0000000..504c31e
--- /dev/null
+++ b/src/contexts/MapInteractionContext.js
@@ -0,0 +1,9 @@
+import React from "react";
+
+const MapInteractionContext = React.createContext({
+ translateRef: null,
+ scaleRef: null,
+});
+export const MapInteractionProvider = MapInteractionContext.Provider;
+
+export default MapInteractionContext;
diff --git a/src/database.js b/src/database.js
new file mode 100644
index 0000000..c1ad236
--- /dev/null
+++ b/src/database.js
@@ -0,0 +1,11 @@
+import Dexie from "dexie";
+
+const db = new Dexie("OwlbearRodeoDB");
+db.version(1).stores({
+ maps: "id, owner",
+ states: "mapId",
+ tokens: "id, owner",
+ user: "key",
+});
+
+export default db;
diff --git a/src/helpers/Peer.js b/src/helpers/Peer.js
index b3aa6e0..04d8893 100644
--- a/src/helpers/Peer.js
+++ b/src/helpers/Peer.js
@@ -1,8 +1,9 @@
import SimplePeer from "simple-peer";
import BinaryPack from "js-binarypack";
-import toBuffer from "blob-to-buffer";
import shortid from "shortid";
+import blobToBuffer from "./blobToBuffer";
+
// Limit buffer size to 16kb to avoid issues with chrome packet size
// http://viblast.com/blog/2015/2/5/webrtc-data-channel-message-size/
const MAX_BUFFER_SIZE = 16000;
@@ -40,13 +41,9 @@ class Peer extends SimplePeer {
});
}
- sendPackedData(packedData) {
- toBuffer(packedData, (error, buffer) => {
- if (error) {
- throw error;
- }
- super.send(buffer);
- });
+ async sendPackedData(packedData) {
+ const buffer = await blobToBuffer(packedData);
+ super.send(buffer);
}
send(data) {
diff --git a/src/helpers/blobToBuffer.js b/src/helpers/blobToBuffer.js
new file mode 100644
index 0000000..55eb6d7
--- /dev/null
+++ b/src/helpers/blobToBuffer.js
@@ -0,0 +1,13 @@
+async function blobToBuffer(blob) {
+ if (blob.arrayBuffer) {
+ const arrayBuffer = await blob.arrayBuffer();
+ const buffer = Buffer.from(arrayBuffer);
+ return buffer;
+ } else {
+ const arrayBuffer = await new Response(blob).arrayBuffer();
+ const buffer = Buffer.from(arrayBuffer);
+ return buffer;
+ }
+}
+
+export default blobToBuffer;
diff --git a/src/helpers/drawing.js b/src/helpers/drawing.js
new file mode 100644
index 0000000..89a192b
--- /dev/null
+++ b/src/helpers/drawing.js
@@ -0,0 +1,361 @@
+import simplify from "simplify-js";
+
+import * as Vector2 from "./vector2";
+import { toDegrees } from "./shared";
+import colors from "./colors";
+
+const snappingThreshold = 1 / 5;
+export function getBrushPositionForTool(
+ brushPosition,
+ tool,
+ toolSettings,
+ gridSize,
+ shapes
+) {
+ let position = brushPosition;
+ if (tool === "shape") {
+ const snapped = Vector2.roundTo(position, gridSize);
+ const minGrid = Vector2.min(gridSize);
+ const distance = Vector2.length(Vector2.subtract(snapped, position));
+ if (distance < minGrid * snappingThreshold) {
+ position = snapped;
+ }
+ }
+ if (tool === "fog" && toolSettings.type === "add") {
+ if (toolSettings.useGridSnapping) {
+ position = Vector2.roundTo(position, gridSize);
+ }
+ if (toolSettings.useEdgeSnapping) {
+ const minGrid = Vector2.min(gridSize);
+ let closestDistance = Number.MAX_VALUE;
+ let closestPosition = position;
+ // Find the closest point on all fog shapes
+ for (let shape of shapes) {
+ if (shape.type === "fog") {
+ const points = shape.data.points;
+ const isInShape = Vector2.pointInPolygon(position, points);
+ // Find the closest point to each line of the shape
+ for (let i = 0; i < points.length; i++) {
+ const a = points[i];
+ // Wrap around points to the start to account for closed shape
+ const b = points[(i + 1) % points.length];
+
+ const {
+ distance: distanceToLine,
+ point: pointOnLine,
+ } = Vector2.distanceToLine(position, a, b);
+ const isCloseToShape = distanceToLine < minGrid * snappingThreshold;
+ if (
+ (isInShape || isCloseToShape) &&
+ distanceToLine < closestDistance
+ ) {
+ closestPosition = pointOnLine;
+ closestDistance = distanceToLine;
+ }
+ }
+ }
+ }
+ position = closestPosition;
+ }
+ }
+
+ return position;
+}
+
+export function getDefaultShapeData(type, brushPosition) {
+ if (type === "circle") {
+ return { x: brushPosition.x, y: brushPosition.y, radius: 0 };
+ } else if (type === "rectangle") {
+ return {
+ x: brushPosition.x,
+ y: brushPosition.y,
+ width: 0,
+ height: 0,
+ };
+ } else if (type === "triangle") {
+ return {
+ points: [
+ { x: brushPosition.x, y: brushPosition.y },
+ { x: brushPosition.x, y: brushPosition.y },
+ { x: brushPosition.x, y: brushPosition.y },
+ ],
+ };
+ }
+}
+
+export function getGridScale(gridSize) {
+ if (gridSize.x < gridSize.y) {
+ return { x: gridSize.y / gridSize.x, y: 1 };
+ } else if (gridSize.y < gridSize.x) {
+ return { x: 1, y: gridSize.x / gridSize.y };
+ } else {
+ return { x: 1, y: 1 };
+ }
+}
+
+export function getUpdatedShapeData(type, data, brushPosition, gridSize) {
+ const gridScale = getGridScale(gridSize);
+ if (type === "circle") {
+ const dif = Vector2.subtract(brushPosition, {
+ x: data.x,
+ y: data.y,
+ });
+ const scaled = Vector2.multiply(dif, gridScale);
+ const distance = Vector2.length(scaled);
+ return {
+ ...data,
+ radius: distance,
+ };
+ } else if (type === "rectangle") {
+ const dif = Vector2.subtract(brushPosition, { x: data.x, y: data.y });
+ return {
+ ...data,
+ width: dif.x,
+ height: dif.y,
+ };
+ } else if (type === "triangle") {
+ const points = data.points;
+ const dif = Vector2.subtract(brushPosition, points[0]);
+ // Scale the distance by the grid scale then unscale before adding
+ const scaled = Vector2.multiply(dif, gridScale);
+ const length = Vector2.length(scaled);
+ const direction = Vector2.normalize(scaled);
+ // Get the angle for a triangle who's width is the same as it's length
+ const angle = Math.atan(length / 2 / length);
+ const sideLength = length / Math.cos(angle);
+
+ const leftDir = Vector2.rotateDirection(direction, toDegrees(angle));
+ const rightDir = Vector2.rotateDirection(direction, -toDegrees(angle));
+
+ const leftDirUnscaled = Vector2.divide(leftDir, gridScale);
+ const rightDirUnscaled = Vector2.divide(rightDir, gridScale);
+
+ return {
+ points: [
+ points[0],
+ Vector2.add(Vector2.multiply(leftDirUnscaled, sideLength), points[0]),
+ Vector2.add(Vector2.multiply(rightDirUnscaled, sideLength), points[0]),
+ ],
+ };
+ }
+}
+
+const defaultStrokeSize = 1 / 10;
+export function getStrokeSize(multiplier, gridSize, canvasWidth, canvasHeight) {
+ const gridPixelSize = Vector2.multiply(gridSize, {
+ x: canvasWidth,
+ y: canvasHeight,
+ });
+ return Vector2.min(gridPixelSize) * defaultStrokeSize * multiplier;
+}
+
+export function shapeHasFill(shape) {
+ return (
+ shape.type === "fog" ||
+ shape.type === "shape" ||
+ (shape.type === "path" && shape.pathType === "fill")
+ );
+}
+
+export function pointsToQuadraticBezier(points) {
+ const quadraticPoints = [];
+
+ // Draw a smooth curve between the points where each control point
+ // is the current point in the array and the next point is the center of
+ // the current point and the next point
+ for (let i = 1; i < points.length - 2; i++) {
+ const start = points[i - 1];
+ const controlPoint = points[i];
+ const next = points[i + 1];
+ const end = Vector2.divide(Vector2.add(controlPoint, next), 2);
+
+ quadraticPoints.push({ start, controlPoint, end });
+ }
+ // Curve through the last two points
+ quadraticPoints.push({
+ start: points[points.length - 2],
+ controlPoint: points[points.length - 1],
+ end: points[points.length - 1],
+ });
+
+ return quadraticPoints;
+}
+
+export function pointsToPathSmooth(points, close, canvasWidth, canvasHeight) {
+ const path = new Path2D();
+ if (points.length < 2) {
+ return path;
+ }
+ path.moveTo(points[0].x * canvasWidth, points[0].y * canvasHeight);
+
+ const quadraticPoints = pointsToQuadraticBezier(points);
+ for (let quadPoint of quadraticPoints) {
+ const pointScaled = Vector2.multiply(quadPoint.end, {
+ x: canvasWidth,
+ y: canvasHeight,
+ });
+ const controlScaled = Vector2.multiply(quadPoint.controlPoint, {
+ x: canvasWidth,
+ y: canvasHeight,
+ });
+ path.quadraticCurveTo(
+ controlScaled.x,
+ controlScaled.y,
+ pointScaled.x,
+ pointScaled.y
+ );
+ }
+
+ if (close) {
+ path.closePath();
+ }
+ return path;
+}
+
+export function pointsToPathSharp(points, close, canvasWidth, canvasHeight) {
+ const path = new Path2D();
+ path.moveTo(points[0].x * canvasWidth, points[0].y * canvasHeight);
+ for (let point of points.slice(1)) {
+ path.lineTo(point.x * canvasWidth, point.y * canvasHeight);
+ }
+ if (close) {
+ path.closePath();
+ }
+
+ return path;
+}
+
+export function circleToPath(x, y, radius, canvasWidth, canvasHeight) {
+ const path = new Path2D();
+ const minSide = canvasWidth < canvasHeight ? canvasWidth : canvasHeight;
+ path.arc(
+ x * canvasWidth,
+ y * canvasHeight,
+ radius * minSide,
+ 0,
+ 2 * Math.PI,
+ true
+ );
+ return path;
+}
+
+export function rectangleToPath(
+ x,
+ y,
+ width,
+ height,
+ canvasWidth,
+ canvasHeight
+) {
+ const path = new Path2D();
+ path.rect(
+ x * canvasWidth,
+ y * canvasHeight,
+ width * canvasWidth,
+ height * canvasHeight
+ );
+ return path;
+}
+
+export function shapeToPath(shape, canvasWidth, canvasHeight) {
+ const data = shape.data;
+ if (shape.type === "path") {
+ return pointsToPathSmooth(
+ data.points,
+ shape.pathType === "fill",
+ canvasWidth,
+ canvasHeight
+ );
+ } else if (shape.type === "shape") {
+ if (shape.shapeType === "circle") {
+ return circleToPath(
+ data.x,
+ data.y,
+ data.radius,
+ canvasWidth,
+ canvasHeight
+ );
+ } else if (shape.shapeType === "rectangle") {
+ return rectangleToPath(
+ data.x,
+ data.y,
+ data.width,
+ data.height,
+ canvasWidth,
+ canvasHeight
+ );
+ } else if (shape.shapeType === "triangle") {
+ return pointsToPathSharp(data.points, true, canvasWidth, canvasHeight);
+ }
+ } else if (shape.type === "fog") {
+ return pointsToPathSharp(
+ shape.data.points,
+ true,
+ canvasWidth,
+ canvasHeight
+ );
+ }
+}
+
+export function isShapeHovered(
+ shape,
+ context,
+ hoverPosition,
+ canvasWidth,
+ canvasHeight
+) {
+ const path = shapeToPath(shape, canvasWidth, canvasHeight);
+ if (shapeHasFill(shape)) {
+ return context.isPointInPath(
+ path,
+ hoverPosition.x * canvasWidth,
+ hoverPosition.y * canvasHeight
+ );
+ } else {
+ return context.isPointInStroke(
+ path,
+ hoverPosition.x * canvasWidth,
+ hoverPosition.y * canvasHeight
+ );
+ }
+}
+
+export function drawShape(shape, context, gridSize, canvasWidth, canvasHeight) {
+ const path = shapeToPath(shape, canvasWidth, canvasHeight);
+ const color = colors[shape.color] || shape.color;
+ const fill = shapeHasFill(shape);
+
+ context.globalAlpha = shape.blend ? 0.5 : 1.0;
+ context.fillStyle = color;
+ context.strokeStyle = color;
+ if (shape.strokeWidth > 0) {
+ context.lineCap = "round";
+ context.lineWidth = getStrokeSize(
+ shape.strokeWidth,
+ gridSize,
+ canvasWidth,
+ canvasHeight
+ );
+ context.stroke(path);
+ }
+ if (fill) {
+ context.fill(path);
+ }
+}
+
+const defaultSimplifySize = 1 / 100;
+export function simplifyPoints(points, gridSize, scale) {
+ return simplify(
+ points,
+ (Vector2.min(gridSize) * defaultSimplifySize) / scale
+ );
+}
+
+export function getRelativePointerPosition(event, container) {
+ if (container) {
+ const containerRect = container.getBoundingClientRect();
+ const x = (event.clientX - containerRect.x) / containerRect.width;
+ const y = (event.clientY - containerRect.y) / containerRect.height;
+ return { x, y };
+ }
+}
diff --git a/src/helpers/gestures.js b/src/helpers/gestures.js
deleted file mode 100644
index 9efe6fb..0000000
--- a/src/helpers/gestures.js
+++ /dev/null
@@ -1,98 +0,0 @@
-import ShapeDetector from "shape-detector";
-import simplify from "simplify-js";
-import { normalize, subtract, dot, length } from "./vector2";
-
-import gestures from "./gesturesData";
-
-const detector = new ShapeDetector(gestures);
-
-export function pointsToGesture(points) {
- return detector.spot(points).pattern;
-}
-
-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 };
-}
-
-function getTrianglePoints(points) {
- if (points.length < 3) {
- return points;
- }
-
- // Simplify edges up to the average distance between points
- let perimeterDistance = 0;
- for (let i = 0; i < points.length - 1; i++) {
- perimeterDistance += length(subtract(points[i + 1], points[i]));
- }
- const averagePointDistance = perimeterDistance / points.length;
- const simplifiedPoints = simplify(points, averagePointDistance);
-
- const edges = [];
- // Find edges of the simplified points that have the highest angular change
- for (let i = 0; i < simplifiedPoints.length; i++) {
- // Ensure index loops over to start and end of erray
- const prevIndex = i - 1 < 0 ? simplifiedPoints.length - i - 1 : i - 1;
- const nextIndex = (i + 1) % simplifiedPoints.length;
-
- const prev = normalize(
- subtract(simplifiedPoints[i], simplifiedPoints[prevIndex])
- );
- const next = normalize(
- subtract(simplifiedPoints[nextIndex], simplifiedPoints[i])
- );
-
- const similarity = dot(prev, next);
- if (similarity < 0.25) {
- edges.push({ similarity, point: simplifiedPoints[i] });
- }
- }
-
- edges.sort((a, b) => a.similarity - b.similarity);
- const trianglePoints = edges.slice(0, 3).map((edge) => edge.point);
- // Return the points with the highest angular change or fallback to a heuristic
- if (trianglePoints.length === 3) {
- return trianglePoints;
- } else {
- return [
- { x: points[0].x, y: points[0].y },
- {
- x: points[Math.floor(points.length / 2)].x,
- y: points[Math.floor(points.length / 2)].y,
- },
- { x: points[points.length - 1].x, y: points[points.length - 1].y },
- ];
- }
-}
-
-export function gestureToData(points, gesture) {
- const bounds = getBounds(points);
- const width = bounds.maxX - bounds.minX;
- const height = bounds.maxY - bounds.minY;
- const maxSide = width > height ? width : height;
- switch (gesture) {
- case "rectangle":
- return { x: bounds.minX, y: bounds.minY, width, height };
- case "triangle":
- return {
- points: getTrianglePoints(points),
- };
- case "circle":
- return {
- x: bounds.minX + width / 2,
- y: bounds.minY + height / 2,
- radius: maxSide / 2,
- };
- default:
- throw Error("Gesture not implemented");
- }
-}
diff --git a/src/helpers/gesturesData.js b/src/helpers/gesturesData.js
deleted file mode 100644
index 1c5dc5a..0000000
--- a/src/helpers/gesturesData.js
+++ /dev/null
@@ -1,1444 +0,0 @@
-export default [
- {
- points: [
- { x: 0.5197956577266922, y: 0.26954388894687403 },
- { x: 0.5197956577266922, y: 0.3309988518943743 },
- { x: 0.5210727969348659, y: 0.3283268969836134 },
- { x: 0.5274584929757343, y: 0.3256549420728525 },
- { x: 0.5644955300127714, y: 0.3256549420728525 },
- { x: 0.5632183908045977, y: 0.2762237762237762 },
- ],
- name: "rectangle",
- },
- {
- points: [
- { x: 0.5223499361430396, y: 0.3643982882788853 },
- { x: 0.5312899106002554, y: 0.3643982882788853 },
- { x: 0.5325670498084292, y: 0.3657342657342657 },
- { x: 0.5530012771392082, y: 0.3684062206450266 },
- { x: 0.5708812260536399, y: 0.3684062206450266 },
- { x: 0.5708812260536399, y: 0.37375013046654837 },
- { x: 0.5696040868454662, y: 0.37375013046654837 },
- { x: 0.5696040868454662, y: 0.3777580628326897 },
- { x: 0.5670498084291188, y: 0.3831019726542115 },
- { x: 0.5670498084291188, y: 0.39646174720801586 },
- { x: 0.5644955300127714, y: 0.39913370211877675 },
- { x: 0.5644955300127714, y: 0.4071495668510594 },
- { x: 0.5274584929757343, y: 0.4071495668510594 },
- { x: 0.5261813537675607, y: 0.40180565702953763 },
- { x: 0.5223499361430396, y: 0.39913370211877675 },
- { x: 0.5197956577266922, y: 0.3884458824757332 },
- ],
- name: "rectangle",
- },
- {
- points: [
- { x: 0.4355044699872286, y: 0.4111574992172007 },
- { x: 0.4355044699872286, y: 0.460588665066277 },
- { x: 0.438058748403576, y: 0.4579167101555161 },
- { x: 0.47509578544061304, y: 0.4579167101555161 },
- { x: 0.4763729246487867, y: 0.4525728003339944 },
- { x: 0.4763729246487867, y: 0.4124934766725811 },
- { x: 0.47509578544061304, y: 0.4124934766725811 },
- { x: 0.47509578544061304, y: 0.40581358939567896 },
- { x: 0.47126436781609193, y: 0.4044776119402985 },
- { x: 0.47126436781609193, y: 0.40581358939567896 },
- { x: 0.4648786717752235, y: 0.40848554430643985 },
- { x: 0.44572158365261816, y: 0.40848554430643985 },
- ],
- name: "rectangle",
- },
- {
- points: [
- { x: 0.3499361430395913, y: 0.5420832898444838 },
- { x: 0.43039591315453385, y: 0.5420832898444838 },
- { x: 0.43167305236270753, y: 0.5434192672998643 },
- { x: 0.43167305236270753, y: 0.5915144556935602 },
- { x: 0.36398467432950193, y: 0.5915144556935602 },
- { x: 0.36270753512132825, y: 0.5901784782381797 },
- { x: 0.34738186462324394, y: 0.5901784782381797 },
- ],
- name: "rectangle",
- },
- {
- points: [
- { x: 0.25925925925925924, y: 0.4552447552447552 },
- { x: 0.21583652618135377, y: 0.4552447552447552 },
- { x: 0.21583652618135377, y: 0.4579167101555161 },
- { x: 0.21839080459770116, y: 0.460588665066277 },
- { x: 0.21839080459770116, y: 0.49398810145078803 },
- { x: 0.21966794380587484, y: 0.49799603381692936 },
- { x: 0.22094508301404853, y: 0.4966600563615489 },
- { x: 0.24393358876117496, y: 0.4966600563615489 },
- { x: 0.24393358876117496, y: 0.49799603381692936 },
- { x: 0.24904214559386972, y: 0.4993320112723098 },
- { x: 0.26436781609195403, y: 0.4993320112723098 },
- { x: 0.26436781609195403, y: 0.5006679887276902 },
- ],
- name: "rectangle",
- },
- {
- points: [
- { x: 0.26053639846743293, y: 0.22812858783008036 },
- { x: 0.26053639846743293, y: 0.3283268969836134 },
- { x: 0.26181353767560667, y: 0.32966287443899384 },
- { x: 0.26181353767560667, y: 0.3403506940820374 },
- { x: 0.26309067688378035, y: 0.3456946039035591 },
- { x: 0.26436781609195403, y: 0.3456946039035591 },
- { x: 0.26436781609195403, y: 0.34970253626970044 },
- { x: 0.2656449553001277, y: 0.34970253626970044 },
- { x: 0.2669220945083014, y: 0.360390355912744 },
- { x: 0.26947637292464877, y: 0.360390355912744 },
- { x: 0.26947637292464877, y: 0.35905437845736354 },
- { x: 0.2835249042145594, y: 0.35905437845736354 },
- { x: 0.28991060025542786, y: 0.36172633336812443 },
- { x: 0.31800766283524906, y: 0.3630623108235049 },
- { x: 0.31928480204342274, y: 0.3643982882788853 },
- { x: 0.32950191570881227, y: 0.3643982882788853 },
- { x: 0.33077905491698595, y: 0.3657342657342657 },
- { x: 0.3448275862068966, y: 0.3657342657342657 },
- { x: 0.34355044699872284, y: 0.3309988518943743 },
- { x: 0.34610472541507026, y: 0.3176390773405699 },
- { x: 0.34610472541507026, y: 0.2588560693038305 },
- { x: 0.3448275862068966, y: 0.25484813693768915 },
- { x: 0.3448275862068966, y: 0.23347249765160213 },
- { x: 0.34355044699872284, y: 0.2321365201962217 },
- ],
- name: "rectangle",
- },
- {
- points: [
- { x: 0.438058748403576, y: 0.1372821208642104 },
- { x: 0.438058748403576, y: 0.16132971506105834 },
- { x: 0.4355044699872286, y: 0.1733535121594823 },
- { x: 0.4355044699872286, y: 0.18136937689176494 },
- { x: 0.4342273307790549, y: 0.18270535434714538 },
- { x: 0.4342273307790549, y: 0.26687193403611315 },
- { x: 0.4355044699872286, y: 0.26954388894687403 },
- { x: 0.4355044699872286, y: 0.2829036635006784 },
- { x: 0.4355044699872286, y: 0.28156768604529797 },
- { x: 0.44061302681992337, y: 0.2788957311345371 },
- { x: 0.4725415070242657, y: 0.2788957311345371 },
- { x: 0.47381864623243936, y: 0.27755975367915664 },
- { x: 0.47381864623243936, y: 0.26954388894687403 },
- { x: 0.4725415070242657, y: 0.26954388894687403 },
- { x: 0.4725415070242657, y: 0.26152802421459137 },
- { x: 0.47126436781609193, y: 0.26152802421459137 },
- { x: 0.47381864623243936, y: 0.13060223358730821 },
- { x: 0.4725415070242657, y: 0.13193821104268866 },
- { x: 0.4610472541507024, y: 0.13193821104268866 },
- { x: 0.4610472541507024, y: 0.1332741884980691 },
- { x: 0.45721583652618136, y: 0.1332741884980691 },
- { x: 0.45721583652618136, y: 0.13461016595344955 },
- ],
- name: "rectangle",
- },
- {
- points: [
- { x: 0.6079182630906769, y: 0.13594614340883 },
- { x: 0.6436781609195402, y: 0.13594614340883 },
- { x: 0.6462324393358876, y: 0.13461016595344955 },
- { x: 0.6564495530012772, y: 0.13461016595344955 },
- { x: 0.6628352490421456, y: 0.13193821104268866 },
- { x: 0.6998722860791826, y: 0.13193821104268866 },
- { x: 0.698595146871009, y: 0.17201753470410186 },
- { x: 0.6947637292464879, y: 0.17201753470410186 },
- { x: 0.6947637292464879, y: 0.17468948961486275 },
- { x: 0.6845466155810983, y: 0.17201753470410186 },
- { x: 0.6130268199233716, y: 0.17201753470410186 },
- { x: 0.6130268199233716, y: 0.1733535121594823 },
- { x: 0.6104725415070242, y: 0.1733535121594823 },
- ],
- name: "rectangle",
- },
- {
- points: [
- { x: 0.6500638569604087, y: 0.2722158438576349 },
- { x: 0.6513409961685823, y: 0.31363114497442857 },
- { x: 0.685823754789272, y: 0.314967122429809 },
- { x: 0.685823754789272, y: 0.31630309988518945 },
- { x: 0.6947637292464879, y: 0.3176390773405699 },
- { x: 0.6934865900383141, y: 0.26954388894687403 },
- { x: 0.6590038314176245, y: 0.26954388894687403 },
- ],
- name: "rectangle",
- },
- {
- points: [
- { x: 0.7420178799489144, y: 0.4031416344849181 },
- { x: 0.7841634738186463, y: 0.4031416344849181 },
- { x: 0.7841634738186463, y: 0.4044776119402985 },
- { x: 0.7879948914431673, y: 0.4044776119402985 },
- { x: 0.7879948914431673, y: 0.39913370211877675 },
- { x: 0.7854406130268199, y: 0.3911178373864941 },
- { x: 0.7841634738186463, y: 0.3911178373864941 },
- { x: 0.7828863346104725, y: 0.38176599519883103 },
- { x: 0.7816091954022989, y: 0.38176599519883103 },
- { x: 0.7816091954022989, y: 0.36707024318964615 },
- { x: 0.7777777777777778, y: 0.36707024318964615 },
- { x: 0.7777777777777778, y: 0.3657342657342657 },
- { x: 0.7586206896551724, y: 0.36707024318964615 },
- { x: 0.7573435504469987, y: 0.3657342657342657 },
- { x: 0.7509578544061303, y: 0.3657342657342657 },
- { x: 0.7509578544061303, y: 0.3643982882788853 },
- { x: 0.7394636015325671, y: 0.3643982882788853 },
- { x: 0.7381864623243933, y: 0.3630623108235049 },
- { x: 0.7381864623243933, y: 0.36707024318964615 },
- { x: 0.7407407407407407, y: 0.36974219810040704 },
- { x: 0.7407407407407407, y: 0.3724141530111679 },
- { x: 0.7420178799489144, y: 0.3724141530111679 },
- { x: 0.7432950191570882, y: 0.3804300177434506 },
- { x: 0.7445721583652618, y: 0.3804300177434506 },
- { x: 0.7445721583652618, y: 0.3924538148418745 },
- ],
- name: "rectangle",
- },
- {
- points: [
- { x: 0.7407407407407407, y: 0.45390877778937483 },
- { x: 0.7407407407407407, y: 0.460588665066277 },
- { x: 0.7420178799489144, y: 0.460588665066277 },
- { x: 0.7407407407407407, y: 0.4993320112723098 },
- { x: 0.7432950191570882, y: 0.4966600563615489 },
- { x: 0.7458492975734355, y: 0.4966600563615489 },
- { x: 0.7458492975734355, y: 0.4953240789061685 },
- { x: 0.7701149425287356, y: 0.4953240789061685 },
- { x: 0.7701149425287356, y: 0.4966600563615489 },
- { x: 0.7803320561941252, y: 0.49799603381692936 },
- { x: 0.7803320561941252, y: 0.4899801690846467 },
- { x: 0.7790549169859514, y: 0.48864419162926626 },
- { x: 0.7803320561941252, y: 0.45925268761089655 },
- { x: 0.7790549169859514, y: 0.4579167101555161 },
- { x: 0.7484035759897829, y: 0.4579167101555161 },
- { x: 0.7484035759897829, y: 0.45658073270013566 },
- ],
- name: "rectangle",
- },
- {
- points: [
- { x: 0.5210727969348659, y: 0.5875065233274188 },
- { x: 0.5210727969348659, y: 0.5460912222106252 },
- { x: 0.5236270753512133, y: 0.5460912222106252 },
- { x: 0.5325670498084292, y: 0.551435132032147 },
- { x: 0.5670498084291188, y: 0.5500991545767665 },
- { x: 0.5670498084291188, y: 0.5834985909612775 },
- { x: 0.565772669220945, y: 0.5848345684166579 },
- { x: 0.5542784163473818, y: 0.5848345684166579 },
- { x: 0.5491698595146871, y: 0.582162613505897 },
- { x: 0.5108556832694764, y: 0.582162613505897 },
- ],
- name: "rectangle",
- },
- {
- points: [
- { x: 0.6513409961685823, y: 0.5420832898444838 },
- { x: 0.6960408684546615, y: 0.5407473123891035 },
- { x: 0.6960408684546615, y: 0.5474271996660056 },
- { x: 0.6973180076628352, y: 0.5487631771213861 },
- { x: 0.70242656449553, y: 0.582162613505897 },
- { x: 0.6717752234993615, y: 0.582162613505897 },
- { x: 0.6704980842911877, y: 0.5834985909612775 },
- { x: 0.6526181353767561, y: 0.5834985909612775 },
- { x: 0.6513409961685823, y: 0.582162613505897 },
- { x: 0.6500638569604087, y: 0.5594509967644296 },
- { x: 0.6513409961685823, y: 0.5594509967644296 },
- { x: 0.6526181353767561, y: 0.5554430643982883 },
- { x: 0.6538952745849298, y: 0.5460912222106252 },
- { x: 0.6564495530012772, y: 0.5367393800229621 },
- { x: 0.6577266922094508, y: 0.5367393800229621 },
- { x: 0.6577266922094508, y: 0.5300594927460599 },
- { x: 0.6590038314176245, y: 0.5300594927460599 },
- ],
- name: "rectangle",
- },
- {
- points: [
- { x: 0.7841634738186463, y: 0.5875065233274188 },
- { x: 0.8275862068965517, y: 0.5875065233274188 },
- { x: 0.8275862068965517, y: 0.6369376891764952 },
- { x: 0.7969348659003831, y: 0.6369376891764952 },
- { x: 0.7816091954022989, y: 0.6409456215426365 },
- { x: 0.7816091954022989, y: 0.6262498695334516 },
- { x: 0.7790549169859514, y: 0.6235779146226907 },
- { x: 0.7777777777777778, y: 0.618234004801169 },
- { x: 0.7777777777777778, y: 0.5995303204258429 },
- { x: 0.7803320561941252, y: 0.6022022753366036 },
- ],
- name: "rectangle",
- },
- {
- points: [
- { x: 0.7841634738186463, y: 0.6810249452040497 },
- { x: 0.7841634738186463, y: 0.7625195699822566 },
- { x: 0.7841634738186463, y: 0.7611835925268761 },
- { x: 0.7867177522349936, y: 0.7611835925268761 },
- { x: 0.789272030651341, y: 0.7585116376161152 },
- { x: 0.7879948914431673, y: 0.7224402463208434 },
- { x: 0.7905491698595147, y: 0.7104164492224194 },
- { x: 0.7918263090676884, y: 0.7104164492224194 },
- { x: 0.7905491698595147, y: 0.6796889677486693 },
- { x: 0.789272030651341, y: 0.6796889677486693 },
- ],
- name: "rectangle",
- },
- {
- points: [
- { x: 0.5208333333333334, y: 0.36547049963984024 },
- { x: 0.5178902453232397, y: 0.36547049963984024 },
- { x: 0.514947157313146, y: 0.36547049963984024 },
- { x: 0.5120040693030523, y: 0.36547049963984024 },
- { x: 0.5090609812929586, y: 0.36547049963984024 },
- { x: 0.5061178932828649, y: 0.36547049963984024 },
- { x: 0.5031748052727713, y: 0.36547049963984024 },
- { x: 0.5002317172626776, y: 0.36547049963984024 },
- { x: 0.4972886292525838, y: 0.36547049963984024 },
- { x: 0.4943455412424901, y: 0.36547049963984024 },
- { x: 0.49140245323239634, y: 0.36547049963984024 },
- { x: 0.4884593652223026, y: 0.36547049963984024 },
- { x: 0.48551627721220886, y: 0.36547049963984024 },
- { x: 0.4825731892021151, y: 0.36547049963984024 },
- { x: 0.47996794871794873, y: 0.36547049963984024 },
- { x: 0.4799546861638081, y: 0.36513291253192154 },
- { x: 0.4798391521992856, y: 0.36219209309880446 },
- { x: 0.47972361823476306, y: 0.3592512736656874 },
- { x: 0.47960808427024054, y: 0.3563104542325703 },
- { x: 0.479492550305718, y: 0.3533696347994532 },
- { x: 0.4793770163411955, y: 0.35042881536633613 },
- { x: 0.479261482376673, y: 0.34748799593321905 },
- { x: 0.47914594841215047, y: 0.34454717650010197 },
- { x: 0.47903041444762795, y: 0.3416063570669849 },
- { x: 0.47891488048310543, y: 0.3386655376338678 },
- { x: 0.4787993465185829, y: 0.3357247182007507 },
- { x: 0.4786838125540604, y: 0.33278389876763365 },
- { x: 0.4785682785895379, y: 0.32984307933451656 },
- { x: 0.47845274462501536, y: 0.3269022599013995 },
- { x: 0.47833721066049284, y: 0.3239614404682824 },
- { x: 0.4782216766959703, y: 0.3210206210351653 },
- { x: 0.47810614273144786, y: 0.31807980160204824 },
- { x: 0.47799060876692534, y: 0.31513898216893116 },
- { x: 0.4778750748024029, y: 0.3121981627358141 },
- { x: 0.47775954083788036, y: 0.309257343302697 },
- { x: 0.4776440068733579, y: 0.3063165238695799 },
- { x: 0.4775641025641026, y: 0.3042826272018859 },
- { x: 0.4775641025641026, y: 0.3033750048273396 },
- { x: 0.4775641025641026, y: 0.30043191681724585 },
- { x: 0.4775641025641026, y: 0.2974888288071521 },
- { x: 0.4775641025641026, y: 0.29454574079705836 },
- { x: 0.4775641025641026, y: 0.2916026527869646 },
- { x: 0.4775641025641026, y: 0.2886595647768709 },
- { x: 0.4775641025641026, y: 0.28571647676677714 },
- { x: 0.4775641025641026, y: 0.2827733887566834 },
- { x: 0.4775641025641026, y: 0.27983030074658966 },
- { x: 0.4775641025641026, y: 0.2768872127364959 },
- { x: 0.4775641025641026, y: 0.27394412472640217 },
- { x: 0.4775641025641026, y: 0.27100103671630843 },
- { x: 0.4775641025641026, y: 0.2680579487062147 },
- { x: 0.4775641025641026, y: 0.26511486069612095 },
- { x: 0.4775641025641026, y: 0.2648876956322441 },
- { x: 0.4791666666666667, y: 0.2665640756990374 },
- { x: 0.4795634385114741, y: 0.2665640756990374 },
- { x: 0.48250652652156784, y: 0.2665640756990374 },
- { x: 0.4854496145316616, y: 0.2665640756990374 },
- { x: 0.4883927025417553, y: 0.2665640756990374 },
- { x: 0.4911858974358974, y: 0.2665640756990374 },
- { x: 0.4913300394226815, y: 0.2665229535169987 },
- { x: 0.4941602064411068, y: 0.2657155368445333 },
- { x: 0.4969903734595321, y: 0.26490812017206794 },
- { x: 0.49982054047795743, y: 0.26410070349960263 },
- { x: 0.5026507074963827, y: 0.26329328682713726 },
- { x: 0.505480874514808, y: 0.26248587015467195 },
- { x: 0.5083110415332334, y: 0.2616784534822066 },
- { x: 0.5088141025641025, y: 0.2615349354986576 },
- { x: 0.5112273308803348, y: 0.26135462232609513 },
- { x: 0.5141622376809069, y: 0.2611353300391504 },
- { x: 0.5170971444814789, y: 0.2609160377522057 },
- { x: 0.520032051282051, y: 0.26069674546526095 },
- { x: 0.5200320512820513, y: 0.26069674546526095 },
- ],
- name: "rectangle",
- },
- {
- points: [
- { x: 0.5632183908045977, y: 0.3122951675190481 },
- { x: 0.5632183908045977, y: 0.3056152802421459 },
- { x: 0.5670498084291188, y: 0.2989353929652437 },
- { x: 0.5670498084291188, y: 0.29626343805448285 },
- { x: 0.5696040868454662, y: 0.2949274605991024 },
- { x: 0.5708812260536399, y: 0.28958355077758063 },
- { x: 0.578544061302682, y: 0.2762237762237762 },
- { x: 0.5810983397190294, y: 0.27355182131301536 },
- { x: 0.5836526181353767, y: 0.27355182131301536 },
- { x: 0.5836526181353767, y: 0.2802317085899175 },
- { x: 0.5849297573435505, y: 0.2802317085899175 },
- { x: 0.5862068965517241, y: 0.28691159586681975 },
- { x: 0.5887611749680716, y: 0.2882475733222002 },
- { x: 0.5964240102171137, y: 0.3016073478760046 },
- { x: 0.6002554278416348, y: 0.30427930278676546 },
- { x: 0.6015325670498084, y: 0.3082872351529068 },
- { x: 0.6040868454661558, y: 0.30962321260828723 },
- { x: 0.6040868454661558, y: 0.3122951675190481 },
- { x: 0.6091954022988506, y: 0.3176390773405699 },
- { x: 0.6002554278416348, y: 0.3176390773405699 },
- { x: 0.6002554278416348, y: 0.31630309988518945 },
- { x: 0.5964240102171137, y: 0.31630309988518945 },
- { x: 0.5964240102171137, y: 0.314967122429809 },
- { x: 0.5708812260536399, y: 0.31630309988518945 },
- ],
- name: "triangle",
- },
- {
- points: [
- { x: 0.438058748403576, y: 0.40180565702953763 },
- { x: 0.44061302681992337, y: 0.39378979229725497 },
- { x: 0.45338441890166026, y: 0.37375013046654837 },
- { x: 0.46998722860791825, y: 0.3510385137250809 },
- { x: 0.47126436781609193, y: 0.34703058135893955 },
- { x: 0.4827586206896552, y: 0.33367080680513517 },
- { x: 0.4840357598978289, y: 0.3376787391712765 },
- { x: 0.48531289910600256, y: 0.3376787391712765 },
- { x: 0.48659003831417624, y: 0.3456946039035591 },
- { x: 0.5185185185185185, y: 0.3884458824757332 },
- { x: 0.5197956577266922, y: 0.3884458824757332 },
- { x: 0.5197956577266922, y: 0.3911178373864941 },
- { x: 0.5210727969348659, y: 0.3911178373864941 },
- { x: 0.5261813537675607, y: 0.4031416344849181 },
- ],
- name: "triangle",
- },
- {
- points: [
- { x: 0.42528735632183906, y: 0.5006679887276902 },
- { x: 0.43039591315453385, y: 0.5006679887276902 },
- { x: 0.4342273307790549, y: 0.4966600563615489 },
- { x: 0.4508301404853129, y: 0.48597223671850537 },
- { x: 0.46871008939974457, y: 0.4766203945308423 },
- { x: 0.46871008939974457, y: 0.4752844170754619 },
- { x: 0.4725415070242657, y: 0.4752844170754619 },
- { x: 0.47509578544061304, y: 0.4833002818077445 },
- { x: 0.47509578544061304, y: 0.49799603381692936 },
- { x: 0.4763729246487867, y: 0.4993320112723098 },
- { x: 0.4763729246487867, y: 0.5340674251122012 },
- { x: 0.46998722860791825, y: 0.5300594927460599 },
- { x: 0.46998722860791825, y: 0.5287235152906795 },
- { x: 0.46360153256704983, y: 0.5260515603799186 },
- { x: 0.45977011494252873, y: 0.5207076505583969 },
- { x: 0.454661558109834, y: 0.5193716731030164 },
- { x: 0.454661558109834, y: 0.518035695647636 },
- { x: 0.44572158365261816, y: 0.5126917858261142 },
- { x: 0.44316730523627074, y: 0.5126917858261142 },
- { x: 0.44189016602809705, y: 0.5100198309153533 },
- ],
- name: "triangle",
- },
- {
- points: [
- { x: 0.6513409961685823, y: 0.4111574992172007 },
- { x: 0.6794380587484036, y: 0.4111574992172007 },
- { x: 0.6819923371647509, y: 0.4098215217618203 },
- { x: 0.6909323116219668, y: 0.4098215217618203 },
- { x: 0.6922094508301405, y: 0.40848554430643985 },
- { x: 0.7011494252873564, y: 0.40848554430643985 },
- { x: 0.7011494252873564, y: 0.41382945412796157 },
- { x: 0.6909323116219668, y: 0.4245172737710051 },
- { x: 0.6883780332056194, y: 0.43119716104790734 },
- { x: 0.685823754789272, y: 0.4325331385032878 },
- { x: 0.6807151979565773, y: 0.44188498069095083 },
- { x: 0.6768837803320562, y: 0.4445569356017117 },
- { x: 0.6756066411238825, y: 0.4499008454232335 },
- { x: 0.6730523627075351, y: 0.4499008454232335 },
- { x: 0.6666666666666666, y: 0.4432209581463313 },
- { x: 0.6666666666666666, y: 0.4405490032355704 },
- { x: 0.6628352490421456, y: 0.4378770483248095 },
- { x: 0.6526181353767561, y: 0.4205093414048638 },
- ],
- name: "triangle",
- },
- {
- points: [
- { x: 0.6756066411238825, y: 0.18136937689176494 },
- { x: 0.6743295019157088, y: 0.18804926416866716 },
- { x: 0.6513409961685823, y: 0.2227846780085586 },
- { x: 0.6500638569604087, y: 0.22812858783008036 },
- { x: 0.648786717752235, y: 0.22812858783008036 },
- { x: 0.6551724137931034, y: 0.22812858783008036 },
- { x: 0.6564495530012772, y: 0.22679261037469992 },
- { x: 0.6922094508301405, y: 0.22812858783008036 },
- { x: 0.6922094508301405, y: 0.2227846780085586 },
- { x: 0.6896551724137931, y: 0.21744076818703684 },
- { x: 0.6883780332056194, y: 0.21744076818703684 },
- { x: 0.685823754789272, y: 0.21076088091013465 },
- { x: 0.6832694763729247, y: 0.2094249034547542 },
- { x: 0.6832694763729247, y: 0.20675294854399331 },
- { x: 0.6807151979565773, y: 0.20408099363323243 },
- { x: 0.6807151979565773, y: 0.1933931739901889 },
- { x: 0.6794380587484036, y: 0.1933931739901889 },
- ],
- name: "triangle",
- },
- {
- points: [
- { x: 0.454661558109834, y: 0.18537730925790627 },
- { x: 0.454661558109834, y: 0.19072121907942804 },
- { x: 0.4444444444444444, y: 0.20808892599937376 },
- { x: 0.44061302681992337, y: 0.21877674564241728 },
- { x: 0.43167305236270753, y: 0.23347249765160213 },
- { x: 0.43039591315453385, y: 0.23347249765160213 },
- { x: 0.42911877394636017, y: 0.23881640747312388 },
- { x: 0.43167305236270753, y: 0.23480847510698258 },
- { x: 0.438058748403576, y: 0.23347249765160213 },
- { x: 0.4827586206896552, y: 0.23347249765160213 },
- ],
- name: "triangle",
- },
- {
- points: [
- { x: 0.26309067688378035, y: 0.22812858783008036 },
- { x: 0.2669220945083014, y: 0.2321365201962217 },
- { x: 0.26947637292464877, y: 0.23881640747312388 },
- { x: 0.27330779054916987, y: 0.2628640016699718 },
- { x: 0.27458492975734355, y: 0.2628640016699718 },
- { x: 0.27458492975734355, y: 0.2682079114914936 },
- { x: 0.27586206896551724, y: 0.2682079114914936 },
- { x: 0.2784163473818646, y: 0.27755975367915664 },
- { x: 0.2784163473818646, y: 0.2762237762237762 },
- { x: 0.27586206896551724, y: 0.2762237762237762 },
- { x: 0.27330779054916987, y: 0.27355182131301536 },
- { x: 0.2707535121328225, y: 0.27355182131301536 },
- { x: 0.2707535121328225, y: 0.2722158438576349 },
- { x: 0.26436781609195403, y: 0.26954388894687403 },
- { x: 0.2388250319284802, y: 0.26954388894687403 },
- { x: 0.2388250319284802, y: 0.2682079114914936 },
- { x: 0.23371647509578544, y: 0.26687193403611315 },
- ],
- name: "triangle",
- },
- {
- points: [
- { x: 0.21839080459770116, y: 0.4499008454232335 },
- { x: 0.21839080459770116, y: 0.4352050934140486 },
- { x: 0.2247765006385696, y: 0.41382945412796157 },
- { x: 0.2247765006385696, y: 0.4071495668510594 },
- { x: 0.227330779054917, y: 0.40180565702953763 },
- { x: 0.22860791826309068, y: 0.3924538148418745 },
- { x: 0.23371647509578544, y: 0.3750861079219288 },
- { x: 0.23627075351213284, y: 0.3724141530111679 },
- { x: 0.23627075351213284, y: 0.3657342657342657 },
- { x: 0.23754789272030652, y: 0.3643982882788853 },
- { x: 0.25287356321839083, y: 0.4245172737710051 },
- { x: 0.25798212005108556, y: 0.4378770483248095 },
- { x: 0.25925925925925924, y: 0.44856486796785305 },
- { x: 0.2515964240102171, y: 0.44856486796785305 },
- { x: 0.2515964240102171, y: 0.4472288905124726 },
- { x: 0.24648786717752236, y: 0.44589291305709217 },
- { x: 0.23243933588761176, y: 0.44589291305709217 },
- { x: 0.23243933588761176, y: 0.4472288905124726 },
- { x: 0.227330779054917, y: 0.44856486796785305 },
- ],
- name: "triangle",
- },
- {
- points: [
- { x: 0.23627075351213284, y: 0.5006679887276902 },
- { x: 0.23627075351213284, y: 0.5153637407368751 },
- { x: 0.24010217113665389, y: 0.527387537835299 },
- { x: 0.24010217113665389, y: 0.5340674251122012 },
- { x: 0.24521072796934865, y: 0.5500991545767665 },
- { x: 0.2515964240102171, y: 0.5794906585951362 },
- { x: 0.2541507024265645, y: 0.5834985909612775 },
- { x: 0.2541507024265645, y: 0.5875065233274188 },
- { x: 0.24648786717752236, y: 0.5875065233274188 },
- { x: 0.24648786717752236, y: 0.5861705458720384 },
- { x: 0.24265644955300128, y: 0.5861705458720384 },
- { x: 0.24265644955300128, y: 0.5848345684166579 },
- { x: 0.210727969348659, y: 0.5834985909612775 },
- ],
- name: "triangle",
- },
- {
- points: [
- { x: 0.3486590038314176, y: 0.6329297568103538 },
- { x: 0.3563218390804598, y: 0.6329297568103538 },
- { x: 0.36909323116219667, y: 0.6289218244442125 },
- { x: 0.38058748403575987, y: 0.6235779146226907 },
- { x: 0.384418901660281, y: 0.6235779146226907 },
- { x: 0.388250319284802, y: 0.6209059597119299 },
- { x: 0.4074074074074074, y: 0.6155620498904081 },
- { x: 0.421455938697318, y: 0.6088821626135059 },
- { x: 0.4342273307790549, y: 0.606210207702745 },
- { x: 0.44189016602809705, y: 0.6022022753366036 },
- { x: 0.44572158365261816, y: 0.6022022753366036 },
- { x: 0.45721583652618136, y: 0.596858365515082 },
- { x: 0.4610472541507024, y: 0.596858365515082 },
- { x: 0.4610472541507024, y: 0.5955223880597015 },
- { x: 0.4623243933588761, y: 0.596858365515082 },
- { x: 0.4623243933588761, y: 0.6022022753366036 },
- { x: 0.4661558109833972, y: 0.6075461851581254 },
- { x: 0.4776500638569604, y: 0.6155620498904081 },
- { x: 0.4904214559386973, y: 0.6262498695334516 },
- { x: 0.49169859514687103, y: 0.6289218244442125 },
- { x: 0.4955300127713921, y: 0.6302578018995929 },
- { x: 0.49680715197956576, y: 0.6329297568103538 },
- { x: 0.5006385696040868, y: 0.6342657342657343 },
- { x: 0.5006385696040868, y: 0.6356017117211147 },
- { x: 0.5044699872286079, y: 0.6369376891764952 },
- { x: 0.508301404853129, y: 0.6409456215426365 },
- { x: 0.5172413793103449, y: 0.6449535539087778 },
- ],
- name: "triangle",
- },
- {
- points: [
- { x: 0.6091954022988506, y: 0.4966600563615489 },
- { x: 0.6091954022988506, y: 0.5527711094875274 },
- { x: 0.6104725415070242, y: 0.5527711094875274 },
- { x: 0.6104725415070242, y: 0.5581150193090492 },
- { x: 0.611749680715198, y: 0.5541070869429079 },
- { x: 0.6130268199233716, y: 0.5541070869429079 },
- { x: 0.6143039591315453, y: 0.5474271996660056 },
- { x: 0.6219667943805874, y: 0.539411334933723 },
- { x: 0.6462324393358876, y: 0.5233796054691577 },
- { x: 0.6551724137931034, y: 0.5140277632814946 },
- { x: 0.6577266922094508, y: 0.5140277632814946 },
- { x: 0.6500638569604087, y: 0.5140277632814946 },
- { x: 0.648786717752235, y: 0.5113558083707337 },
- { x: 0.6257982120051085, y: 0.49799603381692936 },
- { x: 0.6155810983397191, y: 0.4966600563615489 },
- ],
- name: "triangle",
- },
- {
- points: [
- { x: 0.7011494252873564, y: 0.5447552447552447 },
- { x: 0.7650063856960408, y: 0.5447552447552447 },
- { x: 0.7662835249042146, y: 0.5434192672998643 },
- { x: 0.776500638569604, y: 0.5434192672998643 },
- { x: 0.7790549169859514, y: 0.5420832898444838 },
- { x: 0.8582375478927203, y: 0.5407473123891035 },
- { x: 0.8582375478927203, y: 0.5434192672998643 },
- { x: 0.855683269476373, y: 0.5447552447552447 },
- { x: 0.8518518518518519, y: 0.5447552447552447 },
- { x: 0.8518518518518519, y: 0.5460912222106252 },
- { x: 0.8454661558109834, y: 0.5474271996660056 },
- { x: 0.8454661558109834, y: 0.5487631771213861 },
- { x: 0.8416347381864623, y: 0.5487631771213861 },
- { x: 0.7969348659003831, y: 0.582162613505897 },
- { x: 0.7943805874840357, y: 0.582162613505897 },
- { x: 0.7816091954022989, y: 0.5888425007827993 },
- { x: 0.7816091954022989, y: 0.5901784782381797 },
- { x: 0.7752234993614304, y: 0.5901784782381797 },
- { x: 0.7739463601532567, y: 0.5915144556935602 },
- ],
- name: "triangle",
- },
- {
- points: [
- { x: 0.665389527458493, y: 0.6783529902932888 },
- { x: 0.6666666666666666, y: 0.6235779146226907 },
- { x: 0.6781609195402298, y: 0.5661308840413318 },
- { x: 0.6781609195402298, y: 0.5554430643982883 },
- { x: 0.6794380587484036, y: 0.5554430643982883 },
- { x: 0.685823754789272, y: 0.5768187036843754 },
- { x: 0.6896551724137931, y: 0.6168980273457886 },
- { x: 0.6973180076628352, y: 0.6556413735518213 },
- { x: 0.7011494252873564, y: 0.6877048324809519 },
- { x: 0.7011494252873564, y: 0.685032877570191 },
- ],
- name: "triangle",
- },
- {
- points: [
- { x: 0.48148148148148145, y: 0.7277841561423651 },
- { x: 0.48148148148148145, y: 0.7211042688654629 },
- { x: 0.48659003831417624, y: 0.7117524266777998 },
- { x: 0.4878671775223499, y: 0.7050725394008976 },
- { x: 0.4929757343550447, y: 0.697056674668615 },
- { x: 0.4929757343550447, y: 0.6930487423024736 },
- { x: 0.49808429118773945, y: 0.6823609226594302 },
- { x: 0.5006385696040868, y: 0.6810249452040497 },
- { x: 0.5070242656449553, y: 0.6983926521239954 },
- { x: 0.5108556832694764, y: 0.7024005844901368 },
- { x: 0.5223499361430396, y: 0.7237762237762237 },
- { x: 0.5236270753512133, y: 0.7237762237762237 },
- { x: 0.5261813537675607, y: 0.7317920885085064 },
- { x: 0.5210727969348659, y: 0.7317920885085064 },
- { x: 0.5185185185185185, y: 0.7291201335977455 },
- { x: 0.508301404853129, y: 0.7277841561423651 },
- { x: 0.508301404853129, y: 0.7264481786869846 },
- { x: 0.4891443167305236, y: 0.7264481786869846 },
- ],
- name: "triangle",
- },
- {
- points: [
- { x: 0.37037037037037035, y: 0.6836969001148105 },
- { x: 0.37037037037037035, y: 0.6877048324809519 },
- { x: 0.36398467432950193, y: 0.6997286295793759 },
- { x: 0.3537675606641124, y: 0.7157603590439411 },
- { x: 0.34610472541507026, y: 0.7237762237762237 },
- { x: 0.34355044699872284, y: 0.730456111053126 },
- { x: 0.3499361430395913, y: 0.7277841561423651 },
- { x: 0.3499361430395913, y: 0.7264481786869846 },
- { x: 0.38058748403575987, y: 0.7264481786869846 },
- { x: 0.3818646232439336, y: 0.7277841561423651 },
- { x: 0.388250319284802, y: 0.7277841561423651 },
- { x: 0.388250319284802, y: 0.7211042688654629 },
- { x: 0.384418901660281, y: 0.7144243815885607 },
- { x: 0.3831417624521073, y: 0.7144243815885607 },
- { x: 0.38058748403575987, y: 0.7077444943116585 },
- { x: 0.3793103448275862, y: 0.7077444943116585 },
- { x: 0.3793103448275862, y: 0.7050725394008976 },
- ],
- name: "triangle",
- },
- {
- points: [
- { x: 0.1698595146871009, y: 0.6396096440872561 },
- { x: 0.15964240102171137, y: 0.6396096440872561 },
- { x: 0.1583652618135377, y: 0.642281598998017 },
- { x: 0.13026819923371646, y: 0.6623212608287236 },
- { x: 0.13793103448275862, y: 0.6623212608287236 },
- { x: 0.14559386973180077, y: 0.6676651706502453 },
- { x: 0.14814814814814814, y: 0.6676651706502453 },
- { x: 0.1558109833971903, y: 0.6743450579271475 },
- { x: 0.1583652618135377, y: 0.6743450579271475 },
- { x: 0.16219667943805874, y: 0.6783529902932888 },
- { x: 0.1685823754789272, y: 0.6810249452040497 },
- { x: 0.1724137931034483, y: 0.685032877570191 },
- { x: 0.17496807151979565, y: 0.685032877570191 },
- { x: 0.17369093231162197, y: 0.673009080471767 },
- { x: 0.1724137931034483, y: 0.673009080471767 },
- { x: 0.1724137931034483, y: 0.6569773510072018 },
- { x: 0.1698595146871009, y: 0.6569773510072018 },
- ],
- name: "triangle",
- },
- {
- points: [
- { x: 0.4591728525980912, y: 0.1977155336776788 },
- { x: 0.4591728525980912, y: 0.20343754210605733 },
- { x: 0.4591728525980912, y: 0.20915955053443586 },
- { x: 0.4591728525980912, y: 0.21488155896281438 },
- { x: 0.4591728525980912, y: 0.2206035673911929 },
- { x: 0.4591728525980912, y: 0.22632557581957144 },
- { x: 0.4591728525980912, y: 0.23204758424794997 },
- { x: 0.4591728525980912, y: 0.2377695926763285 },
- { x: 0.4591728525980912, y: 0.24349160110470702 },
- { x: 0.4591728525980912, y: 0.24921360953308555 },
- { x: 0.4591728525980912, y: 0.2549356179614641 },
- { x: 0.4591728525980912, y: 0.26065762638984263 },
- { x: 0.4591728525980912, y: 0.2663796348182212 },
- { x: 0.4591728525980912, y: 0.27210164324659974 },
- { x: 0.4591728525980912, y: 0.2778236516749783 },
- { x: 0.4591728525980912, y: 0.28354566010335686 },
- { x: 0.4591728525980912, y: 0.2892676685317354 },
- { x: 0.4591728525980912, y: 0.29498967696011397 },
- { x: 0.4591728525980912, y: 0.3007116853884925 },
- { x: 0.4591728525980912, y: 0.3064336938168711 },
- { x: 0.4591728525980912, y: 0.31215570224524963 },
- { x: 0.4591728525980912, y: 0.3178777106736282 },
- { x: 0.4591728525980912, y: 0.32359971910200674 },
- { x: 0.4591728525980912, y: 0.3293217275303853 },
- { x: 0.4591728525980912, y: 0.33504373595876386 },
- { x: 0.4591728525980912, y: 0.3407657443871424 },
- { x: 0.4591728525980912, y: 0.34648775281552097 },
- { x: 0.4591728525980912, y: 0.34858044164037855 },
- { x: 0.4602332979851538, y: 0.34858044164037855 },
- { x: 0.46053809669050594, y: 0.35113116947556605 },
- { x: 0.46121701695441375, y: 0.35681275796204504 },
- { x: 0.46129374337221635, y: 0.3574548479911256 },
- { x: 0.46129374337221635, y: 0.3625301984352238 },
- { x: 0.46129374337221635, y: 0.3682522068636024 },
- { x: 0.46129374337221635, y: 0.36965715672340277 },
- { x: 0.46307453718696423, y: 0.36572450264836776 },
- { x: 0.4654348751474816, y: 0.36051199987459626 },
- { x: 0.467795213107999, y: 0.35529949710082476 },
- { x: 0.47015555106851636, y: 0.35008699432705326 },
- { x: 0.4708377518557794, y: 0.34858044164037855 },
- { x: 0.473149122362902, y: 0.34523263985426356 },
- { x: 0.4764001173564769, y: 0.34052388078140944 },
- { x: 0.4796511123500518, y: 0.3358151217085553 },
- { x: 0.4829021073436267, y: 0.3311063626357012 },
- { x: 0.4861531023372016, y: 0.3263976035628471 },
- { x: 0.4894040973307765, y: 0.32168884448999296 },
- { x: 0.49265509232435145, y: 0.31698008541713885 },
- { x: 0.49590608731792635, y: 0.3122713263442847 },
- { x: 0.4984093319194062, y: 0.3086456130620168 },
- { x: 0.49921665137814286, y: 0.3076062122130033 },
- { x: 0.5027266348516582, y: 0.3030872082884549 },
- { x: 0.5062366183251736, y: 0.2985682043639065 },
- { x: 0.509746601798689, y: 0.2940492004393581 },
- { x: 0.5132565852722044, y: 0.2895301965148097 },
- { x: 0.5167665687457198, y: 0.2850111925902613 },
- { x: 0.5202765522192352, y: 0.28049218866571296 },
- { x: 0.5237865356927507, y: 0.2759731847411646 },
- { x: 0.5259809119830329, y: 0.27314798765902865 },
- { x: 0.5271455475046231, y: 0.271347035693587 },
- { x: 0.5302527450675039, y: 0.2665421731815412 },
- { x: 0.5333599426303847, y: 0.26173731066949546 },
- { x: 0.5364671401932654, y: 0.25693244815744964 },
- { x: 0.5395743377561462, y: 0.2521275856454039 },
- { x: 0.5426815353190269, y: 0.2473227231333581 },
- { x: 0.5457887328819077, y: 0.2425178606213123 },
- { x: 0.5488959304447883, y: 0.23771299810926652 },
- { x: 0.5503711558854719, y: 0.23543176066835372 },
- { x: 0.5524478697779884, y: 0.23325937121986418 },
- { x: 0.5556733828207847, y: 0.22988525669913681 },
- { x: 0.5561028692813909, y: 0.22892252845540373 },
- { x: 0.5584340796976681, y: 0.22369693390076448 },
- { x: 0.5607652901139454, y: 0.21847133934612523 },
- { x: 0.5630965005302226, y: 0.2132457447914861 },
- ],
- name: "triangle",
- },
- {
- points: [
- { x: 0.7900318133616119, y: 0.15999930668700385 },
- { x: 0.7946838425986257, y: 0.15999930668700385 },
- { x: 0.7993358718356395, y: 0.15999930668700385 },
- { x: 0.8039879010726533, y: 0.15999930668700385 },
- { x: 0.8086399303096671, y: 0.15999930668700385 },
- { x: 0.8132919595466809, y: 0.15999930668700385 },
- { x: 0.8179439887836947, y: 0.15999930668700385 },
- { x: 0.8225960180207085, y: 0.15999930668700385 },
- { x: 0.8272480472577223, y: 0.15999930668700385 },
- { x: 0.8319000764947361, y: 0.15999930668700385 },
- { x: 0.8365521057317499, y: 0.15999930668700385 },
- { x: 0.8412041349687637, y: 0.15999930668700385 },
- { x: 0.8458561642057775, y: 0.15999930668700385 },
- { x: 0.8505081934427913, y: 0.15999930668700385 },
- { x: 0.8551602226798051, y: 0.15999930668700385 },
- { x: 0.8598122519168189, y: 0.15999930668700385 },
- { x: 0.8644642811538327, y: 0.15999930668700385 },
- { x: 0.8691163103908465, y: 0.15999930668700385 },
- { x: 0.8737683396278603, y: 0.15999930668700385 },
- { x: 0.8784203688648741, y: 0.15999930668700385 },
- { x: 0.8830723981018879, y: 0.15999930668700385 },
- { x: 0.8877244273389017, y: 0.15999930668700385 },
- { x: 0.8907741251325557, y: 0.15999930668700385 },
- { x: 0.8904360306336263, y: 0.1615655627848924 },
- { x: 0.8894544450194818, y: 0.16611285491363628 },
- { x: 0.8884728594053373, y: 0.17066014704238017 },
- { x: 0.8874912737911929, y: 0.17520743917112405 },
- { x: 0.8865096881770483, y: 0.17975473129986794 },
- { x: 0.8855281025629039, y: 0.18430202342861182 },
- { x: 0.8845465169487594, y: 0.1888493155573557 },
- { x: 0.883564931334615, y: 0.1933966076860996 },
- { x: 0.8833510074231177, y: 0.19438763129614864 },
- { x: 0.882803987503398, y: 0.19798445204150153 },
- { x: 0.882104529809995, y: 0.20258359706608717 },
- { x: 0.881405072116592, y: 0.2071827420906728 },
- { x: 0.8807056144231892, y: 0.21178188711525844 },
- { x: 0.8800061567297862, y: 0.21638103213984408 },
- { x: 0.8793066990363834, y: 0.22098017716442972 },
- { x: 0.8786072413429804, y: 0.22557932218901536 },
- { x: 0.8779077836495776, y: 0.230178467213601 },
- { x: 0.8772083259561747, y: 0.23477761223818663 },
- { x: 0.8765088682627717, y: 0.23937675726277227 },
- { x: 0.8758094105693689, y: 0.2439759022873579 },
- { x: 0.8751099528759659, y: 0.24857504731194355 },
- { x: 0.8744104951825631, y: 0.2531741923365292 },
- { x: 0.8737110374891601, y: 0.25777333736111485 },
- { x: 0.8730115797957573, y: 0.2623724823857005 },
- { x: 0.8723121221023543, y: 0.2669716274102861 },
- { x: 0.8716126644089515, y: 0.27157077243487177 },
- { x: 0.8709132067155485, y: 0.2761699174594574 },
- { x: 0.8702137490221457, y: 0.28076906248404304 },
- { x: 0.8695142913287427, y: 0.2853682075086287 },
- { x: 0.8688148336353398, y: 0.2899673525332143 },
- { x: 0.8685047720042418, y: 0.29200610115436615 },
- { x: 0.8678613639186619, y: 0.2945147432315597 },
- { x: 0.8667056338560808, y: 0.29902092388089874 },
- { x: 0.8655499037934997, y: 0.3035271045302378 },
- { x: 0.8643941737309186, y: 0.30803328517957684 },
- { x: 0.8632384436683376, y: 0.3125394658289159 },
- { x: 0.8620827136057565, y: 0.31704564647825495 },
- { x: 0.8609269835431754, y: 0.321551827127594 },
- { x: 0.8597712534805944, y: 0.32605800777693306 },
- { x: 0.8586155234180133, y: 0.3305641884262721 },
- { x: 0.8574597933554322, y: 0.33507036907561116 },
- { x: 0.8568398727465536, y: 0.33748743370194473 },
- { x: 0.8560222276367154, y: 0.3394831676528042 },
- { x: 0.8542585838125305, y: 0.34378792507726447 },
- { x: 0.8536585365853658, y: 0.3452525392588484 },
- { x: 0.8525980911983033, y: 0.34636184005269177 },
- { x: 0.8515376458112409, y: 0.34525253925884863 },
- { x: 0.8515376458112407, y: 0.3452525392588484 },
- ],
- name: "triangle",
- },
- {
- points: [
- { x: 0.24390243902439024, y: 0.4306687003847887 },
- { x: 0.23779215776891913, y: 0.430721092072297 },
- { x: 0.23168187651344802, y: 0.4307734837598052 },
- { x: 0.2255715952579769, y: 0.43082587544731343 },
- { x: 0.2194613140025058, y: 0.43087826713482164 },
- { x: 0.21335103274703468, y: 0.4309306588223299 },
- { x: 0.20724075149156357, y: 0.4309830505098381 },
- { x: 0.20113047023609246, y: 0.43103544219734635 },
- { x: 0.19502018898062135, y: 0.43108783388485455 },
- { x: 0.18890990772515023, y: 0.4311402255723628 },
- { x: 0.18279962646967912, y: 0.431192617259871 },
- { x: 0.176689345214208, y: 0.43124500894737927 },
- { x: 0.1705790639587369, y: 0.4312974006348875 },
- { x: 0.1644687827032658, y: 0.43134979232239573 },
- { x: 0.15835850144779468, y: 0.43140218400990393 },
- { x: 0.15224822019232356, y: 0.4314545756974122 },
- { x: 0.14613793893685245, y: 0.4315069673849204 },
- { x: 0.14002765768138134, y: 0.43155935907242865 },
- { x: 0.13391737642591023, y: 0.43161175075993685 },
- { x: 0.12780709517043912, y: 0.4316641424474451 },
- { x: 0.121696813914968, y: 0.4317165341349533 },
- { x: 0.1155865326594969, y: 0.43176892582246157 },
- { x: 0.11452810180275716, y: 0.4317780011786321 },
- { x: 0.10950175517476975, y: 0.43228683153449 },
- { x: 0.1034223210743054, y: 0.43290226871893117 },
- { x: 0.09734288697384107, y: 0.4335177059033724 },
- { x: 0.09126345287337673, y: 0.4341331430878136 },
- { x: 0.08518401877291239, y: 0.4347485802722548 },
- { x: 0.0816542948038176, y: 0.4351059035601622 },
- { x: 0.0816542948038176, y: 0.4362152043540056 },
- { x: 0.08310773561567547, y: 0.4362152043540056 },
- { x: 0.08589607635206786, y: 0.4362152043540056 },
- { x: 0.08827908890679795, y: 0.4339004619716089 },
- { x: 0.0926621980031301, y: 0.4296429231802963 },
- { x: 0.09704530709946224, y: 0.42538538438898366 },
- { x: 0.1007423117709438, y: 0.4217942940340417 },
- { x: 0.10131302523822786, y: 0.4210267141109059 },
- { x: 0.10495897081042967, y: 0.41612310699018107 },
- { x: 0.10816542948038176, y: 0.41181058688945127 },
- { x: 0.10869246118754315, y: 0.41129602864969383 },
- { x: 0.11306467093781976, y: 0.40702729771304913 },
- { x: 0.11743688068809635, y: 0.4027585667764044 },
- { x: 0.12180909043837294, y: 0.39848983583975967 },
- { x: 0.12407211028632026, y: 0.3962803757756439 },
- { x: 0.1258424393864069, y: 0.3939234257447946 },
- { x: 0.12951221333329013, y: 0.3890376258012174 },
- { x: 0.13318198728017336, y: 0.3841518258576403 },
- { x: 0.1357370095440085, y: 0.3807501646618366 },
- { x: 0.13725950843091317, y: 0.37968840372976426 },
- { x: 0.13891834570519618, y: 0.3785315630741498 },
- { x: 0.14068134442232255, y: 0.3748431206511592 },
- { x: 0.14103923647932132, y: 0.3740943598987763 },
- { x: 0.14457794371663737, y: 0.3701748728429738 },
- { x: 0.1486727920126013, y: 0.36563940160583397 },
- { x: 0.15276764030856524, y: 0.36110393036869415 },
- { x: 0.15686248860452917, y: 0.3565684591315543 },
- { x: 0.15906680805938495, y: 0.35412694560959546 },
- { x: 0.16138080963593865, y: 0.35251320618957294 },
- { x: 0.16542948038176034, y: 0.3496897424342219 },
- { x: 0.16614299535089544, y: 0.34875675860104083 },
- { x: 0.16967126193001061, y: 0.34414323846500505 },
- { x: 0.16995687107329605, y: 0.3440436493510847 },
- { x: 0.1728525980911983, y: 0.34303393767116164 },
- { x: 0.17503050021822517, y: 0.340907580813274 },
- { x: 0.17940270996850177, y: 0.3366388498766293 },
- { x: 0.18377491971877838, y: 0.3323701189399846 },
- { x: 0.18814712946905499, y: 0.3281013880033399 },
- { x: 0.1925193392193316, y: 0.3238326570666952 },
- { x: 0.1968915489696082, y: 0.31956392613005047 },
- { x: 0.20126375871988478, y: 0.3152951951934057 },
- { x: 0.2046659597030753, y: 0.311973515443547 },
- { x: 0.20574488271501684, y: 0.311152693847267 },
- { x: 0.2106080116605205, y: 0.3074529291561973 },
- { x: 0.2154711406060242, y: 0.3037531644651277 },
- { x: 0.2163308589607635, y: 0.30309910909279997 },
- { x: 0.2196305093007199, y: 0.2993022749844221 },
- { x: 0.22363874795485683, y: 0.2946900840949029 },
- { x: 0.22693531283138918, y: 0.29089680036052273 },
- { x: 0.22730143152064247, y: 0.28987550432124354 },
- { x: 0.22936345927612115, y: 0.2841234330903288 },
- { x: 0.2301166489925769, y: 0.2820223940097757 },
- { x: 0.23329798515376457, y: 0.27980379242208897 },
- ],
- name: "triangle",
- },
- {
- points: [
- { x: 0.5427841634738186, y: 0.18404133180252583 },
- { x: 0.5402298850574713, y: 0.18404133180252583 },
- { x: 0.5402298850574713, y: 0.18537730925790627 },
- { x: 0.5338441890166028, y: 0.18804926416866716 },
- { x: 0.524904214559387, y: 0.19472915144556935 },
- { x: 0.5172413793103449, y: 0.20408099363323243 },
- { x: 0.5172413793103449, y: 0.21744076818703684 },
- { x: 0.5351213282247765, y: 0.23614445256236302 },
- { x: 0.5504469987228607, y: 0.23614445256236302 },
- { x: 0.5593869731800766, y: 0.22679261037469992 },
- { x: 0.5632183908045977, y: 0.2254566329193195 },
- { x: 0.5734355044699873, y: 0.21343283582089553 },
- { x: 0.5734355044699873, y: 0.20541697108861287 },
- { x: 0.5696040868454662, y: 0.20140903872247157 },
- { x: 0.5696040868454662, y: 0.19740110635633024 },
- { x: 0.5644955300127714, y: 0.18804926416866716 },
- { x: 0.5632183908045977, y: 0.18136937689176494 },
- { x: 0.558109833971903, y: 0.17468948961486275 },
- { x: 0.5555555555555556, y: 0.17468948961486275 },
- { x: 0.5517241379310345, y: 0.18003339943638452 },
- ],
- name: "circle",
- },
- {
- points: [
- { x: 0.454661558109834, y: 0.2748877987683958 },
- { x: 0.4495530012771392, y: 0.2748877987683958 },
- { x: 0.4495530012771392, y: 0.2762237762237762 },
- { x: 0.44316730523627074, y: 0.27755975367915664 },
- { x: 0.4355044699872286, y: 0.28691159586681975 },
- { x: 0.4367816091954023, y: 0.3109591900636677 },
- { x: 0.44061302681992337, y: 0.31897505479595034 },
- { x: 0.44572158365261816, y: 0.3203110322513308 },
- { x: 0.44572158365261816, y: 0.3216470097067112 },
- { x: 0.46360153256704983, y: 0.3203110322513308 },
- { x: 0.46360153256704983, y: 0.31897505479595034 },
- { x: 0.47126436781609193, y: 0.31630309988518945 },
- { x: 0.47509578544061304, y: 0.3109591900636677 },
- { x: 0.4776500638569604, y: 0.2882475733222002 },
- { x: 0.47509578544061304, y: 0.28691159586681975 },
- { x: 0.47381864623243936, y: 0.28156768604529797 },
- { x: 0.46998722860791825, y: 0.27755975367915664 },
- { x: 0.45849297573435505, y: 0.27755975367915664 },
- ],
- name: "circle",
- },
- {
- points: [
- { x: 0.49936143039591313, y: 0.4098215217618203 },
- { x: 0.4955300127713921, y: 0.4098215217618203 },
- { x: 0.48148148148148145, y: 0.41650140903872246 },
- { x: 0.47126436781609193, y: 0.42585325122638557 },
- { x: 0.4674329501915709, y: 0.4338691159586682 },
- { x: 0.4661558109833972, y: 0.44589291305709217 },
- { x: 0.4674329501915709, y: 0.44589291305709217 },
- { x: 0.4674329501915709, y: 0.44856486796785305 },
- { x: 0.46998722860791825, y: 0.44856486796785305 },
- { x: 0.46998722860791825, y: 0.45123682287861394 },
- { x: 0.47381864623243936, y: 0.4552447552447552 },
- { x: 0.4840357598978289, y: 0.46192464252165744 },
- { x: 0.4955300127713921, y: 0.46593257488779877 },
- { x: 0.5057471264367817, y: 0.46593257488779877 },
- { x: 0.51213282247765, y: 0.45925268761089655 },
- { x: 0.5172413793103449, y: 0.4499008454232335 },
- { x: 0.5210727969348659, y: 0.43921302578018995 },
- { x: 0.5223499361430396, y: 0.43921302578018995 },
- { x: 0.5223499361430396, y: 0.43119716104790734 },
- { x: 0.5159642401021711, y: 0.4231812963156247 },
- { x: 0.5095785440613027, y: 0.40848554430643985 },
- { x: 0.5057471264367817, y: 0.40581358939567896 },
- { x: 0.5057471264367817, y: 0.4031416344849181 },
- { x: 0.5019157088122606, y: 0.4031416344849181 },
- ],
- name: "circle",
- },
- {
- points: [
- { x: 0.2413793103448276, y: 0.3643982882788853 },
- { x: 0.23754789272030652, y: 0.3643982882788853 },
- { x: 0.23754789272030652, y: 0.3657342657342657 },
- { x: 0.22860791826309068, y: 0.3684062206450266 },
- { x: 0.22860791826309068, y: 0.36974219810040704 },
- { x: 0.2247765006385696, y: 0.36974219810040704 },
- { x: 0.2247765006385696, y: 0.3710781755557875 },
- { x: 0.21966794380587484, y: 0.37375013046654837 },
- { x: 0.22094508301404853, y: 0.3951257697526354 },
- { x: 0.22349936143039592, y: 0.3977977246633963 },
- { x: 0.22349936143039592, y: 0.4004696795741572 },
- { x: 0.23116219667943805, y: 0.40848554430643985 },
- { x: 0.24010217113665389, y: 0.41382945412796157 },
- { x: 0.24010217113665389, y: 0.415165431583342 },
- { x: 0.2541507024265645, y: 0.415165431583342 },
- { x: 0.25925925925925924, y: 0.4098215217618203 },
- { x: 0.26181353767560667, y: 0.4098215217618203 },
- { x: 0.26309067688378035, y: 0.40581358939567896 },
- { x: 0.26436781609195403, y: 0.40581358939567896 },
- { x: 0.26436781609195403, y: 0.40180565702953763 },
- { x: 0.2656449553001277, y: 0.40180565702953763 },
- { x: 0.2656449553001277, y: 0.3804300177434506 },
- { x: 0.24904214559386972, y: 0.36172633336812443 },
- { x: 0.2388250319284802, y: 0.360390355912744 },
- ],
- name: "circle",
- },
- {
- points: [
- { x: 0.2796934865900383, y: 0.5220436280137772 },
- { x: 0.2822477650063857, y: 0.5153637407368751 },
- { x: 0.28735632183908044, y: 0.5126917858261142 },
- { x: 0.2886334610472541, y: 0.5086838534599729 },
- { x: 0.2886334610472541, y: 0.5033399436384511 },
- { x: 0.2835249042145594, y: 0.4993320112723098 },
- { x: 0.2681992337164751, y: 0.4993320112723098 },
- { x: 0.26181353767560667, y: 0.506011898549212 },
- { x: 0.26181353767560667, y: 0.518035695647636 },
- { x: 0.26309067688378035, y: 0.518035695647636 },
- { x: 0.26309067688378035, y: 0.5207076505583969 },
- { x: 0.2656449553001277, y: 0.5233796054691577 },
- { x: 0.27458492975734355, y: 0.5233796054691577 },
- { x: 0.27458492975734355, y: 0.5207076505583969 },
- ],
- name: "circle",
- },
- {
- points: [
- { x: 0.4904214559386973, y: 0.5126917858261142 },
- { x: 0.4827586206896552, y: 0.5126917858261142 },
- { x: 0.47381864623243936, y: 0.5220436280137772 },
- { x: 0.4674329501915709, y: 0.5300594927460599 },
- { x: 0.45338441890166026, y: 0.5567790418536687 },
- { x: 0.4482758620689655, y: 0.5741467487736145 },
- { x: 0.4482758620689655, y: 0.596858365515082 },
- { x: 0.4495530012771392, y: 0.5995303204258429 },
- { x: 0.4648786717752235, y: 0.618234004801169 },
- { x: 0.47381864623243936, y: 0.6262498695334516 },
- { x: 0.4840357598978289, y: 0.6329297568103538 },
- { x: 0.49936143039591313, y: 0.6396096440872561 },
- { x: 0.51213282247765, y: 0.642281598998017 },
- { x: 0.5351213282247765, y: 0.642281598998017 },
- { x: 0.5542784163473818, y: 0.6382736666318756 },
- { x: 0.565772669220945, y: 0.6329297568103538 },
- { x: 0.5772669220945083, y: 0.6155620498904081 },
- { x: 0.5823754789272031, y: 0.5928504331489406 },
- { x: 0.5823754789272031, y: 0.5634589291305709 },
- { x: 0.5747126436781609, y: 0.539411334933723 },
- { x: 0.561941251596424, y: 0.5220436280137772 },
- { x: 0.5504469987228607, y: 0.5113558083707337 },
- { x: 0.5478927203065134, y: 0.5113558083707337 },
- { x: 0.5478927203065134, y: 0.5100198309153533 },
- { x: 0.5146871008939975, y: 0.5086838534599729 },
- { x: 0.5134099616858238, y: 0.5073478760045924 },
- { x: 0.4929757343550447, y: 0.5073478760045924 },
- { x: 0.49169859514687103, y: 0.5100198309153533 },
- ],
- name: "circle",
- },
- {
- points: [
- { x: 0.7203065134099617, y: 0.4672685523431792 },
- { x: 0.7100893997445722, y: 0.4672685523431792 },
- { x: 0.7037037037037037, y: 0.4699405072539401 },
- { x: 0.6883780332056194, y: 0.4779563719862227 },
- { x: 0.6768837803320562, y: 0.4873082141738858 },
- { x: 0.6641123882503193, y: 0.5033399436384511 },
- { x: 0.6564495530012772, y: 0.5260515603799186 },
- { x: 0.6551724137931034, y: 0.5581150193090492 },
- { x: 0.6590038314176245, y: 0.5688028389520927 },
- { x: 0.669220945083014, y: 0.5875065233274188 },
- { x: 0.6794380587484036, y: 0.5981943429704624 },
- { x: 0.6819923371647509, y: 0.5981943429704624 },
- { x: 0.6909323116219668, y: 0.6035382527919841 },
- { x: 0.7062579821200511, y: 0.6075461851581254 },
- { x: 0.735632183908046, y: 0.6075461851581254 },
- { x: 0.7369093231162197, y: 0.606210207702745 },
- { x: 0.7471264367816092, y: 0.6048742302473645 },
- { x: 0.7522349936143039, y: 0.6022022753366036 },
- { x: 0.7624521072796935, y: 0.6008662978812233 },
- { x: 0.7739463601532567, y: 0.5941864106043211 },
- { x: 0.7790549169859514, y: 0.5928504331489406 },
- { x: 0.7828863346104725, y: 0.5888425007827993 },
- { x: 0.7854406130268199, y: 0.5888425007827993 },
- { x: 0.7969348659003831, y: 0.5768187036843754 },
- { x: 0.8058748403575989, y: 0.56078697421981 },
- { x: 0.8058748403575989, y: 0.5567790418536687 },
- { x: 0.8071519795657727, y: 0.5567790418536687 },
- { x: 0.8071519795657727, y: 0.551435132032147 },
- { x: 0.80970625798212, y: 0.5434192672998643 },
- { x: 0.80970625798212, y: 0.5207076505583969 },
- { x: 0.8084291187739464, y: 0.5207076505583969 },
- { x: 0.8071519795657727, y: 0.5153637407368751 },
- { x: 0.7994891443167306, y: 0.5033399436384511 },
- { x: 0.789272030651341, y: 0.4899801690846467 },
- { x: 0.7803320561941252, y: 0.48597223671850537 },
- ],
- name: "circle",
- },
- {
- points: [
- { x: 0.6934865900383141, y: 0.3309988518943743 },
- { x: 0.669220945083014, y: 0.3309988518943743 },
- { x: 0.665389527458493, y: 0.3283268969836134 },
- { x: 0.6590038314176245, y: 0.32699091952823295 },
- { x: 0.6513409961685823, y: 0.3216470097067112 },
- { x: 0.6424010217113666, y: 0.30962321260828723 },
- { x: 0.6398467432950191, y: 0.302943325331385 },
- { x: 0.6385696040868455, y: 0.2829036635006784 },
- { x: 0.6462324393358876, y: 0.2628640016699718 },
- { x: 0.648786717752235, y: 0.26152802421459137 },
- { x: 0.6500638569604087, y: 0.25752009184845004 },
- { x: 0.6590038314176245, y: 0.24816824966078696 },
- { x: 0.6730523627075351, y: 0.24015238492850433 },
- { x: 0.6832694763729247, y: 0.23748043001774344 },
- { x: 0.6896551724137931, y: 0.23614445256236302 },
- { x: 0.7126436781609196, y: 0.23614445256236302 },
- { x: 0.7343550446998723, y: 0.24683227220540654 },
- { x: 0.7471264367816092, y: 0.2655359565807327 },
- { x: 0.7509578544061303, y: 0.2762237762237762 },
- { x: 0.7509578544061303, y: 0.314967122429809 },
- { x: 0.7496807151979565, y: 0.3176390773405699 },
- { x: 0.7369093231162197, y: 0.33367080680513517 },
- { x: 0.7343550446998723, y: 0.33367080680513517 },
- { x: 0.7318007662835249, y: 0.33634276171589605 },
- { x: 0.7254150702426565, y: 0.33634276171589605 },
- { x: 0.7254150702426565, y: 0.3376787391712765 },
- { x: 0.7164750957854407, y: 0.3376787391712765 },
- ],
- name: "circle",
- },
- {
- points: [
- { x: 0.1583652618135377, y: 0.6262498695334516 },
- { x: 0.1545338441890166, y: 0.6222419371673104 },
- { x: 0.15070242656449553, y: 0.6115541175242668 },
- { x: 0.14942528735632185, y: 0.6115541175242668 },
- { x: 0.14687100893997446, y: 0.5888425007827993 },
- { x: 0.14814814814814814, y: 0.5888425007827993 },
- { x: 0.1532567049808429, y: 0.5794906585951362 },
- { x: 0.16219667943805874, y: 0.5714747938628536 },
- { x: 0.17879948914431673, y: 0.5634589291305709 },
- { x: 0.20434227330779056, y: 0.5634589291305709 },
- { x: 0.21455938697318008, y: 0.5674668614967122 },
- { x: 0.2247765006385696, y: 0.5741467487736145 },
- { x: 0.23371647509578544, y: 0.5888425007827993 },
- { x: 0.23499361430395913, y: 0.5888425007827993 },
- { x: 0.23499361430395913, y: 0.5915144556935602 },
- { x: 0.23627075351213284, y: 0.5915144556935602 },
- { x: 0.23627075351213284, y: 0.6168980273457886 },
- { x: 0.23243933588761176, y: 0.627585846988832 },
- { x: 0.22349936143039592, y: 0.6436175764533973 },
- { x: 0.21711366538952745, y: 0.6502974637302995 },
- { x: 0.2120051085568327, y: 0.65163344118568 },
- { x: 0.2120051085568327, y: 0.6529694186410604 },
- { x: 0.20178799489144317, y: 0.6556413735518213 },
- { x: 0.1826309067688378, y: 0.6556413735518213 },
- { x: 0.17752234993614305, y: 0.6529694186410604 },
- { x: 0.1698595146871009, y: 0.6449535539087778 },
- { x: 0.1698595146871009, y: 0.6396096440872561 },
- ],
- name: "circle",
- },
- {
- points: [
- { x: 0.11749680715197956, y: 0.1399540757749713 },
- { x: 0.10600255427841634, y: 0.1399540757749713 },
- { x: 0.10217113665389528, y: 0.14262603068573218 },
- { x: 0.09578544061302682, y: 0.14396200814111262 },
- { x: 0.09195402298850575, y: 0.14796994050725393 },
- { x: 0.09195402298850575, y: 0.15064189541801482 },
- { x: 0.08939974457215837, y: 0.15197787287339526 },
- { x: 0.08812260536398467, y: 0.16533764742719967 },
- { x: 0.09450830140485313, y: 0.16800960233796056 },
- { x: 0.09450830140485313, y: 0.169345579793341 },
- { x: 0.10089399744572158, y: 0.1733535121594823 },
- { x: 0.10472541507024266, y: 0.1733535121594823 },
- { x: 0.1123882503192848, y: 0.17736144452562363 },
- { x: 0.12005108556832694, y: 0.17736144452562363 },
- { x: 0.12260536398467432, y: 0.17068155724872142 },
- { x: 0.12388250319284802, y: 0.17068155724872142 },
- { x: 0.12388250319284802, y: 0.16800960233796056 },
- { x: 0.1251596424010217, y: 0.16800960233796056 },
- { x: 0.12899106002554278, y: 0.16132971506105834 },
- { x: 0.12899106002554278, y: 0.15064189541801482 },
- { x: 0.1251596424010217, y: 0.1466339630518735 },
- { x: 0.12005108556832694, y: 0.14529798559649307 },
- ],
- name: "circle",
- },
- {
- points: [
- { x: 0.3090676883780332, y: 0.2227846780085586 },
- { x: 0.3090676883780332, y: 0.21744076818703684 },
- { x: 0.30779054916985954, y: 0.21744076818703684 },
- { x: 0.30779054916985954, y: 0.21476881327627595 },
- { x: 0.3052362707535121, y: 0.2120968583655151 },
- { x: 0.3052362707535121, y: 0.1933931739901889 },
- { x: 0.30779054916985954, y: 0.18003339943638452 },
- { x: 0.31928480204342274, y: 0.16800960233796056 },
- { x: 0.32950191570881227, y: 0.16800960233796056 },
- { x: 0.3333333333333333, y: 0.17468948961486275 },
- { x: 0.3333333333333333, y: 0.17736144452562363 },
- { x: 0.334610472541507, y: 0.17736144452562363 },
- { x: 0.334610472541507, y: 0.18003339943638452 },
- { x: 0.3371647509578544, y: 0.18136937689176494 },
- { x: 0.3397190293742018, y: 0.1867132867132867 },
- { x: 0.34099616858237547, y: 0.1960651289009498 },
- { x: 0.34227330779054915, y: 0.1960651289009498 },
- { x: 0.34227330779054915, y: 0.2254566329193195 },
- { x: 0.334610472541507, y: 0.22679261037469992 },
- { x: 0.31928480204342274, y: 0.23347249765160213 },
- { x: 0.3167305236270754, y: 0.23347249765160213 },
- { x: 0.3167305236270754, y: 0.2294645652854608 },
- ],
- name: "circle",
- },
- {
- points: [
- { x: 0.07790549169859515, y: 0.24683227220540654 },
- { x: 0.08045977011494253, y: 0.24683227220540654 },
- { x: 0.08045977011494253, y: 0.2454962947500261 },
- { x: 0.08301404853128991, y: 0.2454962947500261 },
- { x: 0.08684546615581099, y: 0.24148836238388477 },
- { x: 0.09195402298850575, y: 0.24015238492850433 },
- { x: 0.09195402298850575, y: 0.23881640747312388 },
- { x: 0.09578544061302682, y: 0.23881640747312388 },
- { x: 0.09578544061302682, y: 0.23748043001774344 },
- { x: 0.12132822477650064, y: 0.23614445256236302 },
- { x: 0.13409961685823754, y: 0.2508402045715479 },
- { x: 0.13409961685823754, y: 0.25484813693768915 },
- { x: 0.13537675606641125, y: 0.25484813693768915 },
- { x: 0.13537675606641125, y: 0.2708798664022545 },
- { x: 0.13026819923371646, y: 0.2802317085899175 },
- { x: 0.13026819923371646, y: 0.2829036635006784 },
- { x: 0.1277139208173691, y: 0.28423964095605886 },
- { x: 0.12388250319284802, y: 0.2909195282329611 },
- { x: 0.12132822477650064, y: 0.2909195282329611 },
- { x: 0.12132822477650064, y: 0.2922555056883415 },
- { x: 0.11494252873563218, y: 0.2949274605991024 },
- { x: 0.1111111111111111, y: 0.2949274605991024 },
- { x: 0.1111111111111111, y: 0.29626343805448285 },
- { x: 0.08939974457215837, y: 0.29626343805448285 },
- { x: 0.08045977011494253, y: 0.28691159586681975 },
- { x: 0.07790549169859515, y: 0.2762237762237762 },
- { x: 0.07662835249042145, y: 0.2762237762237762 },
- { x: 0.07662835249042145, y: 0.2655359565807327 },
- ],
- name: "circle",
- },
- {
- points: [
- { x: 0.3524904214559387, y: 0.7211042688654629 },
- { x: 0.3524904214559387, y: 0.7157603590439411 },
- { x: 0.3486590038314176, y: 0.7117524266777998 },
- { x: 0.3486590038314176, y: 0.7090804717670389 },
- { x: 0.34738186462324394, y: 0.7090804717670389 },
- { x: 0.34355044699872284, y: 0.6917127648470932 },
- { x: 0.3397190293742018, y: 0.6823609226594302 },
- { x: 0.3397190293742018, y: 0.6556413735518213 },
- { x: 0.3486590038314176, y: 0.6396096440872561 },
- { x: 0.3537675606641124, y: 0.6342657342657343 },
- { x: 0.3614303959131545, y: 0.6289218244442125 },
- { x: 0.3716475095785441, y: 0.627585846988832 },
- { x: 0.3831417624521073, y: 0.6396096440872561 },
- { x: 0.388250319284802, y: 0.6489614862749191 },
- { x: 0.3895274584929757, y: 0.6489614862749191 },
- { x: 0.39719029374201786, y: 0.6743450579271475 },
- { x: 0.39719029374201786, y: 0.697056674668615 },
- { x: 0.3946360153256705, y: 0.7037365619455171 },
- { x: 0.38697318007662834, y: 0.7117524266777998 },
- { x: 0.3767560664112388, y: 0.7277841561423651 },
- { x: 0.3716475095785441, y: 0.7277841561423651 },
- { x: 0.3716475095785441, y: 0.7264481786869846 },
- ],
- name: "circle",
- },
- {
- points: [
- { x: 0.6283524904214559, y: 0.6356017117211147 },
- { x: 0.6104725415070242, y: 0.6356017117211147 },
- { x: 0.6040868454661558, y: 0.6436175764533973 },
- { x: 0.6015325670498084, y: 0.65163344118568 },
- { x: 0.6015325670498084, y: 0.6569773510072018 },
- { x: 0.598978288633461, y: 0.6623212608287236 },
- { x: 0.598978288633461, y: 0.6836969001148105 },
- { x: 0.6040868454661558, y: 0.6863688550255714 },
- { x: 0.6104725415070242, y: 0.6930487423024736 },
- { x: 0.6232439335887612, y: 0.7010646070347563 },
- { x: 0.6309067688378033, y: 0.7037365619455171 },
- { x: 0.6424010217113666, y: 0.7037365619455171 },
- { x: 0.644955300127714, y: 0.7024005844901368 },
- { x: 0.6526181353767561, y: 0.6943847197578541 },
- { x: 0.6564495530012772, y: 0.6836969001148105 },
- { x: 0.6590038314176245, y: 0.6810249452040497 },
- { x: 0.6602809706257982, y: 0.6569773510072018 },
- { x: 0.6590038314176245, y: 0.6569773510072018 },
- { x: 0.6577266922094508, y: 0.65163344118568 },
- { x: 0.6538952745849298, y: 0.6476255088195386 },
- { x: 0.6436781609195402, y: 0.6449535539087778 },
- ],
- name: "circle",
- },
- {
- points: [
- { x: 0.6730523627075351, y: 0.1372821208642104 },
- { x: 0.6666666666666666, y: 0.13461016595344955 },
- { x: 0.6666666666666666, y: 0.1332741884980691 },
- { x: 0.6590038314176245, y: 0.13193821104268866 },
- { x: 0.6551724137931034, y: 0.12793027867654733 },
- { x: 0.6551724137931034, y: 0.12525832376578644 },
- { x: 0.6500638569604087, y: 0.12125039139964514 },
- { x: 0.6500638569604087, y: 0.11323452666736249 },
- { x: 0.6513409961685823, y: 0.11323452666736249 },
- { x: 0.6538952745849298, y: 0.10789061684584073 },
- { x: 0.6551724137931034, y: 0.10121072956893852 },
- { x: 0.6577266922094508, y: 0.0972027972027972 },
- { x: 0.6717752234993615, y: 0.09586681974741676 },
- { x: 0.6730523627075351, y: 0.09853877465817765 },
- { x: 0.6807151979565773, y: 0.10121072956893852 },
- { x: 0.6845466155810983, y: 0.10521866193507985 },
- { x: 0.6883780332056194, y: 0.11991441394426469 },
- { x: 0.6883780332056194, y: 0.1332741884980691 },
- { x: 0.6832694763729247, y: 0.13461016595344955 },
- { x: 0.6832694763729247, y: 0.13594614340883 },
- { x: 0.6781609195402298, y: 0.13594614340883 },
- ],
- name: "circle",
- },
- {
- points: [
- { x: 0.1532567049808429, y: 0.7691994572591587 },
- { x: 0.1417624521072797, y: 0.7691994572591587 },
- { x: 0.13409961685823754, y: 0.7611835925268761 },
- { x: 0.13154533844189017, y: 0.7558396827053544 },
- { x: 0.13026819923371646, y: 0.7558396827053544 },
- { x: 0.12899106002554278, y: 0.7504957728838326 },
- { x: 0.1277139208173691, y: 0.7504957728838326 },
- { x: 0.12643678160919541, y: 0.7358000208746477 },
- { x: 0.13793103448275862, y: 0.7224402463208434 },
- { x: 0.1558109833971903, y: 0.7224402463208434 },
- { x: 0.16219667943805874, y: 0.7291201335977455 },
- { x: 0.16347381864623245, y: 0.7344640434192673 },
- { x: 0.16602809706257982, y: 0.7358000208746477 },
- { x: 0.1673052362707535, y: 0.7398079532407891 },
- { x: 0.1673052362707535, y: 0.763855547437637 },
- { x: 0.16475095785440613, y: 0.763855547437637 },
- { x: 0.15964240102171137, y: 0.7705354347145392 },
- { x: 0.15708812260536398, y: 0.7705354347145392 },
- ],
- name: "circle",
- },
- {
- points: [
- { x: 0.8467432950191571, y: 0.11323452666736249 },
- { x: 0.8263090676883781, y: 0.11323452666736249 },
- { x: 0.8122605363984674, y: 0.1172424590335038 },
- { x: 0.8033205619412516, y: 0.12125039139964514 },
- { x: 0.7918263090676884, y: 0.13060223358730821 },
- { x: 0.7777777777777778, y: 0.15197787287339526 },
- { x: 0.7713920817369093, y: 0.18136937689176494 },
- { x: 0.7713920817369093, y: 0.20541697108861287 },
- { x: 0.776500638569604, y: 0.22144870055317817 },
- { x: 0.7854406130268199, y: 0.23881640747312388 },
- { x: 0.7969348659003831, y: 0.2521761820269283 },
- { x: 0.8058748403575989, y: 0.25752009184845004 },
- { x: 0.8250319284802043, y: 0.2655359565807327 },
- { x: 0.8301404853128991, y: 0.2655359565807327 },
- { x: 0.8378033205619413, y: 0.2682079114914936 },
- { x: 0.8544061302681992, y: 0.2682079114914936 },
- { x: 0.879948914431673, y: 0.25351215948230876 },
- { x: 0.8863346104725415, y: 0.2454962947500261 },
- { x: 0.8876117496807152, y: 0.24015238492850433 },
- { x: 0.9029374201787995, y: 0.2161047907316564 },
- { x: 0.9042145593869731, y: 0.21076088091013465 },
- { x: 0.9054916985951469, y: 0.21076088091013465 },
- { x: 0.9067688378033205, y: 0.16800960233796056 },
- { x: 0.9029374201787995, y: 0.15732178269491703 },
- { x: 0.9016602809706258, y: 0.15732178269491703 },
- { x: 0.9003831417624522, y: 0.15197787287339526 },
- { x: 0.8978288633461047, y: 0.14930591796263437 },
- { x: 0.8978288633461047, y: 0.14529798559649307 },
- { x: 0.8939974457215837, y: 0.1372821208642104 },
- { x: 0.8812260536398467, y: 0.12392234631040601 },
- { x: 0.8697318007662835, y: 0.11991441394426469 },
- { x: 0.8582375478927203, y: 0.11323452666736249 },
- ],
- name: "circle",
- },
-];
diff --git a/src/helpers/shared.js b/src/helpers/shared.js
index aab9b6d..182272a 100644
--- a/src/helpers/shared.js
+++ b/src/helpers/shared.js
@@ -28,9 +28,10 @@ export function roundTo(x, to) {
return Math.round(x / to) * to;
}
-export function snapPositionToGrid(position, gridSize) {
- return {
- x: roundTo(position.x, gridSize.x),
- y: roundTo(position.y, gridSize.y),
- };
+export function toRadians(angle) {
+ return angle * (Math.PI / 180);
+}
+
+export function toDegrees(angle) {
+ return angle * (180 / Math.PI);
}
diff --git a/src/helpers/useDataSource.js b/src/helpers/useDataSource.js
new file mode 100644
index 0000000..9eed094
--- /dev/null
+++ b/src/helpers/useDataSource.js
@@ -0,0 +1,30 @@
+import { useEffect, useState } from "react";
+
+// Helper function to load either file or default data
+// into a URL and ensure that it is revoked if needed
+function useDataSource(data, defaultSources) {
+ const [dataSource, setDataSource] = useState(null);
+ useEffect(() => {
+ if (!data) {
+ setDataSource(null);
+ return;
+ }
+ let url = null;
+ if (data.type === "file") {
+ url = URL.createObjectURL(data.file);
+ } else if (data.type === "default") {
+ url = defaultSources[data.key];
+ }
+ setDataSource(url);
+
+ return () => {
+ if (data.type === "file" && url) {
+ URL.revokeObjectURL(url);
+ }
+ };
+ }, [data, defaultSources]);
+
+ return dataSource;
+}
+
+export default useDataSource;
diff --git a/src/helpers/useDebounce.js b/src/helpers/useDebounce.js
new file mode 100644
index 0000000..057068c
--- /dev/null
+++ b/src/helpers/useDebounce.js
@@ -0,0 +1,18 @@
+import { useEffect, useState } from "react";
+
+function useDebounce(value, delay) {
+ const [debouncedValue, setDebouncedValue] = useState(value);
+
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ setDebouncedValue(value);
+ }, delay);
+ return () => {
+ clearTimeout(timeout);
+ };
+ }, [value, delay]);
+
+ return debouncedValue;
+}
+
+export default useDebounce;
diff --git a/src/helpers/useNickname.js b/src/helpers/useNickname.js
deleted file mode 100644
index 04475da..0000000
--- a/src/helpers/useNickname.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { useEffect, useState } from "react";
-import { getRandomMonster } from "./monsters";
-
-function useNickname() {
- const [nickname, setNickname] = useState(
- localStorage.getItem("nickname") || getRandomMonster()
- );
-
- useEffect(() => {
- localStorage.setItem("nickname", nickname);
- }, [nickname]);
-
- return { nickname, setNickname };
-}
-
-export default useNickname;
diff --git a/src/helpers/usePrevious.js b/src/helpers/usePrevious.js
new file mode 100644
index 0000000..8b8ac74
--- /dev/null
+++ b/src/helpers/usePrevious.js
@@ -0,0 +1,11 @@
+import { useEffect, useRef } from "react";
+
+function usePrevious(value) {
+ const ref = useRef();
+ useEffect(() => {
+ ref.current = value;
+ }, [value]);
+ return ref.current;
+}
+
+export default usePrevious;
diff --git a/src/helpers/useSession.js b/src/helpers/useSession.js
index 0def7a4..164a829 100644
--- a/src/helpers/useSession.js
+++ b/src/helpers/useSession.js
@@ -6,7 +6,7 @@ import Peer from "../helpers/Peer";
import AuthContext from "../contexts/AuthContext";
-const socket = io("https://broker.owlbear.rodeo");
+const socket = io("https://agent.owlbear.rodeo");
function useSession(
partyId,
diff --git a/src/helpers/vector2.js b/src/helpers/vector2.js
index e0ee334..41be521 100644
--- a/src/helpers/vector2.js
+++ b/src/helpers/vector2.js
@@ -1,3 +1,5 @@
+import { toRadians, roundTo as roundToNumber } from "./shared";
+
export function lengthSquared(p) {
return p.x * p.x + p.y * p.y;
}
@@ -8,7 +10,7 @@ export function length(p) {
export function normalize(p) {
const l = length(p);
- return { x: p.x / l, y: p.y / l };
+ return divide(p, l);
}
export function dot(a, b) {
@@ -16,5 +18,201 @@ export function dot(a, b) {
}
export function subtract(a, b) {
- return { x: a.x - b.x, y: a.y - b.y };
+ 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 };
+ }
+}
+
+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 };
+ }
+}
+
+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 };
+ }
+}
+
+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 };
+ }
+}
+
+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,
+ };
+}
+
+export function rotateDirection(direction, angle) {
+ return rotate(direction, { x: 0, y: 0 }, angle);
+}
+
+export function min(a) {
+ return a.x < a.y ? a.x : a.y;
+}
+
+export function max(a) {
+ return a.x > a.y ? a.x : a.y;
+}
+
+export function roundTo(p, to) {
+ return {
+ x: roundToNumber(p.x, to.x),
+ y: roundToNumber(p.y, to.y),
+ };
+}
+
+export function sign(a) {
+ return { x: Math.sign(a.x), y: Math.sign(a.y) };
+}
+
+export function abs(a) {
+ return { x: Math.abs(a.x), y: Math.abs(a.y) };
+}
+
+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) };
+ }
+}
+
+export function dot2(a) {
+ return dot(a, a);
+}
+
+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),
+ };
+}
+
+// https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d
+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 };
+}
+
+// TODO: Fix the robustness of this to allow smoothing on fog layers
+// https://www.shadertoy.com/view/MlKcDD
+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));
+ } else {
+ point = add(d, multiply(add(c, multiply(b, t.y)), t.y));
+ }
+ }
+
+ return { distance: Math.sqrt(distance), point: point };
+}
+
+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 };
+}
+
+// Check bounds then use ray casting algorithm
+// https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm
+// https://stackoverflow.com/questions/217578/how-can-i-determine-whether-a-2d-point-is-within-a-polygon/2922778
+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;
+ }
+ }
+ 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;
}
diff --git a/src/icons/AddIcon.js b/src/icons/AddIcon.js
new file mode 100644
index 0000000..88db9d1
--- /dev/null
+++ b/src/icons/AddIcon.js
@@ -0,0 +1,20 @@
+import React from "react";
+
+function AddIcon({ large }) {
+ return (
+
+ );
+}
+
+AddIcon.defaultProps = { large: false };
+
+export default AddIcon;
diff --git a/src/icons/AddMapIcon.js b/src/icons/AddMapIcon.js
deleted file mode 100644
index aebdf6a..0000000
--- a/src/icons/AddMapIcon.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from "react";
-
-function AddMapIcon() {
- return (
-
- );
-}
-
-export default AddMapIcon;
diff --git a/src/icons/BlendOnIcon.js b/src/icons/BlendOnIcon.js
index 68c680a..9382f9a 100644
--- a/src/icons/BlendOnIcon.js
+++ b/src/icons/BlendOnIcon.js
@@ -9,8 +9,8 @@ function BlendOnIcon() {
width="24"
fill="currentcolor"
>
-
-
+
+
);
}
diff --git a/src/icons/BrushFillIcon.js b/src/icons/BrushFillIcon.js
new file mode 100644
index 0000000..b0776df
--- /dev/null
+++ b/src/icons/BrushFillIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function BrushFillIcon() {
+ return (
+
+ );
+}
+
+export default BrushFillIcon;
diff --git a/src/icons/BrushStrokeIcon.js b/src/icons/BrushStrokeIcon.js
new file mode 100644
index 0000000..f626cb7
--- /dev/null
+++ b/src/icons/BrushStrokeIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function BrushStrokeIcon() {
+ return (
+
+ );
+}
+
+export default BrushStrokeIcon;
diff --git a/src/icons/ExpandMoreDotIcon.js b/src/icons/ExpandMoreDotIcon.js
new file mode 100644
index 0000000..6259f07
--- /dev/null
+++ b/src/icons/ExpandMoreDotIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function ExpandMoreDotIcon() {
+ return (
+
+ );
+}
+
+export default ExpandMoreDotIcon;
diff --git a/src/icons/FogAddIcon.js b/src/icons/FogAddIcon.js
new file mode 100644
index 0000000..aa21d79
--- /dev/null
+++ b/src/icons/FogAddIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function FogAddIcon() {
+ return (
+
+ );
+}
+
+export default FogAddIcon;
diff --git a/src/icons/FogRemoveIcon.js b/src/icons/FogRemoveIcon.js
new file mode 100644
index 0000000..af6c2e6
--- /dev/null
+++ b/src/icons/FogRemoveIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function FogRemoveIcon() {
+ return (
+
+ );
+}
+
+export default FogRemoveIcon;
diff --git a/src/icons/FogToggleIcon.js b/src/icons/FogToggleIcon.js
new file mode 100644
index 0000000..569288b
--- /dev/null
+++ b/src/icons/FogToggleIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function FogToggleIcon() {
+ return (
+
+ );
+}
+
+export default FogToggleIcon;
diff --git a/src/icons/FogToolIcon.js b/src/icons/FogToolIcon.js
new file mode 100644
index 0000000..d81bd89
--- /dev/null
+++ b/src/icons/FogToolIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function FogToolIcon() {
+ return (
+
+ );
+}
+
+export default FogToolIcon;
diff --git a/src/icons/GridOffIcon.js b/src/icons/GridOffIcon.js
index d7b2950..a1287c2 100644
--- a/src/icons/GridOffIcon.js
+++ b/src/icons/GridOffIcon.js
@@ -8,8 +8,6 @@ function GridOffIcon() {
viewBox="0 0 24 24"
width="24"
fill="currentcolor"
- // Fixes bug with not firing click event when used in a button
- style={{ pointerEvents: "none" }}
>
diff --git a/src/icons/GridOnIcon.js b/src/icons/GridOnIcon.js
index 0511b25..1796b30 100644
--- a/src/icons/GridOnIcon.js
+++ b/src/icons/GridOnIcon.js
@@ -8,8 +8,6 @@ function GridOnIcon() {
viewBox="0 0 24 24"
width="24"
fill="currentcolor"
- // Fixes bug with not firing click event when used in a button
- style={{ pointerEvents: "none" }}
>
diff --git a/src/icons/RemoveMapIcon.js b/src/icons/RemoveMapIcon.js
new file mode 100644
index 0000000..a7dd3f3
--- /dev/null
+++ b/src/icons/RemoveMapIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function RemoveMapIcon() {
+ return (
+
+ );
+}
+
+export default RemoveMapIcon;
diff --git a/src/icons/ResetMapIcon.js b/src/icons/ResetMapIcon.js
new file mode 100644
index 0000000..02ac13b
--- /dev/null
+++ b/src/icons/ResetMapIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function ResetMapIcon() {
+ return (
+
+ );
+}
+
+export default ResetMapIcon;
diff --git a/src/icons/SelectMapIcon.js b/src/icons/SelectMapIcon.js
new file mode 100644
index 0000000..dbfd967
--- /dev/null
+++ b/src/icons/SelectMapIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function SelectMapIcon() {
+ return (
+
+ );
+}
+
+export default SelectMapIcon;
diff --git a/src/icons/SettingsIcon.js b/src/icons/SettingsIcon.js
new file mode 100644
index 0000000..13bceb9
--- /dev/null
+++ b/src/icons/SettingsIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function SettingsIcon() {
+ return (
+
+ );
+}
+
+export default SettingsIcon;
diff --git a/src/icons/ShapeCircleIcon.js b/src/icons/ShapeCircleIcon.js
new file mode 100644
index 0000000..c393782
--- /dev/null
+++ b/src/icons/ShapeCircleIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function ShapeCircleIcon() {
+ return (
+
+ );
+}
+
+export default ShapeCircleIcon;
diff --git a/src/icons/ShapeRectangleIcon.js b/src/icons/ShapeRectangleIcon.js
new file mode 100644
index 0000000..55d00ee
--- /dev/null
+++ b/src/icons/ShapeRectangleIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function ShapeRectangleIcon() {
+ return (
+
+ );
+}
+
+export default ShapeRectangleIcon;
diff --git a/src/icons/ShapeToolIcon.js b/src/icons/ShapeToolIcon.js
new file mode 100644
index 0000000..aa4398f
--- /dev/null
+++ b/src/icons/ShapeToolIcon.js
@@ -0,0 +1,20 @@
+import React from "react";
+
+function ShapeToolIcon() {
+ return (
+
+ );
+}
+
+export default ShapeToolIcon;
diff --git a/src/icons/ShapeTriangleIcon.js b/src/icons/ShapeTriangleIcon.js
new file mode 100644
index 0000000..86a29d2
--- /dev/null
+++ b/src/icons/ShapeTriangleIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function ShapeTriangleIcon() {
+ return (
+
+ );
+}
+
+export default ShapeTriangleIcon;
diff --git a/src/icons/SnappingOffIcon.js b/src/icons/SnappingOffIcon.js
new file mode 100644
index 0000000..a51c22b
--- /dev/null
+++ b/src/icons/SnappingOffIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function SnappingOffIcon() {
+ return (
+
+ );
+}
+
+export default SnappingOffIcon;
diff --git a/src/icons/SnappingOnIcon.js b/src/icons/SnappingOnIcon.js
new file mode 100644
index 0000000..355831c
--- /dev/null
+++ b/src/icons/SnappingOnIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function SnappingOnIcon() {
+ return (
+
+ );
+}
+
+export default SnappingOnIcon;
diff --git a/src/images/DiagonalPattern.png b/src/images/DiagonalPattern.png
new file mode 100644
index 0000000..5d44de9
Binary files /dev/null and b/src/images/DiagonalPattern.png differ
diff --git a/src/maps/Blank Grid 22x22.jpg b/src/maps/Blank Grid 22x22.jpg
new file mode 100755
index 0000000..317b171
Binary files /dev/null and b/src/maps/Blank Grid 22x22.jpg differ
diff --git a/src/maps/Grass Grid 22x22.jpg b/src/maps/Grass Grid 22x22.jpg
new file mode 100755
index 0000000..93a20fc
Binary files /dev/null and b/src/maps/Grass Grid 22x22.jpg differ
diff --git a/src/maps/Sand Grid 22x22.jpg b/src/maps/Sand Grid 22x22.jpg
new file mode 100755
index 0000000..41995c3
Binary files /dev/null and b/src/maps/Sand Grid 22x22.jpg differ
diff --git a/src/maps/Stone Grid 22x22.jpg b/src/maps/Stone Grid 22x22.jpg
new file mode 100755
index 0000000..1748205
Binary files /dev/null and b/src/maps/Stone Grid 22x22.jpg differ
diff --git a/src/maps/Water Grid 22x22.jpg b/src/maps/Water Grid 22x22.jpg
new file mode 100755
index 0000000..83fbc3e
Binary files /dev/null and b/src/maps/Water Grid 22x22.jpg differ
diff --git a/src/maps/Wood Grid 22x22.jpg b/src/maps/Wood Grid 22x22.jpg
new file mode 100755
index 0000000..5aae7a6
Binary files /dev/null and b/src/maps/Wood Grid 22x22.jpg differ
diff --git a/src/maps/index.js b/src/maps/index.js
new file mode 100644
index 0000000..9fe93d7
--- /dev/null
+++ b/src/maps/index.js
@@ -0,0 +1,25 @@
+import blankImage from "./Blank Grid 22x22.jpg";
+import grassImage from "./Grass Grid 22x22.jpg";
+import sandImage from "./Sand Grid 22x22.jpg";
+import stoneImage from "./Stone Grid 22x22.jpg";
+import waterImage from "./Water Grid 22x22.jpg";
+import woodImage from "./Wood Grid 22x22.jpg";
+
+export const mapSources = {
+ blank: blankImage,
+ grass: grassImage,
+ sand: sandImage,
+ stone: stoneImage,
+ water: waterImage,
+ wood: woodImage,
+};
+
+export const maps = Object.keys(mapSources).map((key) => ({
+ key,
+ name: key.charAt(0).toUpperCase() + key.slice(1),
+ gridX: 22,
+ gridY: 22,
+ width: 1024,
+ height: 1024,
+ type: "default",
+}));
diff --git a/src/modals/AddMapModal.js b/src/modals/AddMapModal.js
deleted file mode 100644
index dedd74c..0000000
--- a/src/modals/AddMapModal.js
+++ /dev/null
@@ -1,179 +0,0 @@
-import React, { useRef, useState } from "react";
-import {
- Box,
- Button,
- Image as UIImage,
- Flex,
- Label,
- Input,
- Text,
-} from "theme-ui";
-
-import Modal from "../components/Modal";
-
-function AddMapModal({
- isOpen,
- onRequestClose,
- onDone,
- onImageUpload,
- gridX,
- onGridXChange,
- gridY,
- onGridYChange,
- imageLoaded,
- mapSource,
-}) {
- const fileInputRef = useRef();
-
- function handleImageUpload(file) {
- if (file.name) {
- // Match against a regex to find the grid size in the file name
- // e.g. Cave 22x23 will return [["22x22", "22", "x", "23"]]
- const gridMatches = [...file.name.matchAll(/(\d+) ?(x|X) ?(\d+)/g)];
- if (gridMatches.length > 0) {
- const lastMatch = gridMatches[gridMatches.length - 1];
- const matchX = parseInt(lastMatch[1]);
- const matchY = parseInt(lastMatch[3]);
- if (!isNaN(matchX) && !isNaN(matchY)) {
- onImageUpload(file, matchX, matchY);
- return;
- }
- }
- }
- onImageUpload(file);
- }
-
- function openImageDialog() {
- if (fileInputRef.current) {
- fileInputRef.current.click();
- }
- }
-
- const [dragging, setDragging] = useState(false);
- function handleImageDragEnter(event) {
- event.preventDefault();
- event.stopPropagation();
- setDragging(true);
- }
-
- function handleImageDragLeave(event) {
- event.preventDefault();
- event.stopPropagation();
- setDragging(false);
- }
-
- function handleImageDrop(event) {
- event.preventDefault();
- event.stopPropagation();
- const file = event.dataTransfer.files[0];
- if (file && file.type.startsWith("image")) {
- handleImageUpload(file);
- }
- setDragging(false);
- }
-
- return (
-
- {
- e.preventDefault();
- onDone();
- }}
- onDragEnter={handleImageDragEnter}
- >
- handleImageUpload(event.target.files[0])}
- type="file"
- accept="image/*"
- style={{ display: "none" }}
- ref={fileInputRef}
- />
-
-
-
-
-
-
-
- onGridXChange(e.target.value)}
- />
-
-
-
- onGridYChange(e.target.value)}
- />
-
-
- {mapSource ? (
-
- ) : (
-
- )}
- {dragging && (
- {
- e.preventDefault();
- e.stopPropagation();
- e.dataTransfer.dropEffect = "copy";
- }}
- onDrop={handleImageDrop}
- >
- Drop map to upload
-
- )}
-
-
-
- );
-}
-
-export default AddMapModal;
diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js
new file mode 100644
index 0000000..89083e2
--- /dev/null
+++ b/src/modals/SelectMapModal.js
@@ -0,0 +1,346 @@
+import React, { useRef, useState, useEffect, useContext } from "react";
+import { Box, Button, Flex, Label, Text } from "theme-ui";
+import shortid from "shortid";
+
+import db from "../database";
+
+import Modal from "../components/Modal";
+import MapTiles from "../components/map/MapTiles";
+import MapSettings from "../components/map/MapSettings";
+
+import AuthContext from "../contexts/AuthContext";
+
+import usePrevious from "../helpers/usePrevious";
+
+import { maps as defaultMaps } from "../maps";
+
+const defaultMapSize = 22;
+const defaultMapState = {
+ tokens: {},
+ // An index into the draw actions array to which only actions before the
+ // index will be performed (used in undo and redo)
+ mapDrawActionIndex: -1,
+ mapDrawActions: [],
+ fogDrawActionIndex: -1,
+ fogDrawActions: [],
+ // Flags to determine what other people can edit
+ editFlags: ["drawing", "tokens"],
+};
+
+const defaultMapProps = {
+ // Grid type
+ // TODO: add support for hex horizontal and hex vertical
+ gridType: "grid",
+};
+
+function SelectMapModal({
+ isOpen,
+ onRequestClose,
+ onDone,
+ onMapChange,
+ onMapStateChange,
+ // The map currently being view in the map screen
+ currentMap,
+}) {
+ const { userId } = useContext(AuthContext);
+
+ const wasOpen = usePrevious(isOpen);
+
+ const [imageLoading, setImageLoading] = useState(false);
+
+ // The map selected in the modal
+ const [selectedMap, setSelectedMap] = useState(null);
+ const [selectedMapState, setSelectedMapState] = useState(null);
+ const [maps, setMaps] = useState([]);
+ // Load maps from the database and ensure state is properly setup
+ useEffect(() => {
+ if (!userId) {
+ return;
+ }
+ async function getDefaultMaps() {
+ const defaultMapsWithIds = [];
+ for (let i = 0; i < defaultMaps.length; i++) {
+ const defaultMap = defaultMaps[i];
+ const id = `__default-${defaultMap.name}`;
+ defaultMapsWithIds.push({
+ ...defaultMap,
+ id,
+ owner: userId,
+ // Emulate the time increasing to avoid sort errors
+ created: Date.now() + i,
+ lastModified: Date.now() + i,
+ ...defaultMapProps,
+ });
+ // Add a state for the map if there isn't one already
+ const state = await db.table("states").get(id);
+ if (!state) {
+ await db.table("states").add({ ...defaultMapState, mapId: id });
+ }
+ }
+ return defaultMapsWithIds;
+ }
+
+ async function loadMaps() {
+ let storedMaps = await db
+ .table("maps")
+ .where({ owner: userId })
+ .toArray();
+ const sortedMaps = storedMaps.sort((a, b) => b.created - a.created);
+ const defaultMapsWithIds = await getDefaultMaps();
+ const allMaps = [...sortedMaps, ...defaultMapsWithIds];
+ setMaps(allMaps);
+
+ // reload map state as is may have changed while the modal was closed
+ if (selectedMap) {
+ const state = await db.table("states").get(selectedMap.id);
+ if (state) {
+ setSelectedMapState(state);
+ }
+ }
+ }
+
+ if (!wasOpen && isOpen) {
+ loadMaps();
+ }
+ }, [userId, isOpen, wasOpen, selectedMap]);
+
+ const fileInputRef = useRef();
+
+ function handleImageUpload(file) {
+ if (!file) {
+ return;
+ }
+ let fileGridX = defaultMapSize;
+ let fileGridY = defaultMapSize;
+ let name = "Unknown Map";
+ if (file.name) {
+ // TODO: match all not supported on safari, find alternative
+ if (file.name.matchAll) {
+ // Match against a regex to find the grid size in the file name
+ // e.g. Cave 22x23 will return [["22x22", "22", "x", "23"]]
+ const gridMatches = [...file.name.matchAll(/(\d+) ?(x|X) ?(\d+)/g)];
+ if (gridMatches.length > 0) {
+ const lastMatch = gridMatches[gridMatches.length - 1];
+ const matchX = parseInt(lastMatch[1]);
+ const matchY = parseInt(lastMatch[3]);
+ if (!isNaN(matchX) && !isNaN(matchY)) {
+ fileGridX = matchX;
+ fileGridY = matchY;
+ }
+ }
+ }
+
+ // Remove file extension
+ name = file.name.replace(/\.[^/.]+$/, "");
+ // Removed grid size expression
+ name = name.replace(/(\[ ?|\( ?)?\d+ ?(x|X) ?\d+( ?\]| ?\))?/, "");
+ // Clean string
+ name = name.replace(/ +/g, " ");
+ name = name.trim();
+ }
+ let image = new Image();
+ setImageLoading(true);
+
+ // Copy file to avoid permissions issues
+ const copy = new Blob([file], { type: file.type });
+ // Create and load the image temporarily to get its dimensions
+ const url = URL.createObjectURL(copy);
+ image.onload = function () {
+ handleMapAdd({
+ file: copy,
+ name,
+ type: "file",
+ gridX: fileGridX,
+ gridY: fileGridY,
+ width: image.width,
+ height: image.height,
+ id: shortid.generate(),
+ created: Date.now(),
+ lastModified: Date.now(),
+ owner: userId,
+ ...defaultMapProps,
+ });
+ setImageLoading(false);
+ URL.revokeObjectURL(url);
+ };
+ image.src = url;
+ // Set file input to null to allow adding the same image 2 times in a row
+ fileInputRef.current.value = null;
+ }
+
+ function openImageDialog() {
+ if (fileInputRef.current) {
+ fileInputRef.current.click();
+ }
+ }
+
+ async function handleMapAdd(map) {
+ await db.table("maps").add(map);
+ const state = { ...defaultMapState, mapId: map.id };
+ await db.table("states").add(state);
+ setMaps((prevMaps) => [map, ...prevMaps]);
+ setSelectedMap(map);
+ setSelectedMapState(state);
+ }
+
+ async function handleMapRemove(id) {
+ await db.table("maps").delete(id);
+ await db.table("states").delete(id);
+ setMaps((prevMaps) => {
+ const filtered = prevMaps.filter((map) => map.id !== id);
+ setSelectedMap(filtered[0]);
+ db.table("states").get(filtered[0].id).then(setSelectedMapState);
+ return filtered;
+ });
+ // Removed the map from the map screen if needed
+ if (currentMap && currentMap.id === selectedMap.id) {
+ onMapChange(null, null);
+ }
+ }
+
+ async function handleMapSelect(map) {
+ const state = await db.table("states").get(map.id);
+ setSelectedMapState(state);
+ setSelectedMap(map);
+ }
+
+ async function handleMapReset(id) {
+ const state = { ...defaultMapState, mapId: id };
+ await db.table("states").put(state);
+ setSelectedMapState(state);
+ // Reset the state of the current map if needed
+ if (currentMap && currentMap.id === selectedMap.id) {
+ onMapStateChange(state);
+ }
+ }
+
+ async function handleSubmit(e) {
+ e.preventDefault();
+ if (selectedMap) {
+ onMapChange(selectedMap, selectedMapState);
+ onDone();
+ }
+ onDone();
+ }
+
+ /**
+ * Drag and Drop
+ */
+ const [dragging, setDragging] = useState(false);
+ function handleImageDragEnter(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ setDragging(true);
+ }
+
+ function handleImageDragLeave(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ setDragging(false);
+ }
+
+ function handleImageDrop(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ const file = event.dataTransfer.files[0];
+ if (file && file.type.startsWith("image")) {
+ handleImageUpload(file);
+ }
+ setDragging(false);
+ }
+
+ /**
+ * Map settings
+ */
+ const [showMoreSettings, setShowMoreSettings] = useState(false);
+
+ async function handleMapSettingsChange(key, value) {
+ const change = { [key]: value, lastModified: Date.now() };
+ db.table("maps").update(selectedMap.id, change);
+ const newMap = { ...selectedMap, ...change };
+ setMaps((prevMaps) => {
+ const newMaps = [...prevMaps];
+ const i = newMaps.findIndex((map) => map.id === selectedMap.id);
+ if (i > -1) {
+ newMaps[i] = newMap;
+ }
+ return newMaps;
+ });
+ setSelectedMap(newMap);
+ }
+
+ async function handleMapStateSettingsChange(key, value) {
+ db.table("states").update(selectedMap.id, { [key]: value });
+ setSelectedMapState((prevState) => ({ ...prevState, [key]: value }));
+ }
+
+ return (
+
+
+ handleImageUpload(event.target.files[0])}
+ type="file"
+ accept="image/*"
+ style={{ display: "none" }}
+ ref={fileInputRef}
+ />
+
+
+
+
+
+ {dragging && (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ e.dataTransfer.dropEffect = "copy";
+ }}
+ onDrop={handleImageDrop}
+ >
+ Drop map to upload
+
+ )}
+
+
+
+ );
+}
+
+export default SelectMapModal;
diff --git a/src/modals/SettingsModal.js b/src/modals/SettingsModal.js
new file mode 100644
index 0000000..c60cb20
--- /dev/null
+++ b/src/modals/SettingsModal.js
@@ -0,0 +1,81 @@
+import React, { useState, useContext } from "react";
+import { Box, Label, Flex, Button, useColorMode, Checkbox } from "theme-ui";
+
+import Modal from "../components/Modal";
+
+import AuthContext from "../contexts/AuthContext";
+
+import db from "../database";
+
+function SettingsModal({ isOpen, onRequestClose }) {
+ const { userId } = useContext(AuthContext);
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+
+ async function handleEraseAllData() {
+ await db.delete();
+ window.location.reload();
+ }
+
+ async function handleClearCache() {
+ await db.table("maps").where("owner").notEqual(userId).delete();
+ // TODO: With custom tokens look up all tokens that aren't being used in a state
+ window.location.reload();
+ }
+
+ const [colorMode, setColorMode] = useColorMode();
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ setIsDeleteModalOpen(false)}
+ >
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default SettingsModal;
diff --git a/src/routes/Game.js b/src/routes/Game.js
index e2c7488..d83c7ca 100644
--- a/src/routes/Game.js
+++ b/src/routes/Game.js
@@ -1,20 +1,16 @@
-import React, {
- useState,
- useRef,
- useEffect,
- useCallback,
- useContext,
-} from "react";
+import React, { useState, useEffect, useCallback, useContext } from "react";
import { Flex, Box, Text } from "theme-ui";
import { useParams } from "react-router-dom";
+import db from "../database";
+
import { omit, isStreamStopped } from "../helpers/shared";
import useSession from "../helpers/useSession";
-import useNickname from "../helpers/useNickname";
+import useDebounce from "../helpers/useDebounce";
-import Party from "../components/Party";
-import Tokens from "../components/Tokens";
-import Map from "../components/Map";
+import Party from "../components/party/Party";
+import Tokens from "../components/token/Tokens";
+import Map from "../components/map/Map";
import Banner from "../components/Banner";
import LoadingOverlay from "../components/LoadingOverlay";
import Link from "../components/Link";
@@ -23,9 +19,13 @@ import AuthModal from "../modals/AuthModal";
import AuthContext from "../contexts/AuthContext";
+import { tokens as defaultTokens } from "../tokens";
+
function Game() {
const { id: gameId } = useParams();
- const { authenticationStatus } = useContext(AuthContext);
+ const { authenticationStatus, userId, nickname, setNickname } = useContext(
+ AuthContext
+ );
const { peers, socket } = useSession(
gameId,
@@ -41,83 +41,189 @@ function Game() {
* Map state
*/
- const [mapSource, setMapSource] = useState(null);
- const mapDataRef = useRef(null);
+ const [map, setMap] = useState(null);
+ const [mapState, setMapState] = useState(null);
+ const [mapLoading, setMapLoading] = useState(false);
- function handleMapChange(mapData, mapSource) {
- mapDataRef.current = mapData;
- setMapSource(mapSource);
- for (let peer of Object.values(peers)) {
- peer.connection.send({ id: "map", data: mapDataRef.current });
+ const canEditMapDrawing =
+ map !== null &&
+ mapState !== null &&
+ (mapState.editFlags.includes("drawing") || map.owner === userId);
+
+ const canEditFogDrawing =
+ map !== null &&
+ mapState !== null &&
+ (mapState.editFlags.includes("fog") || map.owner === userId);
+
+ const disabledMapTokens = {};
+ // If we have a map and state and have the token permission disabled
+ // and are not the map owner
+ if (
+ mapState !== null &&
+ map !== null &&
+ !mapState.editFlags.includes("tokens") &&
+ map.owner !== userId
+ ) {
+ for (let token of Object.values(mapState.tokens)) {
+ if (token.owner !== userId) {
+ disabledMapTokens[token.id] = true;
+ }
}
}
- const [mapTokens, setMapTokens] = useState({});
+ // Sync the map state to the database after 500ms of inactivity
+ const debouncedMapState = useDebounce(mapState, 500);
+ useEffect(() => {
+ if (
+ debouncedMapState &&
+ debouncedMapState.mapId &&
+ map &&
+ map.owner === userId
+ ) {
+ db.table("states").update(debouncedMapState.mapId, debouncedMapState);
+ }
+ }, [map, debouncedMapState, userId]);
- function handleMapTokenChange(token) {
- if (!mapSource) {
+ function handleMapChange(newMap, newMapState) {
+ setMapState(newMapState);
+ setMap(newMap);
+ for (let peer of Object.values(peers)) {
+ // Clear the map so the new map state isn't shown on an old map
+ peer.connection.send({ id: "map", data: null });
+ peer.connection.send({ id: "mapState", data: newMapState });
+ sendMapDataToPeer(peer, newMap);
+ }
+ }
+
+ function sendMapDataToPeer(peer, mapData) {
+ // Omit file from map change, receiver will request the file if
+ // they have an outdated version
+ if (mapData.type === "file") {
+ const { file, ...rest } = mapData;
+ peer.connection.send({ id: "map", data: rest });
+ } else {
+ peer.connection.send({ id: "map", data: mapData });
+ }
+ }
+
+ function handleMapStateChange(newMapState) {
+ setMapState(newMapState);
+ for (let peer of Object.values(peers)) {
+ peer.connection.send({ id: "mapState", data: newMapState });
+ }
+ }
+
+ async function handleMapTokenStateChange(token) {
+ if (mapState === null) {
return;
}
- setMapTokens((prevMapTokens) => ({
- ...prevMapTokens,
- [token.id]: token,
+ setMapState((prevMapState) => ({
+ ...prevMapState,
+ tokens: {
+ ...prevMapState.tokens,
+ [token.id]: token,
+ },
}));
for (let peer of Object.values(peers)) {
const data = { [token.id]: token };
- peer.connection.send({ id: "tokenEdit", data });
+ peer.connection.send({ id: "tokenStateEdit", data });
}
}
- function handleMapTokenRemove(token) {
- setMapTokens((prevMapTokens) => {
- const { [token.id]: old, ...rest } = prevMapTokens;
- return rest;
+ function handleMapTokenStateRemove(token) {
+ setMapState((prevMapState) => {
+ const { [token.id]: old, ...rest } = prevMapState.tokens;
+ return { ...prevMapState, tokens: rest };
});
for (let peer of Object.values(peers)) {
const data = { [token.id]: token };
- peer.connection.send({ id: "tokenRemove", data });
+ peer.connection.send({ id: "tokenStateRemove", data });
}
}
- const [mapDrawActions, setMapDrawActions] = useState([]);
- // An index into the draw actions array to which only actions before the
- // index will be performed (used in undo and redo)
- const [mapDrawActionIndex, setMapDrawActionIndex] = useState(-1);
- function addNewMapDrawActions(actions) {
- setMapDrawActions((prevActions) => {
+ function addMapDrawActions(actions, indexKey, actionsKey) {
+ setMapState((prevMapState) => {
const newActions = [
- ...prevActions.slice(0, mapDrawActionIndex + 1),
+ ...prevMapState[actionsKey].slice(0, prevMapState[indexKey] + 1),
...actions,
];
const newIndex = newActions.length - 1;
- setMapDrawActionIndex(newIndex);
- return newActions;
+ return {
+ ...prevMapState,
+ [actionsKey]: newActions,
+ [indexKey]: newIndex,
+ };
});
}
+ function updateDrawActionIndex(change, indexKey, actionsKey, peerId) {
+ const newIndex = Math.min(
+ Math.max(mapState[indexKey] + change, -1),
+ mapState[actionsKey].length - 1
+ );
+
+ setMapState((prevMapState) => ({
+ ...prevMapState,
+ [indexKey]: newIndex,
+ }));
+ return newIndex;
+ }
+
function handleMapDraw(action) {
- addNewMapDrawActions([action]);
+ addMapDrawActions([action], "mapDrawActionIndex", "mapDrawActions");
for (let peer of Object.values(peers)) {
peer.connection.send({ id: "mapDraw", data: [action] });
}
}
function handleMapDrawUndo() {
- const newIndex = Math.max(mapDrawActionIndex - 1, -1);
- setMapDrawActionIndex(newIndex);
+ const index = updateDrawActionIndex(
+ -1,
+ "mapDrawActionIndex",
+ "mapDrawActions"
+ );
for (let peer of Object.values(peers)) {
- peer.connection.send({ id: "mapDrawIndex", data: newIndex });
+ peer.connection.send({ id: "mapDrawIndex", data: index });
}
}
function handleMapDrawRedo() {
- const newIndex = Math.min(
- mapDrawActionIndex + 1,
- mapDrawActions.length - 1
+ const index = updateDrawActionIndex(
+ 1,
+ "mapDrawActionIndex",
+ "mapDrawActions"
);
- setMapDrawActionIndex(newIndex);
for (let peer of Object.values(peers)) {
- peer.connection.send({ id: "mapDrawIndex", data: newIndex });
+ peer.connection.send({ id: "mapDrawIndex", data: index });
+ }
+ }
+
+ function handleFogDraw(action) {
+ addMapDrawActions([action], "fogDrawActionIndex", "fogDrawActions");
+ for (let peer of Object.values(peers)) {
+ peer.connection.send({ id: "mapFog", data: [action] });
+ }
+ }
+
+ function handleFogDrawUndo() {
+ const index = updateDrawActionIndex(
+ -1,
+ "fogDrawActionIndex",
+ "fogDrawActions"
+ );
+ for (let peer of Object.values(peers)) {
+ peer.connection.send({ id: "fogDrawIndex", data: index });
+ }
+ }
+
+ function handleFogDrawRedo() {
+ const index = updateDrawActionIndex(
+ 1,
+ "fogDrawActionIndex",
+ "fogDrawActions"
+ );
+ for (let peer of Object.values(peers)) {
+ peer.connection.send({ id: "fogDrawIndex", data: index });
}
}
@@ -125,7 +231,6 @@ function Game() {
* Party state
*/
- const { nickname, setNickname } = useNickname();
const [partyNicknames, setPartyNicknames] = useState({});
function handleNicknameChange(nickname) {
@@ -151,34 +256,66 @@ function Game() {
function handlePeerData({ data, peer }) {
if (data.id === "sync") {
- if (mapSource) {
- peer.connection.send({ id: "map", data: mapDataRef.current });
+ if (mapState) {
+ peer.connection.send({ id: "mapState", data: mapState });
}
- if (mapTokens) {
- peer.connection.send({ id: "tokenEdit", data: mapTokens });
- }
- if (mapDrawActions) {
- peer.connection.send({ id: "mapDraw", data: mapDrawActions });
- }
- if (mapDrawActionIndex !== mapDrawActions.length - 1) {
- peer.connection.send({ id: "mapDrawIndex", data: mapDrawActionIndex });
+ if (map) {
+ sendMapDataToPeer(peer, map);
}
}
if (data.id === "map") {
- const blob = new Blob([data.data.file]);
- mapDataRef.current = { ...data.data, file: blob };
- setMapSource(URL.createObjectURL(mapDataRef.current.file));
+ const newMap = data.data;
+ // If is a file map check cache and request the full file if outdated
+ if (newMap && newMap.type === "file") {
+ db.table("maps")
+ .get(newMap.id)
+ .then((cachedMap) => {
+ if (cachedMap && cachedMap.lastModified === newMap.lastModified) {
+ setMap(cachedMap);
+ } else {
+ setMapLoading(true);
+ peer.connection.send({ id: "mapRequest" });
+ }
+ });
+ } else {
+ setMap(newMap);
+ }
}
- if (data.id === "tokenEdit") {
- setMapTokens((prevMapTokens) => ({
- ...prevMapTokens,
- ...data.data,
+ // Send full map data including file
+ if (data.id === "mapRequest") {
+ peer.connection.send({ id: "mapResponse", data: map });
+ }
+ // A new map response with a file attached
+ if (data.id === "mapResponse") {
+ setMapLoading(false);
+ if (data.data && data.data.type === "file") {
+ // Convert file back to blob after peer transfer
+ const file = new Blob([data.data.file]);
+ const newMap = { ...data.data, file };
+ // Store in db
+ db.table("maps")
+ .put(newMap)
+ .then(() => {
+ setMap(newMap);
+ });
+ } else {
+ setMap(data.data);
+ }
+ }
+ if (data.id === "mapState") {
+ setMapState(data.data);
+ }
+ if (data.id === "tokenStateEdit") {
+ setMapState((prevMapState) => ({
+ ...prevMapState,
+ tokens: { ...prevMapState.tokens, ...data.data },
}));
}
- if (data.id === "tokenRemove") {
- setMapTokens((prevMapTokens) =>
- omit(prevMapTokens, Object.keys(data.data))
- );
+ if (data.id === "tokenStateRemove") {
+ setMapState((prevMapState) => ({
+ ...prevMapState,
+ tokens: omit(prevMapState.tokens, Object.keys(data.data)),
+ }));
}
if (data.id === "nickname") {
setPartyNicknames((prevNicknames) => ({
@@ -187,10 +324,22 @@ function Game() {
}));
}
if (data.id === "mapDraw") {
- addNewMapDrawActions(data.data);
+ addMapDrawActions(data.data, "mapDrawActionIndex", "mapDrawActions");
}
if (data.id === "mapDrawIndex") {
- setMapDrawActionIndex(data.data);
+ setMapState((prevMapState) => ({
+ ...prevMapState,
+ mapDrawActionIndex: data.data,
+ }));
+ }
+ if (data.id === "mapFog") {
+ addMapDrawActions(data.data, "fogDrawActionIndex", "fogDrawActions");
+ }
+ if (data.id === "mapFogIndex") {
+ setMapState((prevMapState) => ({
+ ...prevMapState,
+ fogDrawActionIndex: data.data,
+ }));
}
}
@@ -286,6 +435,25 @@ function Game() {
}
}, [stream, peers, handleStreamEnd]);
+ /**
+ * Token data
+ */
+ const [tokens, setTokens] = useState([]);
+ useEffect(() => {
+ if (!userId) {
+ return;
+ }
+ const defaultTokensWithIds = [];
+ for (let defaultToken of defaultTokens) {
+ defaultTokensWithIds.push({
+ ...defaultToken,
+ id: `__default-${defaultToken.name}`,
+ owner: userId,
+ });
+ }
+ setTokens(defaultTokensWithIds);
+ }, [userId]);
+
return (
<>
@@ -303,19 +471,28 @@ function Game() {
onStreamEnd={handleStreamEnd}
/>
+
-
setPeerError(null)}>
diff --git a/src/routes/Home.js b/src/routes/Home.js
index 08b710e..e3c8ce3 100644
--- a/src/routes/Home.js
+++ b/src/routes/Home.js
@@ -51,7 +51,7 @@ function Home() {
Join Game
- Beta v1.1.0
+ Beta v1.2.0