From 142b3f804f0e047904d7acde56dffa72d09c7dea Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 31 Jul 2020 14:20:36 +1000 Subject: [PATCH] Added a trail to the pointer --- src/components/map/MapPointer.js | 19 +++--- src/helpers/konva.js | 94 +++++++++++++++++++++++++++++- src/helpers/vector2.js | 48 +++++++++++++++ src/network/NetworkedMapPointer.js | 4 +- 4 files changed, 154 insertions(+), 11 deletions(-) diff --git a/src/components/map/MapPointer.js b/src/components/map/MapPointer.js index abab61e..440910e 100644 --- a/src/components/map/MapPointer.js +++ b/src/components/map/MapPointer.js @@ -1,11 +1,15 @@ import React, { useContext, useEffect } from "react"; -import { Group, Circle } from "react-konva"; +import { Group } from "react-konva"; import MapInteractionContext from "../../contexts/MapInteractionContext"; import MapStageContext from "../../contexts/MapStageContext"; import { getStrokeWidth } from "../../helpers/drawing"; -import { getRelativePointerPositionNormalized } from "../../helpers/konva"; +import { + getRelativePointerPositionNormalized, + Trail, +} from "../../helpers/konva"; +import { multiply } from "../../helpers/vector2"; import colors from "../../helpers/colors"; @@ -63,12 +67,11 @@ function MapPointer({ return ( {visible && ( - )} diff --git a/src/helpers/konva.js b/src/helpers/konva.js index 83da4dd..5767d8c 100644 --- a/src/helpers/konva.js +++ b/src/helpers/konva.js @@ -1,5 +1,7 @@ -import React, { useState } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { Line, Group, Path, Circle } from "react-konva"; +import { lerp } from "./shared"; +import * as Vector2 from "./vector2"; // Holes should be wound in the opposite direction as the containing points array export function HoleyLine({ holes, ...props }) { @@ -140,6 +142,96 @@ export function Tick({ x, y, scale, onClick, cross }) { ); } +export function Trail({ position, size, duration, segments }) { + const trailRef = useRef(); + const pointsRef = useRef([]); + const prevPositionRef = useRef(position); + // Add a new point every time position is changed + useEffect(() => { + if (Vector2.compare(position, prevPositionRef.current, 0.0001)) { + return; + } + pointsRef.current.push({ ...position, lifetime: duration }); + prevPositionRef.current = position; + }, [position, duration]); + + // Advance lifetime of trail + useEffect(() => { + let prevTime = performance.now(); + let request = requestAnimationFrame(animate); + function animate(time) { + request = requestAnimationFrame(animate); + const deltaTime = time - prevTime; + prevTime = time; + + if (pointsRef.current.length === 0) { + return; + } + + let expired = 0; + for (let point of pointsRef.current) { + point.lifetime -= deltaTime; + if (point.lifetime < 0) { + expired++; + } + } + if (expired > 0) { + pointsRef.current = pointsRef.current.slice( + expired, + pointsRef.current.length + ); + } + if (trailRef.current) { + trailRef.current.getLayer().draw(); + } + } + + return () => { + cancelAnimationFrame(request); + }; + }, []); + + // Custom scene function for drawing a trail from a line + function sceneFunc(context) { + // Resample points to ensure a smooth trail + const resampledPoints = Vector2.resample(pointsRef.current, segments); + 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(); + } + } + + return ( + + + + + ); +} + +Trail.defaultProps = { + // Duration of each point in milliseconds + duration: 200, + // Number of segments in the trail, resampled from the points + segments: 50, +}; + export function getRelativePointerPosition(node) { let transform = node.getAbsoluteTransform().copy(); transform.invert(); diff --git a/src/helpers/vector2.js b/src/helpers/vector2.js index 8b2bdb3..7697b9b 100644 --- a/src/helpers/vector2.js +++ b/src/helpers/vector2.js @@ -246,3 +246,51 @@ export function distance(a, b, type) { export function lerp(a, b, alpha) { return { x: lerpNumber(a.x, b.x, alpha), y: lerpNumber(a.y, b.y, alpha) }; } + +/** + * Returns total length of a an array of points treated as a path + * @param {Array} points the array of points in the path + */ +export function pathLength(points) { + let l = 0; + for (let i = 1; i < points.length; i++) { + l += distance(points[i - 1], points[i], "euclidean"); + } + return l; +} + +/** + * Resample a path to n number of evenly distributed points + * based off of http://depts.washington.edu/acelab/proj/dollar/index.html + * @param {Array} points the points to resample + * @param {number} n the number of new points + */ +export function resample(points, n) { + if (points.length === 0 || n <= 0) { + return []; + } + let localPoints = [...points]; + const intervalLength = pathLength(localPoints) / (n - 1); + let resampledPoints = [localPoints[0]]; + let currentDistance = 0; + for (let i = 1; i < localPoints.length; i++) { + let d = distance(localPoints[i - 1], localPoints[i], "euclidean"); + if (currentDistance + d >= intervalLength) { + let newPoint = lerp( + localPoints[i - 1], + localPoints[i], + (intervalLength - currentDistance) / d + ); + resampledPoints.push(newPoint); + localPoints.splice(i, 0, newPoint); + currentDistance = 0; + } else { + currentDistance += d; + } + } + if (resampledPoints.length === n - 1) { + resampledPoints.push(localPoints[localPoints.length - 1]); + } + + return resampledPoints; +} diff --git a/src/network/NetworkedMapPointer.js b/src/network/NetworkedMapPointer.js index 5611750..c010605 100644 --- a/src/network/NetworkedMapPointer.js +++ b/src/network/NetworkedMapPointer.js @@ -7,8 +7,8 @@ import MapPointer from "../components/map/MapPointer"; import { isEmpty } from "../helpers/shared"; import { lerp } from "../helpers/vector2"; -// Send pointer updates every 100ms -const sendTickRate = 100; +// Send pointer updates every 50ms +const sendTickRate = 50; function NetworkedMapPointer({ session, active, gridSize }) { const { userId } = useContext(AuthContext);