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);