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(null) const pathRef = useRef(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 ( ) }