Skip to content

Commit

Permalink
new(drag): Add restrictToPath as a Drag parameter (#1379)
Browse files Browse the repository at this point in the history
* Add restrictToPath as a Drag parameter

Allow the user to pass an SVGGeometryElement to restrict the drag area
to following the path of an SVG.

Useful for constrining the drag of an object to a curved line.

* Performance Improvements

Calculate and cache sample points along restrictToPath.

This turns out to be fairly tricky as the path is a ref and we need to
use the getTotalLength function as an estimate to when the path has changed.

Remove getParentSvg. Turns out we can just use the DOMMatrix from the
path element itself, saving the need to traverse the DOM.

Fix bug with summing points for offset, rather than finding the difference.

* Update packages/visx-drag/src/useDrag.ts

Complete sentence for docs

Co-authored-by: Chris Williams <williaster@users.noreply.github.com>

Co-authored-by: Chris Williams <williaster@users.noreply.github.com>
  • Loading branch information
valtism and williaster authored Jan 20, 2022
1 parent 499e086 commit 3ac3f5e
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 44 deletions.
1 change: 1 addition & 0 deletions packages/visx-drag/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"dependencies": {
"@types/react": "*",
"@visx/event": "2.1.2",
"@visx/point": "^2.1.0",
"prop-types": "^15.5.10"
}
}
2 changes: 2 additions & 0 deletions packages/visx-drag/src/Drag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default function Drag({
y,
isDragging,
restrict,
restrictToPath,
}: DragProps) {
const drag = useDrag({
resetOnStart,
Expand All @@ -46,6 +47,7 @@ export default function Drag({
dy,
isDragging,
restrict,
restrictToPath,
});

return (
Expand Down
90 changes: 46 additions & 44 deletions packages/visx-drag/src/useDrag.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Point, subtractPoints, sumPoints } from '@visx/point';
import { localPoint } from '@visx/event';
import useStateWithCallback from './util/useStateWithCallback';
import clampNumber from './util/clampNumber';
import restrictPoint from './util/restrictPoint';
import useSamplesAlongPath from './util/useSamplesAlongPath';

export type MouseTouchOrPointerEvent = React.MouseEvent | React.TouchEvent | React.PointerEvent;

Expand Down Expand Up @@ -38,6 +40,8 @@ export type UseDragOptions = {
yMin?: number;
yMax?: number;
};
/** Limit drag to an SVG path. Overrides `restrict` constraints. */
restrictToPath?: SVGGeometryElement | null;
};

export type DragState = {
Expand Down Expand Up @@ -75,20 +79,11 @@ export default function useDrag({
dy,
isDragging,
restrict = {},
restrictToPath,
}: UseDragOptions | undefined = {}): UseDrag {
// use ref to detect prop changes
const positionPropsRef = useRef({ x, y, dx, dy });

const { xMin, xMax, yMin, yMax } = restrict;
const clampX = useCallback(
(num: number) => clampNumber(num, xMin ?? -Infinity, xMax ?? Infinity),
[xMax, xMin],
);
const clampY = useCallback(
(num: number) => clampNumber(num, yMin ?? -Infinity, yMax ?? Infinity),
[yMax, yMin],
);

const [dragState, setDragStateWithCallback] = useStateWithCallback<DragState>({
x,
y,
Expand All @@ -98,10 +93,9 @@ export default function useDrag({
});

// Track distance between pointer on dragStart and position of element being dragged
const [dragStartPointerOffset, setDragStartPointerOffset] = useState<{
x: number;
y: number;
}>({ x: 0, y: 0 });
const [dragStartPointerOffset, setDragStartPointerOffset] = useState<Point>(
new Point({ x: 0, y: 0 }),
);

// if prop position changes, update state
useEffect(() => {
Expand All @@ -128,30 +122,32 @@ export default function useDrag({
}
}, [dragState.isDragging, isDragging, setDragStateWithCallback]);

const restrictToPathSamples = useSamplesAlongPath(restrictToPath);

const handleDragStart = useCallback(
(event: MouseTouchOrPointerEvent) => {
event.persist();

setDragStateWithCallback(
(currState) => {
const currentPoint = {
x: (currState.x || 0) + currState.dx,
y: (currState.y || 0) + currState.dy,
};
const eventPoint = localPoint(event) || { x: 0, y: 0 };
// eslint-disable-next-line no-shadow
const { x = 0, y = 0, dx, dy } = currState;
const currentPoint = new Point({
x: (x || 0) + dx,
y: (y || 0) + dy,
});
const eventPoint = localPoint(event) || new Point({ x: 0, y: 0 });
const point = snapToPointer ? eventPoint : currentPoint;
const dragPoint = restrictPoint(point, restrictToPathSamples, restrict);

setDragStartPointerOffset({
x: currentPoint.x - eventPoint.x,
y: currentPoint.y - eventPoint.y,
});
setDragStartPointerOffset(subtractPoints(currentPoint, eventPoint));

return {
isDragging: true,
dx: resetOnStart ? 0 : currState.dx,
dy: resetOnStart ? 0 : currState.dy,
x: resetOnStart ? clampX(point.x) : clampX(point.x) - currState.dx,
y: resetOnStart ? clampY(point.y) : clampY(point.y) - currState.dy,
x: resetOnStart ? dragPoint.x : dragPoint.x - currState.dx,
y: resetOnStart ? dragPoint.y : dragPoint.y - currState.dy,
};
},
onDragStart &&
Expand All @@ -160,7 +156,14 @@ export default function useDrag({
}),
);
},
[clampX, clampY, onDragStart, resetOnStart, setDragStateWithCallback, snapToPointer],
[
onDragStart,
resetOnStart,
restrict,
restrictToPathSamples,
setDragStateWithCallback,
snapToPointer,
],
);

const handleDragMove = useCallback(
Expand All @@ -169,19 +172,19 @@ export default function useDrag({

setDragStateWithCallback(
(currState) => {
const point = localPoint(event) || { x: 0, y: 0 };
return currState.isDragging
? {
...currState,
isDragging: true,
dx: snapToPointer
? clampX(point.x) - (currState.x || 0)
: clampX(point.x + dragStartPointerOffset.x) - (currState.x || 0),
dy: snapToPointer
? clampY(point.y) - (currState.y || 0)
: clampY(point.y + dragStartPointerOffset.y) - (currState.y || 0),
}
: currState;
if (!currState.isDragging) return currState;
// eslint-disable-next-line no-shadow
const { x = 0, y = 0 } = currState;
const pointerPoint = localPoint(event) || new Point({ x: 0, y: 0 });
const point = snapToPointer
? pointerPoint
: sumPoints(pointerPoint, dragStartPointerOffset);
const dragPoint = restrictPoint(point, restrictToPathSamples, restrict);
return {
...currState,
dx: dragPoint.x - x,
dy: dragPoint.y - y,
};
},
onDragMove &&
((currState) => {
Expand All @@ -193,10 +196,9 @@ export default function useDrag({
setDragStateWithCallback,
onDragMove,
snapToPointer,
dragStartPointerOffset.x,
dragStartPointerOffset.y,
clampX,
clampY,
dragStartPointerOffset,
restrictToPathSamples,
restrict,
],
);

Expand Down
13 changes: 13 additions & 0 deletions packages/visx-drag/src/util/getClosestPoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/** Gets closest point from list of points */
export default function getClosestPoint(point: { x: number; y: number }, samples: DOMPoint[]) {
let closestPoint = point;
let minDistance = Infinity;
for (const sample of samples) {
const distance = Math.sqrt((sample.x - point.x) ** 2 + (sample.y - point.y) ** 2);
if (distance < minDistance) {
minDistance = distance;
closestPoint = { x: sample.x, y: sample.y };
}
}
return closestPoint;
}
18 changes: 18 additions & 0 deletions packages/visx-drag/src/util/restrictPoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { UseDragOptions } from '../useDrag';
import clampNumber from './clampNumber';
import getClosestPoint from './getClosestPoint';

/** Restrict a point to an area, or samples along a path. */
export default function restrictPoint(
point: { x: number; y: number },
samples: DOMPoint[],
restrict: UseDragOptions['restrict'] = {},
) {
if (samples.length > 0) {
return getClosestPoint(point, samples);
}
return {
x: clampNumber(point.x, restrict.xMin ?? -Infinity, restrict.xMax ?? Infinity),
y: clampNumber(point.y, restrict.yMin ?? -Infinity, restrict.yMax ?? Infinity),
};
}
26 changes: 26 additions & 0 deletions packages/visx-drag/src/util/useSamplesAlongPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useMemo } from 'react';

function getSamples(restrictToPath: SVGGeometryElement, transform?: DOMMatrix, precision = 1) {
if (!restrictToPath) return [];
const samples = [];
const pathLength = restrictToPath.getTotalLength();
for (let sampleLength = 0; sampleLength <= pathLength; sampleLength += precision) {
const sample = restrictToPath.getPointAtLength(sampleLength);
const transformedSample = sample.matrixTransform(transform);
samples.push(transformedSample);
}
return samples;
}

/** Return samples along a path, relative to the parent SVG */
export default function useSamplesAlongPath(restrictToPath?: SVGGeometryElement | null) {
const samples = useMemo(() => {
if (!restrictToPath) return [];
const transform = restrictToPath.getCTM() || new DOMMatrix();
return getSamples(restrictToPath, transform);
// The path can transform without triggering a re-render,
// so we need to update the samples whenever the length changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [restrictToPath?.getTotalLength()]);
return samples;
}
2 changes: 2 additions & 0 deletions packages/visx-point/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { default as Point } from './Point';
export { default as sumPoints } from './sumPoints';
export { default as subtractPoints } from './subtractPoints';
8 changes: 8 additions & 0 deletions packages/visx-point/src/subtractPoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Point from './Point';

export default function subtractPoints(point1: Point, point2: Point) {
return new Point({
x: point1.x - point2.x,
y: point1.y - point2.y,
});
}
8 changes: 8 additions & 0 deletions packages/visx-point/src/sumPoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Point from './Point';

export default function sumPoints(point1: Point, point2: Point) {
return new Point({
x: point1.x + point2.x,
y: point1.y + point2.y,
});
}

0 comments on commit 3ac3f5e

Please sign in to comment.