A world-class math input for the web
1import { html } from "htm/preact";
2import { useEffect, useState } from "preact/hooks";
3import { getArrow, getBoxToBoxArrow } from "perfect-arrows";
4
5interface ArrowProps {
6 from: string | Element; // Element selector
7 to: string | Element; // Element selector
8 fromSide: "top" | "bottom" | "left" | "right";
9 toSide: "top" | "bottom" | "left" | "right";
10}
11
12export function Arrow({ from, to, fromSide, toSide }: ArrowProps) {
13 // Rerender 5fps
14 const [, setTick] = useState(0);
15 useEffect(() => {
16 const interval = setInterval(() => {
17 setTick((t) => t + 1);
18 }, 200);
19 return () => clearInterval(interval);
20 }, []);
21
22 const fromElem =
23 from instanceof Element ? from : document.querySelector(from);
24 const toElem = to instanceof Element ? to : document.querySelector(to);
25
26 if (!fromElem || !toElem) {
27 // console.error("Arrow: from or to element not found", { fromElem, toElem });
28 return null;
29 }
30
31 const fromRect = fromElem.getBoundingClientRect();
32 const toRect = toElem.getBoundingClientRect();
33
34 const arrowOptions = {
35 bow: 0.2,
36 stretch: 0.5,
37 stretchMin: 40,
38 stretchMax: 420,
39 padStart: 0,
40 padEnd: 0,
41 flip: false,
42 straights: false,
43 };
44
45 // const arrow = getBoxToBoxArrow(
46 // fromRect.left,
47 // fromRect.top,
48 // fromRect.width,
49 // fromRect.height,
50 // toRect.left,
51 // toRect.top,
52 // toRect.width,
53 // toRect.height,
54 // arrowOptions,
55 // );
56
57 const arrow = getArrow(
58 ...getSide(fromRect, fromSide),
59 ...getSide(toRect, toSide),
60 arrowOptions,
61 );
62
63 const [sx, sy, cx, cy, ex, ey, ae, as, ec] = arrow;
64
65 const endAngleAsDegrees = ae * (180 / Math.PI);
66
67 return html`
68 <svg
69 viewBox="0 0 1280 720"
70 style=${{
71 width: 1280,
72 height: 720,
73 position: "fixed",
74 top: 0,
75 left: 0,
76 pointerEvents: "none",
77 }}
78 stroke="#000"
79 fill="#000"
80 strokeWidth=${3}
81 >
82 <circle cx=${sx} cy=${sy} r=${4} />
83 <path d=${`M${sx},${sy} Q${cx},${cy} ${ex},${ey}`} fill="none" />
84 <polygon
85 points="0,-3 6,0, 0,3"
86 transform=${`translate(${ex},${ey}) rotate(${endAngleAsDegrees})`}
87 />
88 </svg>
89 `;
90}
91
92function getSide(
93 box: DOMRect,
94 side: "top" | "bottom" | "left" | "right",
95): [number, number] {
96 switch (side) {
97 case "top":
98 return [box.left + box.width / 2, box.top];
99 case "bottom":
100 return [box.left + box.width / 2, box.bottom];
101 case "left":
102 return [box.left, box.top + box.height / 2];
103 case "right":
104 return [box.right, box.top + box.height / 2];
105 }
106}