Added colours and gradients to pointers

This commit is contained in:
Mitchell McCaffrey 2021-01-28 15:12:30 +11:00
parent 72ecd002dd
commit 2108d32501
9 changed files with 147 additions and 33 deletions

View File

@ -14,6 +14,7 @@
"@testing-library/user-event": "^12.2.2", "@testing-library/user-event": "^12.2.2",
"ammo.js": "kripken/ammo.js#aab297a4164779c3a9d8dc8d9da26958de3cb778", "ammo.js": "kripken/ammo.js#aab297a4164779c3a9d8dc8d9da26958de3cb778",
"case": "^1.6.3", "case": "^1.6.3",
"color": "^3.1.3",
"comlink": "^4.3.0", "comlink": "^4.3.0",
"deep-diff": "^1.0.2", "deep-diff": "^1.0.2",
"dexie": "^3.0.3", "dexie": "^3.0.3",

View File

@ -9,6 +9,7 @@ import SelectMapButton from "./SelectMapButton";
import FogToolSettings from "./controls/FogToolSettings"; import FogToolSettings from "./controls/FogToolSettings";
import DrawingToolSettings from "./controls/DrawingToolSettings"; import DrawingToolSettings from "./controls/DrawingToolSettings";
import MeasureToolSettings from "./controls/MeasureToolSettings"; import MeasureToolSettings from "./controls/MeasureToolSettings";
import PointerToolSettings from "./controls/PointerToolSettings";
import PanToolIcon from "../../icons/PanToolIcon"; import PanToolIcon from "../../icons/PanToolIcon";
import FogToolIcon from "../../icons/FogToolIcon"; import FogToolIcon from "../../icons/FogToolIcon";
@ -66,6 +67,7 @@ function MapContols({
id: "pointer", id: "pointer",
icon: <PointerToolIcon />, icon: <PointerToolIcon />,
title: "Pointer Tool (Q)", title: "Pointer Tool (Q)",
SettingsComponent: PointerToolSettings,
}, },
note: { note: {
id: "note", id: "note",

View File

@ -21,6 +21,7 @@ function MapPointer({
onPointerMove, onPointerMove,
onPointerUp, onPointerUp,
visible, visible,
color,
}) { }) {
const { mapWidth, mapHeight, interactionEmitter } = useContext( const { mapWidth, mapHeight, interactionEmitter } = useContext(
MapInteractionContext MapInteractionContext
@ -69,7 +70,7 @@ function MapPointer({
{visible && ( {visible && (
<Trail <Trail
position={multiply(position, { x: mapWidth, y: mapHeight })} position={multiply(position, { x: mapWidth, y: mapHeight })}
color={colors.red} color={colors[color]}
size={size} size={size}
duration={200} duration={200}
/> />
@ -78,4 +79,8 @@ function MapPointer({
); );
} }
MapPointer.defaultProps = {
color: "red",
};
export default MapPointer; export default MapPointer;

View File

@ -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 [showColorMenu, setShowColorMenu] = useState(false);
const [colorMenuOptions, setColorMenuOptions] = useState({}); const [colorMenuOptions, setColorMenuOptions] = useState({});
@ -74,19 +74,21 @@ function ColorControl({ color, onColorChange }) {
}} }}
p={1} p={1}
> >
{colorOptions.map((c) => ( {colorOptions
<ColorCircle .filter((color) => !exclude.includes(color))
key={c} .map((c) => (
color={c} <ColorCircle
selected={c === color} key={c}
onClick={() => { color={c}
onColorChange(c); selected={c === color}
setShowColorMenu(false); onClick={() => {
setColorMenuOptions({}); onColorChange(c);
}} setShowColorMenu(false);
sx={{ width: "25%", paddingTop: "25%" }} setColorMenuOptions({});
/> }}
))} sx={{ width: "25%", paddingTop: "25%" }}
/>
))}
</Box> </Box>
</MapMenu> </MapMenu>
); );
@ -104,4 +106,8 @@ function ColorControl({ color, onColorChange }) {
); );
} }
ColorControl.defaultProps = {
exclude: [],
};
export default ColorControl; export default ColorControl;

View File

@ -0,0 +1,18 @@
import React from "react";
import { Flex } from "theme-ui";
import ColorControl from "./ColorControl";
function PointerToolSettings({ settings, onSettingChange }) {
return (
<Flex sx={{ alignItems: "center" }}>
<ColorControl
color={settings.color}
onColorChange={(color) => onSettingChange({ color })}
exclude={["black", "darkGray", "lightGray", "white"]}
/>
</Flex>
);
}
export default PointerToolSettings;

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import { Line, Group, Path, Circle } from "react-konva"; import { Line, Group, Path, Circle } from "react-konva";
import { lerp } from "./shared"; import Color from "color";
import * as Vector2 from "./vector2"; import * as Vector2 from "./vector2";
// Holes should be wound in the opposite direction as the containing points array // 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 trailRef = useRef();
const pointsRef = useRef([]); const pointsRef = useRef([]);
const prevPositionRef = useRef(position); 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 // Add a new point every time position is changed
useEffect(() => { useEffect(() => {
if (Vector2.compare(position, prevPositionRef.current, 0.0001)) { if (Vector2.compare(position, prevPositionRef.current, 0.0001)) {
@ -178,6 +195,13 @@ export function Trail({ position, size, duration, segments }) {
if (expired > 0) { if (expired > 0) {
pointsRef.current = pointsRef.current.slice(expired); 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) { if (trailRef.current) {
trailRef.current.getLayer().draw(); trailRef.current.getLayer().draw();
} }
@ -192,20 +216,57 @@ export function Trail({ position, size, duration, segments }) {
function sceneFunc(context) { function sceneFunc(context) {
// Resample points to ensure a smooth trail // Resample points to ensure a smooth trail
const resampledPoints = Vector2.resample(pointsRef.current, segments); 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++) { for (let i = 1; i < resampledPoints.length; i++) {
const from = resampledPoints[i - 1]; const from = resampledPoints[i - 1];
const to = resampledPoints[i]; const to = resampledPoints[i];
const alpha = i / resampledPoints.length; drawOffsetLine(from, to, 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();
} }
// 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 ( return (
@ -214,9 +275,10 @@ export function Trail({ position, size, duration, segments }) {
<Circle <Circle
x={position.x} x={position.x}
y={position.y} y={position.y}
fill="hsl(0, 63%, 50%)" fill={color}
width={size} width={size}
height={size} height={size}
ref={circleRef}
/> />
</Group> </Group>
); );

View File

@ -6,6 +6,7 @@ import AuthContext from "../contexts/AuthContext";
import MapPointer from "../components/map/MapPointer"; import MapPointer from "../components/map/MapPointer";
import { isEmpty } from "../helpers/shared"; import { isEmpty } from "../helpers/shared";
import { lerp, compare } from "../helpers/vector2"; import { lerp, compare } from "../helpers/vector2";
import useSetting from "../helpers/useSetting";
// Send pointer updates every 50ms (20fps) // Send pointer updates every 50ms (20fps)
const sendTickRate = 50; const sendTickRate = 50;
@ -13,6 +14,7 @@ const sendTickRate = 50;
function NetworkedMapPointer({ session, active, gridSize }) { function NetworkedMapPointer({ session, active, gridSize }) {
const { userId } = useContext(AuthContext); const { userId } = useContext(AuthContext);
const [localPointerState, setLocalPointerState] = useState({}); const [localPointerState, setLocalPointerState] = useState({});
const [pointerColor] = useSetting("pointer.color");
const sessionRef = useRef(session); const sessionRef = useRef(session);
useEffect(() => { useEffect(() => {
@ -22,10 +24,15 @@ function NetworkedMapPointer({ session, active, gridSize }) {
useEffect(() => { useEffect(() => {
if (userId && !(userId in localPointerState)) { if (userId && !(userId in localPointerState)) {
setLocalPointerState({ 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 // Send pointer updates every sendTickRate to peers to save on bandwidth
// We use requestAnimationFrame as setInterval was being blocked during // We use requestAnimationFrame as setInterval was being blocked during
@ -65,9 +72,14 @@ function NetworkedMapPointer({ session, active, gridSize }) {
function updateOwnPointerState(position, visible) { function updateOwnPointerState(position, visible) {
setLocalPointerState((prev) => ({ setLocalPointerState((prev) => ({
...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) { function handleOwnPointerDown(position) {
@ -142,6 +154,7 @@ function NetworkedMapPointer({ session, active, gridSize }) {
id: interp.id, id: interp.id,
visible: interp.from.visible, visible: interp.from.visible,
position: lerp(interp.from.position, interp.to.position, alpha), position: lerp(interp.from.position, interp.to.position, alpha),
color: interp.from.color,
}; };
} }
if (alpha > 1 && !interp.to.visible) { if (alpha > 1 && !interp.to.visible) {
@ -149,6 +162,7 @@ function NetworkedMapPointer({ session, active, gridSize }) {
id: interp.id, id: interp.id,
visible: interp.to.visible, visible: interp.to.visible,
position: interp.to.position, position: interp.to.position,
color: interp.to.color,
}; };
delete interpolationsRef.current[interp.id]; delete interpolationsRef.current[interp.id];
} }
@ -178,6 +192,7 @@ function NetworkedMapPointer({ session, active, gridSize }) {
onPointerDown={pointer.id === userId && handleOwnPointerDown} onPointerDown={pointer.id === userId && handleOwnPointerDown}
onPointerMove={pointer.id === userId && handleOwnPointerMove} onPointerMove={pointer.id === userId && handleOwnPointerMove}
onPointerUp={pointer.id === userId && handleOwnPointerUp} onPointerUp={pointer.id === userId && handleOwnPointerUp}
color={pointer.color}
/> />
))} ))}
</Group> </Group>

View File

@ -37,6 +37,11 @@ function loadVersions(settings) {
...prev, ...prev,
game: { usePassword: true }, game: { usePassword: true },
})); }));
// v1.7.1 - Added pointer color
settings.version(4, (prev) => ({
...prev,
pointer: { color: "red" },
}));
} }
export function getSettings() { export function getSettings() {

View File

@ -3781,7 +3781,7 @@ color-string@^1.5.4:
color-name "^1.0.0" color-name "^1.0.0"
simple-swizzle "^0.2.2" simple-swizzle "^0.2.2"
color@^3.0.0: color@^3.0.0, color@^3.1.3:
version "3.1.3" version "3.1.3"
resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e" resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e"
integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ== integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==