diff --git a/package.json b/package.json
index 743183a..98ef32b 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"@testing-library/user-event": "^12.2.2",
"ammo.js": "kripken/ammo.js#aab297a4164779c3a9d8dc8d9da26958de3cb778",
"case": "^1.6.3",
+ "color": "^3.1.3",
"comlink": "^4.3.0",
"deep-diff": "^1.0.2",
"dexie": "^3.0.3",
diff --git a/src/components/map/MapControls.js b/src/components/map/MapControls.js
index fb71b72..9cc968b 100644
--- a/src/components/map/MapControls.js
+++ b/src/components/map/MapControls.js
@@ -9,6 +9,7 @@ import SelectMapButton from "./SelectMapButton";
import FogToolSettings from "./controls/FogToolSettings";
import DrawingToolSettings from "./controls/DrawingToolSettings";
import MeasureToolSettings from "./controls/MeasureToolSettings";
+import PointerToolSettings from "./controls/PointerToolSettings";
import PanToolIcon from "../../icons/PanToolIcon";
import FogToolIcon from "../../icons/FogToolIcon";
@@ -66,6 +67,7 @@ function MapContols({
id: "pointer",
icon: ,
title: "Pointer Tool (Q)",
+ SettingsComponent: PointerToolSettings,
},
note: {
id: "note",
diff --git a/src/components/map/MapPointer.js b/src/components/map/MapPointer.js
index 17934d4..a680966 100644
--- a/src/components/map/MapPointer.js
+++ b/src/components/map/MapPointer.js
@@ -21,6 +21,7 @@ function MapPointer({
onPointerMove,
onPointerUp,
visible,
+ color,
}) {
const { mapWidth, mapHeight, interactionEmitter } = useContext(
MapInteractionContext
@@ -69,7 +70,7 @@ function MapPointer({
{visible && (
@@ -78,4 +79,8 @@ function MapPointer({
);
}
+MapPointer.defaultProps = {
+ color: "red",
+};
+
export default MapPointer;
diff --git a/src/components/map/controls/ColorControl.js b/src/components/map/controls/ColorControl.js
index d9264f1..d00128c 100644
--- a/src/components/map/controls/ColorControl.js
+++ b/src/components/map/controls/ColorControl.js
@@ -34,7 +34,7 @@ function ColorCircle({ color, selected, onClick, sx }) {
);
}
-function ColorControl({ color, onColorChange }) {
+function ColorControl({ color, onColorChange, exclude }) {
const [showColorMenu, setShowColorMenu] = useState(false);
const [colorMenuOptions, setColorMenuOptions] = useState({});
@@ -74,19 +74,21 @@ function ColorControl({ color, onColorChange }) {
}}
p={1}
>
- {colorOptions.map((c) => (
- {
- onColorChange(c);
- setShowColorMenu(false);
- setColorMenuOptions({});
- }}
- sx={{ width: "25%", paddingTop: "25%" }}
- />
- ))}
+ {colorOptions
+ .filter((color) => !exclude.includes(color))
+ .map((c) => (
+ {
+ onColorChange(c);
+ setShowColorMenu(false);
+ setColorMenuOptions({});
+ }}
+ sx={{ width: "25%", paddingTop: "25%" }}
+ />
+ ))}
);
@@ -104,4 +106,8 @@ function ColorControl({ color, onColorChange }) {
);
}
+ColorControl.defaultProps = {
+ exclude: [],
+};
+
export default ColorControl;
diff --git a/src/components/map/controls/PointerToolSettings.js b/src/components/map/controls/PointerToolSettings.js
new file mode 100644
index 0000000..9d04ab9
--- /dev/null
+++ b/src/components/map/controls/PointerToolSettings.js
@@ -0,0 +1,18 @@
+import React from "react";
+import { Flex } from "theme-ui";
+
+import ColorControl from "./ColorControl";
+
+function PointerToolSettings({ settings, onSettingChange }) {
+ return (
+
+ onSettingChange({ color })}
+ exclude={["black", "darkGray", "lightGray", "white"]}
+ />
+
+ );
+}
+
+export default PointerToolSettings;
diff --git a/src/helpers/konva.js b/src/helpers/konva.js
index 227a2cc..541115d 100644
--- a/src/helpers/konva.js
+++ b/src/helpers/konva.js
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef } from "react";
import { Line, Group, Path, Circle } from "react-konva";
-import { lerp } from "./shared";
+import Color from "color";
import * as Vector2 from "./vector2";
// Holes should be wound in the opposite direction as the containing points array
@@ -142,10 +142,27 @@ export function Tick({ x, y, scale, onClick, cross }) {
);
}
-export function Trail({ position, size, duration, segments }) {
+export function Trail({ position, size, duration, segments, color }) {
const trailRef = useRef();
const pointsRef = useRef([]);
const prevPositionRef = useRef(position);
+ const positionRef = useRef(position);
+ const circleRef = useRef();
+ // Color of the end of the trial
+ const transparentColorRef = useRef(
+ Color(color).lighten(0.5).alpha(0).string()
+ );
+
+ useEffect(() => {
+ // Lighten color to give it a `glow` effect
+ transparentColorRef.current = Color(color).lighten(0.5).alpha(0).string();
+ }, [color]);
+
+ // Keep track of position so we can use it in the trail animation
+ useEffect(() => {
+ positionRef.current = position;
+ }, [position]);
+
// Add a new point every time position is changed
useEffect(() => {
if (Vector2.compare(position, prevPositionRef.current, 0.0001)) {
@@ -178,6 +195,13 @@ export function Trail({ position, size, duration, segments }) {
if (expired > 0) {
pointsRef.current = pointsRef.current.slice(expired);
}
+
+ // Update the circle position to keep it in sync with the trail
+ if (circleRef.current) {
+ circleRef.current.x(positionRef.current.x);
+ circleRef.current.y(positionRef.current.y);
+ }
+
if (trailRef.current) {
trailRef.current.getLayer().draw();
}
@@ -192,20 +216,57 @@ export function Trail({ position, size, duration, segments }) {
function sceneFunc(context) {
// Resample points to ensure a smooth trail
const resampledPoints = Vector2.resample(pointsRef.current, segments);
+ if (resampledPoints.length === 0) {
+ return;
+ }
+ // Draws a line offset in the direction perpendicular to its travel direction
+ const drawOffsetLine = (from, to, alpha) => {
+ const forward = Vector2.normalize(Vector2.subtract(from, to));
+ // Rotate the forward vector 90 degrees based off of the direction
+ const side = { x: forward.y, y: -forward.x };
+
+ // Offset the `to` position by the size of the point and in the side direction
+ const toSize = (alpha * size) / 2;
+ const toOffset = Vector2.add(to, Vector2.multiply(side, toSize));
+
+ context.lineTo(toOffset.x, toOffset.y);
+ };
+ context.beginPath();
+ // Sample the points starting from the tail then traverse counter clockwise drawing each point
+ // offset to make a taper, stops at the base of the trail
+ context.moveTo(resampledPoints[0].x, resampledPoints[0].y);
for (let i = 1; i < resampledPoints.length; i++) {
const from = resampledPoints[i - 1];
const to = resampledPoints[i];
- const alpha = i / resampledPoints.length;
- context.beginPath();
- context.lineJoin = "round";
- context.lineCap = "round";
- context.lineWidth = alpha * size;
- context.strokeStyle = `hsl(0, 63%, ${lerp(90, 50, alpha)}%)`;
- context.moveTo(from.x, from.y);
- context.lineTo(to.x, to.y);
- context.stroke();
- context.closePath();
+ drawOffsetLine(from, to, i / resampledPoints.length);
}
+ // Start from the base of the trail and continue drawing down back to the end of the tail
+ for (let i = resampledPoints.length - 2; i >= 0; i--) {
+ const from = resampledPoints[i + 1];
+ const to = resampledPoints[i];
+ drawOffsetLine(from, to, i / resampledPoints.length);
+ }
+ context.lineTo(resampledPoints[0].x, resampledPoints[0].y);
+ context.closePath();
+
+ // Create a radial gradient from the center of the trail to the tail
+ const gradientCenter = resampledPoints[resampledPoints.length - 1];
+ const gradientEnd = resampledPoints[0];
+ const gradientRadius = Vector2.length(
+ Vector2.subtract(gradientCenter, gradientEnd)
+ );
+ let gradient = context.createRadialGradient(
+ gradientCenter.x,
+ gradientCenter.y,
+ 0,
+ gradientCenter.x,
+ gradientCenter.y,
+ gradientRadius
+ );
+ gradient.addColorStop(0, color);
+ gradient.addColorStop(1, transparentColorRef.current);
+ context.fillStyle = gradient;
+ context.fill();
}
return (
@@ -214,9 +275,10 @@ export function Trail({ position, size, duration, segments }) {
);
diff --git a/src/network/NetworkedMapPointer.js b/src/network/NetworkedMapPointer.js
index aab6b32..a449abe 100644
--- a/src/network/NetworkedMapPointer.js
+++ b/src/network/NetworkedMapPointer.js
@@ -6,6 +6,7 @@ import AuthContext from "../contexts/AuthContext";
import MapPointer from "../components/map/MapPointer";
import { isEmpty } from "../helpers/shared";
import { lerp, compare } from "../helpers/vector2";
+import useSetting from "../helpers/useSetting";
// Send pointer updates every 50ms (20fps)
const sendTickRate = 50;
@@ -13,6 +14,7 @@ const sendTickRate = 50;
function NetworkedMapPointer({ session, active, gridSize }) {
const { userId } = useContext(AuthContext);
const [localPointerState, setLocalPointerState] = useState({});
+ const [pointerColor] = useSetting("pointer.color");
const sessionRef = useRef(session);
useEffect(() => {
@@ -22,10 +24,15 @@ function NetworkedMapPointer({ session, active, gridSize }) {
useEffect(() => {
if (userId && !(userId in localPointerState)) {
setLocalPointerState({
- [userId]: { position: { x: 0, y: 0 }, visible: false, id: userId },
+ [userId]: {
+ position: { x: 0, y: 0 },
+ visible: false,
+ id: userId,
+ color: pointerColor,
+ },
});
}
- }, [userId, localPointerState]);
+ }, [userId, localPointerState, pointerColor]);
// Send pointer updates every sendTickRate to peers to save on bandwidth
// We use requestAnimationFrame as setInterval was being blocked during
@@ -65,9 +72,14 @@ function NetworkedMapPointer({ session, active, gridSize }) {
function updateOwnPointerState(position, visible) {
setLocalPointerState((prev) => ({
...prev,
- [userId]: { position, visible, id: userId },
+ [userId]: { position, visible, id: userId, color: pointerColor },
}));
- ownPointerUpdateRef.current = { position, visible, id: userId };
+ ownPointerUpdateRef.current = {
+ position,
+ visible,
+ id: userId,
+ color: pointerColor,
+ };
}
function handleOwnPointerDown(position) {
@@ -142,6 +154,7 @@ function NetworkedMapPointer({ session, active, gridSize }) {
id: interp.id,
visible: interp.from.visible,
position: lerp(interp.from.position, interp.to.position, alpha),
+ color: interp.from.color,
};
}
if (alpha > 1 && !interp.to.visible) {
@@ -149,6 +162,7 @@ function NetworkedMapPointer({ session, active, gridSize }) {
id: interp.id,
visible: interp.to.visible,
position: interp.to.position,
+ color: interp.to.color,
};
delete interpolationsRef.current[interp.id];
}
@@ -178,6 +192,7 @@ function NetworkedMapPointer({ session, active, gridSize }) {
onPointerDown={pointer.id === userId && handleOwnPointerDown}
onPointerMove={pointer.id === userId && handleOwnPointerMove}
onPointerUp={pointer.id === userId && handleOwnPointerUp}
+ color={pointer.color}
/>
))}
diff --git a/src/settings.js b/src/settings.js
index e2dce0c..5f7375f 100644
--- a/src/settings.js
+++ b/src/settings.js
@@ -37,6 +37,11 @@ function loadVersions(settings) {
...prev,
game: { usePassword: true },
}));
+ // v1.7.1 - Added pointer color
+ settings.version(4, (prev) => ({
+ ...prev,
+ pointer: { color: "red" },
+ }));
}
export function getSettings() {
diff --git a/yarn.lock b/yarn.lock
index 1c299a8..57b3296 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3781,7 +3781,7 @@ color-string@^1.5.4:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
-color@^3.0.0:
+color@^3.0.0, color@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e"
integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==