A world-class math input for the web
at main 106 lines 2.6 kB view raw
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}