Skip to content

Commit

Permalink
Enable bumping points via the keyboard (#23)
Browse files Browse the repository at this point in the history
* wip

* Update API report (no actual breaking changes)
  • Loading branch information
stevenpetryk authored Nov 5, 2021
1 parent c085758 commit 5a9c7ff
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 77 deletions.
2 changes: 1 addition & 1 deletion .api-report/mafs.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ export interface UseMovablePoint {
}

// @public (undocumented)
export function useMovablePoint([initialX, initialY]: Vector2, { constrain, color, transform }?: UseMovablePointArguments): UseMovablePoint;
export function useMovablePoint(initialPoint: Vector2, { constrain, color, transform }?: UseMovablePointArguments): UseMovablePoint;

// @public (undocumented)
export interface UseMovablePointArguments {
Expand Down
7 changes: 7 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
display: block;
background: var(--mafs-bg);
overflow: hidden;
-webkit-user-select: none;
user-select: none;
font-family: inherit;
font-variant-numeric: tabular-nums;
Expand Down Expand Up @@ -32,11 +33,17 @@
stroke: var(--mafs-fg);
}

.MafsWrapper,
.MafsView .draggable-hitbox {
touch-action: none;
}

.MafsView .draggable {
transition: r 0.2s ease, stroke-width 0.2s ease;
stroke-width: 18px;
cursor: grab;
outline: 0 !important;
touch-action: none;

color: hsl(0, 100%, 47%);
}
Expand Down
112 changes: 79 additions & 33 deletions src/interaction/MovablePoint.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,104 @@
import React, { useState } from "react"
import * as vec from "vec-la"
import { useDrag } from "@use-gesture/react"
import React, { useMemo, useRef, useState } from "react"
import invariant from "tiny-invariant"
import * as vec from "vec-la"
import { matrixInvert, range, Vector2 } from "../math"
import { useScaleContext } from "../view/ScaleContext"
import { Vector2 } from "../math"

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

interface MovablePointProps {
x: number
y: number
onMovement: (props: { movement: Vector2; first: boolean }) => void
color?: string
point: Vector2
onMove: (point: Vector2) => void
transform: vec.Matrix
constrain: ConstraintFunction
color: string
}

/** @private */
const MovablePoint: React.VFC<MovablePointProps> = ({
x,
y,
onMovement,
color = "var(--mafs-blue)",
point,
onMove,
transform,
constrain,
color,
}) => {
const { scaleX, scaleY, xSpan, ySpan, inversePixelMatrix } = useScaleContext()
const { xSpan, ySpan, pixelMatrix, inversePixelMatrix } = useScaleContext()
const inverseTransform = useMemo(() => getInverseTransform(transform), [transform])

const [dragging, setDragging] = useState(false)
const [displayX, displayY] = vec.transform(vec.transform(point, transform), pixelMatrix)

const bind = useDrag(
({ event, down, movement, first }) => {
event?.stopPropagation()
setDragging(down)
onMovement({ movement: vec.transform(movement, inversePixelMatrix), first })
},
{ eventOptions: { passive: false } }
)
const pickup = useRef<Vector2>([0, 0])

const bind = useDrag(({ event, down, movement: pixelMovement, first }) => {
event?.stopPropagation()
setDragging(down)

if (first) pickup.current = vec.transform(point, transform)
if (vec.mag(pixelMovement) === 0) return

const movement = vec.transform(pixelMovement, inversePixelMatrix)
const newPoint = constrain(vec.transform(vec.add(pickup.current, movement), inverseTransform))

onMove(newPoint)
})

function handleKeyDown(event: React.KeyboardEvent) {
const scale = event.altKey || event.metaKey || event.shiftKey ? 400 : 40
const small = event.altKey || event.metaKey || event.shiftKey

let span: number
let testDir: Vector2

switch (event.key) {
case "ArrowLeft":
span = xSpan
testDir = [-1, 0]
event.preventDefault()
onMovement({ movement: [0, 0], first: true })
onMovement({ movement: [-xSpan / scale, 0], first: false })
break
case "ArrowRight":
onMovement({ movement: [0, 0], first: true })
onMovement({ movement: [xSpan / scale, 0], first: false })
span = xSpan
testDir = [1, 0]
event.preventDefault()
break
case "ArrowUp":
onMovement({ movement: [0, 0], first: true })
onMovement({ movement: [0, ySpan / scale], first: false })
span = ySpan
testDir = [0, 1]
event.preventDefault()
break
case "ArrowDown":
onMovement({ movement: [0, 0], first: true })
onMovement({ movement: [0, -ySpan / scale], first: false })
span = ySpan
testDir = [0, -1]
event.preventDefault()
break
default:
return
}

const divisions = small ? 200 : 50
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(testDir, dx)
const testPoint = constrain(
vec.transform(vec.add(vec.transform(point, transform), testMovement), inverseTransform)
)

if (vec.dist(testPoint, point) > min) {
onMove(testPoint)
break
}
}
}

return (
<g {...bind()}>
<circle cx={scaleX(x)} cy={scaleY(y)} r={30} fill="transparent"></circle>
<g {...bind()} className="draggable-hitbox">
<circle cx={displayX} cy={displayY} r={30} fill="transparent"></circle>
<circle
cx={scaleX(x)}
cy={scaleY(y)}
cx={displayX}
cy={displayY}
r={6}
fill={color}
stroke={color}
Expand All @@ -74,4 +111,13 @@ const MovablePoint: React.VFC<MovablePointProps> = ({
)
}

function getInverseTransform(transform: vec.Matrix) {
const invert = matrixInvert(transform)
invariant(
invert !== null,
"Could not invert transform matrix. Your movable point's constraint function might be degenerative (mapping 2D space to a line)."
)
return invert
}

export default MovablePoint
65 changes: 23 additions & 42 deletions src/interaction/useMovablePoint.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import React, { useRef } from "react"
import { useMemo, useState } from "react"
import MovablePoint from "./MovablePoint"
import React, { useMemo, useState } from "react"
import * as vec from "vec-la"
import { theme } from "../display/Theme"
import { matrixInvert, Vector2 } from "../math"
import invariant from "tiny-invariant"
import { Vector2 } from "../math"
import MovablePoint from "./MovablePoint"

const identity = vec.matrixBuilder().get()

Expand Down Expand Up @@ -36,59 +34,42 @@ export interface UseMovablePoint {
}

function useMovablePoint(
[initialX, initialY]: Vector2,
initialPoint: Vector2,
{ constrain, color = theme.pink, transform = identity }: UseMovablePointArguments = {}
): UseMovablePoint {
let constraintFunction: ConstraintFunction = ([x, y]) => [x, y]
if (constrain === "horizontal") {
constraintFunction = ([x]) => [x, initialY]
} else if (constrain === "vertical") {
constraintFunction = ([, y]) => [initialX, y]
} else if (typeof constrain === "function") {
constraintFunction = constrain
}

const [point, setPoint] = useState(() => constraintFunction([initialX, initialY]))
const [initialX, initialY] = initialPoint
const [point, setPoint] = useState<Vector2>(initialPoint)
const [x, y] = point
const [displayX, displayY] = vec.transform(point, transform)

const pickup = useRef<Vector2>([0, 0])
const constraintFunction: ConstraintFunction = React.useMemo(() => {
if (constrain === "horizontal") {
return ([x]) => [x, initialY]
} else if (constrain === "vertical") {
return ([, y]) => [initialX, y]
} else if (typeof constrain === "function") {
return constrain
}

return ([x, y]) => [x, y]
}, [constrain, initialX, initialY])

const element = useMemo(() => {
return (
<MovablePoint
x={displayX}
y={displayY}
color={color}
onMovement={({ movement, first }) => {
if (first) {
pickup.current = vec.transform([x, y], transform)
}
setPoint(
constraintFunction(
vec.transform(vec.add(pickup.current, movement), getInverseTransform(transform))
)
)
}}
{...{ point, transform, color }}
constrain={constraintFunction}
point={point}
onMove={setPoint}
/>
)
}, [x, y, displayX, displayY, color, transform])
}, [point, transform, color, constraintFunction])

return {
x,
y,
point,
point: [x, y],
element,
}
}

function getInverseTransform(transform: vec.Matrix) {
const invert = matrixInvert(transform)
invariant(
invert !== null,
"Could not invert transform matrix. Your movable point's constraint function might be degenerative (mapping 2D space to a line)."
)
return invert
}

export default useMovablePoint
2 changes: 1 addition & 1 deletion src/view/MafsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ const MafsView: React.FC<MafsViewProps> = ({

return (
<div
className="overflow-hidden w-auto"
className="MafsWrapper overflow-hidden w-auto"
style={{ width: desiredCssWidth }}
ref={ref}
{...bind()}
Expand Down

0 comments on commit 5a9c7ff

Please sign in to comment.