diff --git a/src/components/map/Map.js b/src/components/map/Map.js index df9384a..553bfdf 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -10,6 +10,7 @@ import MapDice from "./MapDice"; import MapGrid from "./MapGrid"; import MapMeasure from "./MapMeasure"; import MapLoadingOverlay from "./MapLoadingOverlay"; +import NetworkedMapPointer from "../../network/NetworkedMapPointer"; import TokenDataContext from "../../contexts/TokenDataContext"; @@ -35,6 +36,7 @@ function Map({ allowFogDrawing, allowMapChange, disabledTokens, + session, }) { const { tokensById } = useContext(TokenDataContext); @@ -139,6 +141,7 @@ function Map({ if (!map) { disabledControls.push("pan"); disabledControls.push("measure"); + disabledControls.push("pointer"); } if (!allowFogDrawing) { disabledControls.push("fog"); @@ -304,6 +307,14 @@ function Map({ /> ); + const mapPointer = ( + + ); + return ( ); diff --git a/src/components/map/MapControls.js b/src/components/map/MapControls.js index 33873ba..ca46ae1 100644 --- a/src/components/map/MapControls.js +++ b/src/components/map/MapControls.js @@ -15,6 +15,7 @@ import FogToolIcon from "../../icons/FogToolIcon"; import BrushToolIcon from "../../icons/BrushToolIcon"; import MeasureToolIcon from "../../icons/MeasureToolIcon"; import ExpandMoreIcon from "../../icons/ExpandMoreIcon"; +import PointerToolIcon from "../../icons/PointerToolIcon"; function MapContols({ onMapChange, @@ -55,8 +56,13 @@ function MapContols({ title: "Measure Tool", SettingsComponent: MeasureToolSettings, }, + pointer: { + id: "pointer", + icon: , + title: "Pointer Tool", + }, }; - const tools = ["pan", "fog", "drawing", "measure"]; + const tools = ["pan", "fog", "drawing", "measure", "pointer"]; const sections = [ { diff --git a/src/components/map/MapPointer.js b/src/components/map/MapPointer.js new file mode 100644 index 0000000..141e9cf --- /dev/null +++ b/src/components/map/MapPointer.js @@ -0,0 +1,81 @@ +import React, { useContext, useEffect } from "react"; +import { Group, Circle } from "react-konva"; + +import MapInteractionContext from "../../contexts/MapInteractionContext"; +import MapStageContext from "../../contexts/MapStageContext"; + +import { getStrokeWidth } from "../../helpers/drawing"; +import { getRelativePointerPositionNormalized } from "../../helpers/konva"; + +import colors from "../../helpers/colors"; + +function MapPointer({ + gridSize, + active, + position, + onPointerDown, + onPointerMove, + onPointerUp, + visible, +}) { + const { mapWidth, mapHeight, interactionEmitter } = useContext( + MapInteractionContext + ); + const mapStageRef = useContext(MapStageContext); + + // const [isBrushDown, setIsBrushDown] = useState(false); + // const [brushPosition, setBrushPosition] = useState({ x: 0, y: 0 }); + + useEffect(() => { + if (!active) { + return; + } + + const mapStage = mapStageRef.current; + + function getBrushPosition() { + const mapImage = mapStage.findOne("#mapImage"); + return getRelativePointerPositionNormalized(mapImage); + } + + function handleBrushDown() { + onPointerDown && onPointerDown(getBrushPosition()); + } + + function handleBrushMove() { + onPointerMove && onPointerMove(getBrushPosition()); + } + + function handleBrushUp() { + onPointerMove && onPointerUp({ x: 0, y: 0 }); + } + + interactionEmitter.on("dragStart", handleBrushDown); + interactionEmitter.on("drag", handleBrushMove); + interactionEmitter.on("dragEnd", handleBrushUp); + + return () => { + interactionEmitter.off("dragStart", handleBrushDown); + interactionEmitter.off("drag", handleBrushMove); + interactionEmitter.off("dragEnd", handleBrushUp); + }; + }); + + const size = getStrokeWidth(2, gridSize, mapWidth, mapHeight); + + return ( + + {visible && ( + + )} + + ); +} + +export default MapPointer; diff --git a/src/icons/PointerToolIcon.js b/src/icons/PointerToolIcon.js new file mode 100644 index 0000000..7704c99 --- /dev/null +++ b/src/icons/PointerToolIcon.js @@ -0,0 +1,18 @@ +import React from "react"; + +function PointerToolIcon() { + return ( + + + + + ); +} + +export default PointerToolIcon; diff --git a/src/network/NetworkedMapAndTokens.js b/src/network/NetworkedMapAndTokens.js index c3e99e4..9355c1e 100644 --- a/src/network/NetworkedMapAndTokens.js +++ b/src/network/NetworkedMapAndTokens.js @@ -427,6 +427,7 @@ function NetworkedMapAndTokens({ session }) { allowFogDrawing={canEditFogDrawing} allowMapChange={canChangeMap} disabledTokens={disabledMapTokens} + session={session} /> diff --git a/src/network/NetworkedMapPointer.js b/src/network/NetworkedMapPointer.js new file mode 100644 index 0000000..a7cda69 --- /dev/null +++ b/src/network/NetworkedMapPointer.js @@ -0,0 +1,76 @@ +import React, { useState, useContext, useEffect } from "react"; +import { Group } from "react-konva"; + +import AuthContext from "../contexts/AuthContext"; + +import MapPointer from "../components/map/MapPointer"; + +function NetworkedMapPointer({ session, active, gridSize }) { + const { userId } = useContext(AuthContext); + const [pointerState, setPointerState] = useState({}); + + useEffect(() => { + if (userId && !(userId in pointerState)) { + setPointerState({ + [userId]: { position: { x: 0, y: 0 }, visible: false, id: userId }, + }); + } + }, [userId, pointerState]); + + function updateOwnPointerState(position, visible) { + const update = { [userId]: { position, visible, id: userId } }; + setPointerState((prev) => ({ + ...prev, + ...update, + })); + session.send("pointer", update); + } + + function handleOwnPointerDown(position) { + updateOwnPointerState(position, true); + } + + function handleOwnPointerMove(position) { + updateOwnPointerState(position, true); + } + + function handleOwnPointerUp(position) { + updateOwnPointerState(position, false); + } + + useEffect(() => { + function handlePeerData({ id, data }) { + if (id === "pointer") { + setPointerState((prev) => ({ + ...prev, + ...data, + })); + } + } + + session.on("data", handlePeerData); + + return () => { + session.off("data", handlePeerData); + }; + }); + + return ( + + {Object.values(pointerState).map((pointer) => ( + + ))} + + ); +} + +export default NetworkedMapPointer;