You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
95 lines
2.4 KiB
95 lines
2.4 KiB
import { CSSProperties, ReactElement, useCallback, useEffect, useRef } from "react"
|
|
import { add, angle, Pos, relativeTo, size } from "./Pos"
|
|
|
|
export interface BendableArrowProps {
|
|
basePos: Pos
|
|
startPos: Pos
|
|
endPos: Pos
|
|
|
|
startRadius?: number
|
|
endRadius?: number
|
|
|
|
|
|
style?: ArrowStyle
|
|
}
|
|
|
|
export interface ArrowStyle {
|
|
width?: number,
|
|
head?: () => ReactElement,
|
|
tail?: () => ReactElement,
|
|
}
|
|
|
|
|
|
const ArrowStyleDefaults = {
|
|
width: 4
|
|
}
|
|
|
|
function constraintInCircle(pos: Pos, from: Pos, radius: number): Pos {
|
|
const theta = angle(pos, from)
|
|
|
|
return {
|
|
x: pos.x - Math.sin(theta) * radius,
|
|
y: pos.y - Math.cos(theta) * radius
|
|
}
|
|
}
|
|
|
|
export default function BendableArrow({ basePos, startPos, endPos, style, startRadius = 0, endRadius = 0 }: BendableArrowProps) {
|
|
const svgRef = useRef<SVGSVGElement>(null)
|
|
|
|
const pathRef = useRef<SVGPathElement>(null);
|
|
|
|
const styleWidth = style?.width ?? ArrowStyleDefaults.width
|
|
|
|
|
|
const update = () => {
|
|
const startRelative = relativeTo(startPos, basePos)
|
|
const endRelative = relativeTo(endPos, basePos)
|
|
|
|
const tailPos = constraintInCircle(startRelative, endRelative, startRadius)
|
|
const headPos = constraintInCircle(endRelative, startRelative, endRadius)
|
|
|
|
|
|
// the width and height of the arrow svg
|
|
const svgBoxBounds = size(startPos, endPos)
|
|
|
|
const left = Math.min(tailPos.x, headPos.x)
|
|
const top = Math.min(tailPos.y, headPos.y)
|
|
|
|
|
|
const svgStyle: CSSProperties = {
|
|
width: `${svgBoxBounds.x}px`,
|
|
height: `${svgBoxBounds.y}px`,
|
|
|
|
left: `${left}px`,
|
|
top: `${top}px`,
|
|
}
|
|
|
|
|
|
const d = `M${tailPos.x - left} ${tailPos.y - top} L${headPos.x - left} ${headPos.y - top}`
|
|
pathRef.current!.setAttribute("d", d)
|
|
|
|
Object.assign(svgRef.current!.style, svgStyle)
|
|
}
|
|
|
|
useEffect(() => {
|
|
//update on resize
|
|
window.addEventListener('resize', update)
|
|
|
|
return () => window.removeEventListener('resize', update)
|
|
}, [svgRef, basePos, startPos, endPos])
|
|
//update on position changes
|
|
useEffect(update, [svgRef, basePos, startPos, endPos])
|
|
|
|
return (
|
|
<svg ref={svgRef} style={{
|
|
overflow: "visible",
|
|
position: "absolute",
|
|
}}>
|
|
<path
|
|
ref={pathRef}
|
|
stroke={"#000"}
|
|
strokeWidth={styleWidth} />
|
|
</svg>
|
|
)
|
|
}
|