-
Notifications
You must be signed in to change notification settings - Fork 93
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Extract hook and display component from MovablePoint (#144)
We at @Khan would like to add movable circles, polygons, and line segments to Mafs graphs. For the sake of keeping the UX consistent, we'd like the movement interaction for these objects to reuse the hook from MovablePoint, but that hook currently isn't exported. We're also thinking that we may want to customize e.g. the size of the movable points. To that end, this PR splits MovablePoint into a hook and a presentational component (both exported), so that these aspects can be reused and changed independently. @stevenpetryk I'd welcome any feedback you have. I'm especially looking for feedback on naming and code organization. --------- Co-authored-by: Steven Petryk <[email protected]>
- Loading branch information
1 parent
e1eba6a
commit 1d638ec
Showing
7 changed files
with
204 additions
and
105 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { MovablePointDisplay } from "./MovablePointDisplay" | ||
|
||
describe("MovablePointDisplay", () => { | ||
it("has a human-readable displayName", () => { | ||
expect(MovablePointDisplay.displayName).toBe("MovablePointDisplay") | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import * as React from "react" | ||
import { vec } from "../vec" | ||
import { useTransformContext } from "../context/TransformContext" | ||
import { Theme } from "./Theme" | ||
|
||
export interface MovablePointDisplayProps { | ||
color?: string | ||
ringRadiusPx?: number | ||
dragging: boolean | ||
point: vec.Vector2 | ||
} | ||
|
||
export const MovablePointDisplay = React.forwardRef<SVGGElement, MovablePointDisplayProps>( | ||
(props: MovablePointDisplayProps, ref) => { | ||
const { color = Theme.pink, ringRadiusPx = 15, dragging, point } = props | ||
|
||
const { viewTransform, userTransform } = useTransformContext() | ||
|
||
const combinedTransform = React.useMemo( | ||
() => vec.matrixMult(viewTransform, userTransform), | ||
[viewTransform, userTransform], | ||
) | ||
|
||
const [xPx, yPx] = vec.transform(point, combinedTransform) | ||
|
||
return ( | ||
<g | ||
ref={ref} | ||
style={ | ||
{ | ||
"--movable-point-color": color, | ||
"--movable-point-ring-size": `${ringRadiusPx}px`, | ||
} as React.CSSProperties | ||
} | ||
className={`mafs-movable-point ${dragging ? "mafs-movable-point-dragging" : ""}`} | ||
tabIndex={0} | ||
> | ||
<circle className="mafs-movable-point-hitbox" r={30} cx={xPx} cy={yPx}></circle> | ||
<circle | ||
className="mafs-movable-point-focus" | ||
r={ringRadiusPx + 1} | ||
cx={xPx} | ||
cy={yPx} | ||
></circle> | ||
<circle className="mafs-movable-point-ring" r={ringRadiusPx} cx={xPx} cy={yPx}></circle> | ||
<circle className="mafs-movable-point-point" r={6} cx={xPx} cy={yPx}></circle> | ||
</g> | ||
) | ||
}, | ||
) | ||
|
||
MovablePointDisplay.displayName = "MovablePointDisplay" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { MovablePoint } from "./MovablePoint" | ||
|
||
describe("MovablePoint", () => { | ||
it("has a human-readable displayName", () => { | ||
expect(MovablePoint.displayName).toBe("MovablePoint") | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import * as React from "react" | ||
import { useDrag } from "@use-gesture/react" | ||
import invariant from "tiny-invariant" | ||
import { vec } from "../vec" | ||
import { useSpanContext } from "../context/SpanContext" | ||
import { useTransformContext } from "../context/TransformContext" | ||
import { range } from "../math" | ||
|
||
export interface UseMovableArguments { | ||
gestureTarget: React.RefObject<Element> | ||
onMove: (point: vec.Vector2) => unknown | ||
point: vec.Vector2 | ||
constrain: (point: vec.Vector2) => vec.Vector2 | ||
} | ||
|
||
export interface UseMovable { | ||
dragging: boolean | ||
} | ||
|
||
export function useMovable(args: UseMovableArguments): UseMovable { | ||
const { gestureTarget: target, onMove, point, constrain } = args | ||
const [dragging, setDragging] = React.useState(false) | ||
const { xSpan, ySpan } = useSpanContext() | ||
const { viewTransform, userTransform } = useTransformContext() | ||
|
||
const inverseViewTransform = vec.matrixInvert(viewTransform) | ||
invariant(inverseViewTransform, "The view transform must be invertible.") | ||
|
||
const inverseTransform = React.useMemo(() => getInverseTransform(userTransform), [userTransform]) | ||
|
||
const pickup = React.useRef<vec.Vector2>([0, 0]) | ||
|
||
useDrag( | ||
(state) => { | ||
const { type, event } = state | ||
event?.stopPropagation() | ||
|
||
const isKeyboard = type.includes("key") | ||
if (isKeyboard) { | ||
event?.preventDefault() | ||
const { direction: yDownDirection, altKey, metaKey, shiftKey } = state | ||
|
||
const direction = [yDownDirection[0], -yDownDirection[1]] as vec.Vector2 | ||
const span = Math.abs(direction[0]) ? xSpan : ySpan | ||
|
||
let divisions = 50 | ||
if (altKey || metaKey) divisions = 200 | ||
if (shiftKey) divisions = 10 | ||
|
||
const min = span / (divisions * 2) | ||
const tests = range(span / divisions, span / 2, span / divisions) | ||
|
||
for (const dx of tests) { | ||
// Transform the test back into the point's coordinate system | ||
const testMovement = vec.scale(direction, dx) | ||
const testPoint = constrain( | ||
vec.transform( | ||
vec.add(vec.transform(point, userTransform), testMovement), | ||
inverseTransform, | ||
), | ||
) | ||
|
||
if (vec.dist(testPoint, point) > min) { | ||
onMove(testPoint) | ||
break | ||
} | ||
} | ||
} else { | ||
const { last, movement: pixelMovement, first } = state | ||
|
||
setDragging(!last) | ||
|
||
if (first) pickup.current = vec.transform(point, userTransform) | ||
if (vec.mag(pixelMovement) === 0) return | ||
|
||
const movement = vec.transform(pixelMovement, inverseViewTransform) | ||
onMove(constrain(vec.transform(vec.add(pickup.current, movement), inverseTransform))) | ||
} | ||
}, | ||
{ target, eventOptions: { passive: false } }, | ||
) | ||
return { dragging } | ||
} | ||
|
||
function getInverseTransform(transform: vec.Matrix) { | ||
const invert = vec.matrixInvert(transform) | ||
invariant( | ||
invert !== null, | ||
"Could not invert transform matrix. A parent transformation matrix might be degenerative (mapping 2D space to a line).", | ||
) | ||
return invert | ||
} |