From 2108d3250164f40aec9480a062c5072a10d4ce69 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 28 Jan 2021 15:12:30 +1100 Subject: [PATCH] Added colours and gradients to pointers --- package.json | 1 + src/components/map/MapControls.js | 2 + src/components/map/MapPointer.js | 7 +- src/components/map/controls/ColorControl.js | 34 ++++--- .../map/controls/PointerToolSettings.js | 18 ++++ src/helpers/konva.js | 88 ++++++++++++++++--- src/network/NetworkedMapPointer.js | 23 ++++- src/settings.js | 5 ++ yarn.lock | 2 +- 9 files changed, 147 insertions(+), 33 deletions(-) create mode 100644 src/components/map/controls/PointerToolSettings.js 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==