Skip to content

Commit

Permalink
Let ringRadiusPx default
Browse files Browse the repository at this point in the history
  • Loading branch information
benchristel committed Mar 6, 2024
1 parent d65f39f commit 60556ed
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 150 deletions.
7 changes: 7 additions & 0 deletions src/display/MovablePointDisplay.test.ts
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")
})
})
52 changes: 52 additions & 0 deletions src/display/MovablePointDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 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"
13 changes: 11 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,17 @@ export type { TextProps, CardinalDirection } from "./display/Text"
export { Theme } from "./display/Theme"
export type { Filled, Stroked } from "./display/Theme"

export { MovablePoint, MovablePointSVG, useMovementInteraction } from "./interaction/MovablePoint"
export type { MovablePointProps, MovablePointSVGProps } from "./interaction/MovablePoint"
export { MovablePoint } from "./interaction/MovablePoint"
export type { MovablePointProps } from "./interaction/MovablePoint"

export { MovablePointDisplay } from "./display/MovablePointDisplay"
export type { MovablePointDisplayProps } from "./display/MovablePointDisplay"

export { useMovementInteraction } from "./interaction/useMovementInteraction"
export type {
UseMovementInteraction,
UseMovementInteractionArguments,
} from "./interaction/useMovementInteraction"

export { useMovablePoint } from "./interaction/useMovablePoint"
export type {
Expand Down
14 changes: 4 additions & 10 deletions src/interaction/MovablePoint.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import { MovablePoint, MovablePointSVG } from "./MovablePoint"
import { MovablePoint } from "./MovablePoint"

describe("MovablePoint", () => {
it("has a human-readable displayName", () => {
expect(MovablePoint.displayName).toBe("MovablePoint");
})
})

describe("MovablePointSVG", () => {
it("has a human-readable displayName", () => {
expect(MovablePointSVG.displayName).toBe("MovablePointSVG");
})
it("has a human-readable displayName", () => {
expect(MovablePoint.displayName).toBe("MovablePoint")
})
})
147 changes: 9 additions & 138 deletions src/interaction/MovablePoint.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { useDrag } from "@use-gesture/react"
import * as React from "react"
import invariant from "tiny-invariant"
import { Theme } from "../display/Theme"
import { range } from "../math"
import { vec } from "../vec"
import { useTransformContext } from "../context/TransformContext"
import { useSpanContext } from "../context/SpanContext"
import { useMovementInteraction } from "./useMovementInteraction"
import { MovablePointDisplay } from "../display/MovablePointDisplay"

export type ConstraintFunction = (position: vec.Vector2) => vec.Vector2

Expand All @@ -24,128 +21,6 @@ export interface MovablePointProps {
color?: string
}

export function useMovementInteraction({
target,
onMove,
point,
constrain,
}: {
target: React.RefObject<Element>
onMove: (point: vec.Vector2) => unknown
point: vec.Vector2
constrain: (point: vec.Vector2) => vec.Vector2
}): { dragging: boolean } {
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 }
}

export interface MovablePointSVGProps {
color: string
ringRadiusPx: number
dragging: boolean
point: vec.Vector2
}

export const MovablePointSVG = React.forwardRef<SVGGElement, MovablePointSVGProps>(
(props: MovablePointSVGProps, ref) => {
const { color, ringRadiusPx, 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>
)
},
)

MovablePointSVG.displayName = "MovablePointSVG"

export function MovablePoint({
point,
onMove,
Expand All @@ -154,20 +29,16 @@ export function MovablePoint({
}: MovablePointProps) {
const ref = React.useRef<SVGGElement>(null)

const { dragging } = useMovementInteraction({ target: ref, onMove, point, constrain })
const { dragging } = useMovementInteraction({ gestureTarget: ref, onMove, point, constrain })

return (
<MovablePointSVG ref={ref} point={point} color={color} ringRadiusPx={15} dragging={dragging} />
<MovablePointDisplay
ref={ref}
point={point}
color={color}
dragging={dragging}
/>
)
}

MovablePoint.displayName = "MovablePoint"

function getInverseTransform(transform: vec.Matrix) {
const invert = vec.matrixInvert(transform)
invariant(
invert !== null,
"Could not invert transform matrix. Your movable point's transformation matrix might be degenerative (mapping 2D space to a line).",
)
return invert
}
94 changes: 94 additions & 0 deletions src/interaction/useMovementInteraction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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 UseMovementInteractionArguments {
gestureTarget: React.RefObject<Element>
onMove: (point: vec.Vector2) => unknown
point: vec.Vector2
constrain: (point: vec.Vector2) => vec.Vector2
}

export interface UseMovementInteraction {
dragging: boolean
}

export function useMovementInteraction(
args: UseMovementInteractionArguments,
): UseMovementInteraction {
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. Your movable point's transformation matrix might be degenerative (mapping 2D space to a line).",
)
return invert
}

0 comments on commit 60556ed

Please sign in to comment.