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.

utils.ts 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890
  1. import { Data, Bounds } from "types"
  2. import * as svg from "./svg"
  3. import * as vec from "./vec"
  4. export function screenToWorld(point: number[], data: Data) {
  5. return vec.sub(vec.div(point, data.camera.zoom), data.camera.point)
  6. }
  7. /**
  8. * Get a bounding box that includes two bounding boxes.
  9. * @param a Bounding box
  10. * @param b Bounding box
  11. * @returns
  12. */
  13. export function getExpandedBounds(a: Bounds, b: Bounds) {
  14. const minX = Math.min(a.minX, b.minX),
  15. minY = Math.min(a.minY, b.minY),
  16. maxX = Math.max(a.maxX, b.maxX),
  17. maxY = Math.max(a.maxY, b.maxY),
  18. width = Math.abs(maxX - minX),
  19. height = Math.abs(maxY - minY)
  20. return { minX, minY, maxX, maxY, width, height }
  21. }
  22. /**
  23. * Get the common bounds of a group of bounds.
  24. * @returns
  25. */
  26. export function getCommonBounds(...b: Bounds[]) {
  27. if (b.length < 2) return b[0]
  28. let bounds = b[0]
  29. for (let i = 1; i < b.length; i++) {
  30. bounds = getExpandedBounds(bounds, b[i])
  31. }
  32. return bounds
  33. }
  34. export function getBoundsFromPoints(a: number[], b: number[]) {
  35. const minX = Math.min(a[0], b[0])
  36. const maxX = Math.max(a[0], b[0])
  37. const minY = Math.min(a[1], b[1])
  38. const maxY = Math.max(a[1], b[1])
  39. return {
  40. minX,
  41. maxX,
  42. minY,
  43. maxY,
  44. width: maxX - minX,
  45. height: maxY - minY,
  46. }
  47. }
  48. // A helper for getting tangents.
  49. export function getCircleTangentToPoint(
  50. A: number[],
  51. r0: number,
  52. P: number[],
  53. side: number
  54. ) {
  55. const B = vec.lrp(A, P, 0.5),
  56. r1 = vec.dist(A, B),
  57. delta = vec.sub(B, A),
  58. d = vec.len(delta)
  59. if (!(d <= r0 + r1 && d >= Math.abs(r0 - r1))) {
  60. return
  61. }
  62. const a = (r0 * r0 - r1 * r1 + d * d) / (2.0 * d),
  63. n = 1 / d,
  64. p = vec.add(A, vec.mul(delta, a * n)),
  65. h = Math.sqrt(r0 * r0 - a * a),
  66. k = vec.mul(vec.per(delta), h * n)
  67. return side === 0 ? vec.add(p, k) : vec.sub(p, k)
  68. }
  69. export function circleCircleIntersections(a: number[], b: number[]) {
  70. const R = a[2],
  71. r = b[2]
  72. let dx = b[0] - a[0],
  73. dy = b[1] - a[1]
  74. const d = Math.sqrt(dx * dx + dy * dy),
  75. x = (d * d - r * r + R * R) / (2 * d),
  76. y = Math.sqrt(R * R - x * x)
  77. dx /= d
  78. dy /= d
  79. return [
  80. [a[0] + dx * x - dy * y, a[1] + dy * x + dx * y],
  81. [a[0] + dx * x + dy * y, a[1] + dy * x - dx * y],
  82. ]
  83. }
  84. export function getClosestPointOnCircle(
  85. C: number[],
  86. r: number,
  87. P: number[],
  88. padding = 0
  89. ) {
  90. const v = vec.sub(C, P)
  91. return vec.sub(C, vec.mul(vec.div(v, vec.len(v)), r + padding))
  92. }
  93. export function projectPoint(p0: number[], a: number, d: number) {
  94. return [Math.cos(a) * d + p0[0], Math.sin(a) * d + p0[1]]
  95. }
  96. function shortAngleDist(a0: number, a1: number) {
  97. const max = Math.PI * 2
  98. const da = (a1 - a0) % max
  99. return ((2 * da) % max) - da
  100. }
  101. export function lerpAngles(a0: number, a1: number, t: number) {
  102. return a0 + shortAngleDist(a0, a1) * t
  103. }
  104. export function getBezierCurveSegments(points: number[][], tension = 0.4) {
  105. const len = points.length,
  106. cpoints: number[][] = [...points]
  107. if (len < 2) {
  108. throw Error("Curve must have at least two points.")
  109. }
  110. for (let i = 1; i < len - 1; i++) {
  111. const p0 = points[i - 1],
  112. p1 = points[i],
  113. p2 = points[i + 1]
  114. const pdx = p2[0] - p0[0],
  115. pdy = p2[1] - p0[1],
  116. pd = Math.hypot(pdx, pdy),
  117. nx = pdx / pd, // normalized x
  118. ny = pdy / pd, // normalized y
  119. dp = Math.hypot(p1[0] - p0[0], p1[1] - p0[1]), // Distance to previous
  120. dn = Math.hypot(p1[0] - p2[0], p1[1] - p2[1]) // Distance to next
  121. cpoints[i] = [
  122. // tangent start
  123. p1[0] - nx * dp * tension,
  124. p1[1] - ny * dp * tension,
  125. // tangent end
  126. p1[0] + nx * dn * tension,
  127. p1[1] + ny * dn * tension,
  128. // normal
  129. nx,
  130. ny,
  131. ]
  132. }
  133. // TODO: Reflect the nearest control points, not average them
  134. const d0 = Math.hypot(points[0][0] + cpoints[1][0])
  135. cpoints[0][2] = (points[0][0] + cpoints[1][0]) / 2
  136. cpoints[0][3] = (points[0][1] + cpoints[1][1]) / 2
  137. cpoints[0][4] = (cpoints[1][0] - points[0][0]) / d0
  138. cpoints[0][5] = (cpoints[1][1] - points[0][1]) / d0
  139. const d1 = Math.hypot(points[len - 1][1] + cpoints[len - 1][1])
  140. cpoints[len - 1][0] = (points[len - 1][0] + cpoints[len - 2][2]) / 2
  141. cpoints[len - 1][1] = (points[len - 1][1] + cpoints[len - 2][3]) / 2
  142. cpoints[len - 1][4] = (cpoints[len - 2][2] - points[len - 1][0]) / -d1
  143. cpoints[len - 1][5] = (cpoints[len - 2][3] - points[len - 1][1]) / -d1
  144. const results: {
  145. start: number[]
  146. tangentStart: number[]
  147. normalStart: number[]
  148. pressureStart: number
  149. end: number[]
  150. tangentEnd: number[]
  151. normalEnd: number[]
  152. pressureEnd: number
  153. }[] = []
  154. for (let i = 1; i < cpoints.length; i++) {
  155. results.push({
  156. start: points[i - 1].slice(0, 2),
  157. tangentStart: cpoints[i - 1].slice(2, 4),
  158. normalStart: cpoints[i - 1].slice(4, 6),
  159. pressureStart: 2 + ((i - 1) % 2 === 0 ? 1.5 : 0),
  160. end: points[i].slice(0, 2),
  161. tangentEnd: cpoints[i].slice(0, 2),
  162. normalEnd: cpoints[i].slice(4, 6),
  163. pressureEnd: 2 + (i % 2 === 0 ? 1.5 : 0),
  164. })
  165. }
  166. return results
  167. }
  168. export function cubicBezier(
  169. tx: number,
  170. x1: number,
  171. y1: number,
  172. x2: number,
  173. y2: number
  174. ) {
  175. // Inspired by Don Lancaster's two articles
  176. // http://www.tinaja.com/glib/cubemath.pdf
  177. // http://www.tinaja.com/text/bezmath.html
  178. // Set start and end point
  179. const x0 = 0,
  180. y0 = 0,
  181. x3 = 1,
  182. y3 = 1,
  183. // Convert the coordinates to equation space
  184. A = x3 - 3 * x2 + 3 * x1 - x0,
  185. B = 3 * x2 - 6 * x1 + 3 * x0,
  186. C = 3 * x1 - 3 * x0,
  187. D = x0,
  188. E = y3 - 3 * y2 + 3 * y1 - y0,
  189. F = 3 * y2 - 6 * y1 + 3 * y0,
  190. G = 3 * y1 - 3 * y0,
  191. H = y0,
  192. // Variables for the loop below
  193. iterations = 5
  194. let i: number,
  195. slope: number,
  196. x: number,
  197. t = tx
  198. // Loop through a few times to get a more accurate time value, according to the Newton-Raphson method
  199. // http://en.wikipedia.org/wiki/Newton's_method
  200. for (i = 0; i < iterations; i++) {
  201. // The curve's x equation for the current time value
  202. x = A * t * t * t + B * t * t + C * t + D
  203. // The slope we want is the inverse of the derivate of x
  204. slope = 1 / (3 * A * t * t + 2 * B * t + C)
  205. // Get the next estimated time value, which will be more accurate than the one before
  206. t -= (x - tx) * slope
  207. t = t > 1 ? 1 : t < 0 ? 0 : t
  208. }
  209. // Find the y value through the curve's y equation, with the now more accurate time value
  210. return Math.abs(E * t * t * t + F * t * t + G * t * H)
  211. }
  212. export function copyToClipboard(string: string) {
  213. let textarea: HTMLTextAreaElement
  214. let result: boolean
  215. try {
  216. navigator.clipboard.writeText(string)
  217. } catch (e) {
  218. try {
  219. textarea = document.createElement("textarea")
  220. textarea.setAttribute("position", "fixed")
  221. textarea.setAttribute("top", "0")
  222. textarea.setAttribute("readonly", "true")
  223. textarea.setAttribute("contenteditable", "true")
  224. textarea.style.position = "fixed" // prevent scroll from jumping to the bottom when focus is set.
  225. textarea.value = string
  226. document.body.appendChild(textarea)
  227. textarea.focus()
  228. textarea.select()
  229. const range = document.createRange()
  230. range.selectNodeContents(textarea)
  231. const sel = window.getSelection()
  232. sel.removeAllRanges()
  233. sel.addRange(range)
  234. textarea.setSelectionRange(0, textarea.value.length)
  235. result = document.execCommand("copy")
  236. } catch (err) {
  237. result = null
  238. } finally {
  239. document.body.removeChild(textarea)
  240. }
  241. }
  242. return !!result
  243. }
  244. /**
  245. * Get a bezier curve data to for a spline that fits an array of points.
  246. * @param points An array of points formatted as [x, y]
  247. * @param k Tension
  248. * @returns An array of points as [cp1x, cp1y, cp2x, cp2y, px, py].
  249. */
  250. export function getSpline(pts: number[][], k = 0.5) {
  251. let p0: number[],
  252. [p1, p2, p3] = pts
  253. const results: number[][] = []
  254. for (let i = 1, len = pts.length; i < len; i++) {
  255. p0 = p1
  256. p1 = p2
  257. p2 = p3
  258. p3 = pts[i + 2] ? pts[i + 2] : p2
  259. results.push([
  260. p1[0] + ((p2[0] - p0[0]) / 6) * k,
  261. p1[1] + ((p2[1] - p0[1]) / 6) * k,
  262. p2[0] - ((p3[0] - p1[0]) / 6) * k,
  263. p2[1] - ((p3[1] - p1[1]) / 6) * k,
  264. pts[i][0],
  265. pts[i][1],
  266. ])
  267. }
  268. return results
  269. }
  270. export function getCurvePoints(
  271. pts: number[][],
  272. tension = 0.5,
  273. isClosed = false,
  274. numOfSegments = 3
  275. ) {
  276. const _pts = [...pts],
  277. len = pts.length,
  278. res: number[][] = [] // results
  279. let t1x: number, // tension vectors
  280. t2x: number,
  281. t1y: number,
  282. t2y: number,
  283. c1: number, // cardinal points
  284. c2: number,
  285. c3: number,
  286. c4: number,
  287. st: number,
  288. st2: number,
  289. st3: number
  290. // The algorithm require a previous and next point to the actual point array.
  291. // Check if we will draw closed or open curve.
  292. // If closed, copy end points to beginning and first points to end
  293. // If open, duplicate first points to befinning, end points to end
  294. if (isClosed) {
  295. _pts.unshift(_pts[len - 1])
  296. _pts.push(_pts[0])
  297. } else {
  298. //copy 1. point and insert at beginning
  299. _pts.unshift(_pts[0])
  300. _pts.push(_pts[len - 1])
  301. // _pts.push(_pts[len - 1])
  302. }
  303. // For each point, calculate a segment
  304. for (let i = 1; i < _pts.length - 2; i++) {
  305. // Calculate points along segment and add to results
  306. for (let t = 0; t <= numOfSegments; t++) {
  307. // Step
  308. st = t / numOfSegments
  309. st2 = Math.pow(st, 2)
  310. st3 = Math.pow(st, 3)
  311. // Cardinals
  312. c1 = 2 * st3 - 3 * st2 + 1
  313. c2 = -(2 * st3) + 3 * st2
  314. c3 = st3 - 2 * st2 + st
  315. c4 = st3 - st2
  316. // Tension
  317. t1x = (_pts[i + 1][0] - _pts[i - 1][0]) * tension
  318. t2x = (_pts[i + 2][0] - _pts[i][0]) * tension
  319. t1y = (_pts[i + 1][1] - _pts[i - 1][1]) * tension
  320. t2y = (_pts[i + 2][1] - _pts[i][1]) * tension
  321. // Control points
  322. res.push([
  323. c1 * _pts[i][0] + c2 * _pts[i + 1][0] + c3 * t1x + c4 * t2x,
  324. c1 * _pts[i][1] + c2 * _pts[i + 1][1] + c3 * t1y + c4 * t2y,
  325. ])
  326. }
  327. }
  328. res.push(pts[pts.length - 1])
  329. return res
  330. }
  331. export function angleDelta(a0: number, a1: number) {
  332. return shortAngleDist(a0, a1)
  333. }
  334. /**
  335. * Rotate a point around a center.
  336. * @param x The x-axis coordinate of the point.
  337. * @param y The y-axis coordinate of the point.
  338. * @param cx The x-axis coordinate of the point to rotate round.
  339. * @param cy The y-axis coordinate of the point to rotate round.
  340. * @param angle The distance (in radians) to rotate.
  341. */
  342. export function rotatePoint(A: number[], B: number[], angle: number) {
  343. const s = Math.sin(angle)
  344. const c = Math.cos(angle)
  345. const px = A[0] - B[0]
  346. const py = A[1] - B[1]
  347. const nx = px * c - py * s
  348. const ny = px * s + py * c
  349. return [nx + B[0], ny + B[1]]
  350. }
  351. export function degreesToRadians(d: number) {
  352. return (d * Math.PI) / 180
  353. }
  354. export function radiansToDegrees(r: number) {
  355. return (r * 180) / Math.PI
  356. }
  357. export function getArcLength(C: number[], r: number, A: number[], B: number[]) {
  358. const sweep = getSweep(C, A, B)
  359. return r * (2 * Math.PI) * (sweep / (2 * Math.PI))
  360. }
  361. export function getArcDashOffset(
  362. C: number[],
  363. r: number,
  364. A: number[],
  365. B: number[],
  366. step: number
  367. ) {
  368. const del0 = getSweep(C, A, B)
  369. const len0 = getArcLength(C, r, A, B)
  370. const off0 = del0 < 0 ? len0 : 2 * Math.PI * C[2] - len0
  371. return -off0 / 2 + step
  372. }
  373. export function getEllipseDashOffset(A: number[], step: number) {
  374. const c = 2 * Math.PI * A[2]
  375. return -c / 2 + -step
  376. }
  377. export function getSweep(C: number[], A: number[], B: number[]) {
  378. return angleDelta(vec.angle(C, A), vec.angle(C, B))
  379. }
  380. export function deepCompareArrays<T>(a: T[], b: T[]) {
  381. if (a?.length !== b?.length) return false
  382. return deepCompare(a, b)
  383. }
  384. export function deepCompare<T>(a: T, b: T) {
  385. return a === b || JSON.stringify(a) === JSON.stringify(b)
  386. }
  387. /**
  388. * Get outer tangents of two circles.
  389. * @param x0
  390. * @param y0
  391. * @param r0
  392. * @param x1
  393. * @param y1
  394. * @param r1
  395. * @returns [lx0, ly0, lx1, ly1, rx0, ry0, rx1, ry1]
  396. */
  397. export function getOuterTangents(
  398. C0: number[],
  399. r0: number,
  400. C1: number[],
  401. r1: number
  402. ) {
  403. const a0 = vec.angle(C0, C1)
  404. const d = vec.dist(C0, C1)
  405. // Circles are overlapping, no tangents
  406. if (d < Math.abs(r1 - r0)) return
  407. const a1 = Math.acos((r0 - r1) / d),
  408. t0 = a0 + a1,
  409. t1 = a0 - a1
  410. return [
  411. [C0[0] + r0 * Math.cos(t1), C0[1] + r0 * Math.sin(t1)],
  412. [C1[0] + r1 * Math.cos(t1), C1[1] + r1 * Math.sin(t1)],
  413. [C0[0] + r0 * Math.cos(t0), C0[1] + r0 * Math.sin(t0)],
  414. [C1[0] + r1 * Math.cos(t0), C1[1] + r1 * Math.sin(t0)],
  415. ]
  416. }
  417. export function arrsIntersect<T, K>(
  418. a: T[],
  419. b: K[],
  420. fn?: (item: K) => T
  421. ): boolean
  422. export function arrsIntersect<T>(a: T[], b: T[]): boolean
  423. export function arrsIntersect<T>(
  424. a: T[],
  425. b: unknown[],
  426. fn?: (item: unknown) => T
  427. ) {
  428. return a.some((item) => b.includes(fn ? fn(item) : item))
  429. }
  430. // /**
  431. // * Will mutate an array to remove items.
  432. // * @param arr
  433. // * @param item
  434. // */
  435. // export function pull<T>(arr: T[], ...items: T[]) {
  436. // for (let item of items) {
  437. // arr.splice(arr.indexOf(item), 1)
  438. // }
  439. // return arr
  440. // }
  441. // /**
  442. // * Will mutate an array to remove items, based on a function
  443. // * @param arr
  444. // * @param fn
  445. // * @returns
  446. // */
  447. // export function pullWith<T>(arr: T[], fn: (item: T) => boolean) {
  448. // pull(arr, ...arr.filter((item) => fn(item)))
  449. // return arr
  450. // }
  451. // export function rectContainsRect(
  452. // x0: number,
  453. // y0: number,
  454. // x1: number,
  455. // y1: number,
  456. // box: { x: number; y: number; width: number; height: number }
  457. // ) {
  458. // return !(
  459. // x0 > box.x ||
  460. // x1 < box.x + box.width ||
  461. // y0 > box.y ||
  462. // y1 < box.y + box.height
  463. // )
  464. // }
  465. export function getTouchDisplay() {
  466. return (
  467. "ontouchstart" in window ||
  468. navigator.maxTouchPoints > 0 ||
  469. navigator.msMaxTouchPoints > 0
  470. )
  471. }
  472. const rounds = [1, 10, 100, 1000]
  473. export function round(n: number, p = 2) {
  474. return Math.floor(n * rounds[p]) / rounds[p]
  475. }
  476. /**
  477. * Linear interpolation betwen two numbers.
  478. * @param y1
  479. * @param y2
  480. * @param mu
  481. */
  482. export function lerp(y1: number, y2: number, mu: number) {
  483. mu = clamp(mu, 0, 1)
  484. return y1 * (1 - mu) + y2 * mu
  485. }
  486. /**
  487. * Modulate a value between two ranges.
  488. * @param value
  489. * @param rangeA from [low, high]
  490. * @param rangeB to [low, high]
  491. * @param clamp
  492. */
  493. export function modulate(
  494. value: number,
  495. rangeA: number[],
  496. rangeB: number[],
  497. clamp = false
  498. ) {
  499. const [fromLow, fromHigh] = rangeA
  500. const [v0, v1] = rangeB
  501. const result = v0 + ((value - fromLow) / (fromHigh - fromLow)) * (v1 - v0)
  502. return clamp
  503. ? v0 < v1
  504. ? Math.max(Math.min(result, v1), v0)
  505. : Math.max(Math.min(result, v0), v1)
  506. : result
  507. }
  508. /**
  509. * Clamp a value into a range.
  510. * @param n
  511. * @param min
  512. */
  513. export function clamp(n: number, min: number): number
  514. export function clamp(n: number, min: number, max: number): number
  515. export function clamp(n: number, min: number, max?: number): number {
  516. return Math.max(min, typeof max !== "undefined" ? Math.min(n, max) : n)
  517. }
  518. // CURVES
  519. // Mostly adapted from https://github.com/Pomax/bezierjs
  520. export function computePointOnCurve(t: number, points: number[][]) {
  521. // shortcuts
  522. if (t === 0) {
  523. return points[0]
  524. }
  525. const order = points.length - 1
  526. if (t === 1) {
  527. return points[order]
  528. }
  529. const mt = 1 - t
  530. let p = points // constant?
  531. if (order === 0) {
  532. return points[0]
  533. } // linear?
  534. if (order === 1) {
  535. return [mt * p[0][0] + t * p[1][0], mt * p[0][1] + t * p[1][1]]
  536. } // quadratic/cubic curve?
  537. if (order < 4) {
  538. const mt2 = mt * mt,
  539. t2 = t * t
  540. let a: number,
  541. b: number,
  542. c: number,
  543. d = 0
  544. if (order === 2) {
  545. p = [p[0], p[1], p[2], [0, 0]]
  546. a = mt2
  547. b = mt * t * 2
  548. c = t2
  549. } else if (order === 3) {
  550. a = mt2 * mt
  551. b = mt2 * t * 3
  552. c = mt * t2 * 3
  553. d = t * t2
  554. }
  555. return [
  556. a * p[0][0] + b * p[1][0] + c * p[2][0] + d * p[3][0],
  557. a * p[0][1] + b * p[1][1] + c * p[2][1] + d * p[3][1],
  558. ]
  559. } // higher order curves: use de Casteljau's computation
  560. }
  561. function distance2(p: DOMPoint, point: number[]) {
  562. const dx = p.x - point[0],
  563. dy = p.y - point[1]
  564. return dx * dx + dy * dy
  565. }
  566. /**
  567. * Find the closest point on a path to an off-path point.
  568. * @param pathNode
  569. * @param point
  570. * @returns
  571. */
  572. export function getClosestPointOnPath(
  573. pathNode: SVGPathElement,
  574. point: number[]
  575. ) {
  576. const pathLen = pathNode.getTotalLength()
  577. let p = 8,
  578. best: DOMPoint,
  579. bestLen: number,
  580. bestDist = Infinity,
  581. bl: number,
  582. al: number
  583. // linear scan for coarse approximation
  584. for (
  585. let scan: DOMPoint, scanLen = 0, scanDist: number;
  586. scanLen <= pathLen;
  587. scanLen += p
  588. ) {
  589. if (
  590. (scanDist = distance2(
  591. (scan = pathNode.getPointAtLength(scanLen)),
  592. point
  593. )) < bestDist
  594. ) {
  595. ;(best = scan), (bestLen = scanLen), (bestDist = scanDist)
  596. }
  597. }
  598. // binary search for precise estimate
  599. p /= 2
  600. while (p > 0.5) {
  601. let before: DOMPoint, after: DOMPoint, bd: number, ad: number
  602. if (
  603. (bl = bestLen - p) >= 0 &&
  604. (bd = distance2((before = pathNode.getPointAtLength(bl)), point)) <
  605. bestDist
  606. ) {
  607. ;(best = before), (bestLen = bl), (bestDist = bd)
  608. } else if (
  609. (al = bestLen + p) <= pathLen &&
  610. (ad = distance2((after = pathNode.getPointAtLength(al)), point)) <
  611. bestDist
  612. ) {
  613. ;(best = after), (bestLen = al), (bestDist = ad)
  614. } else {
  615. p /= 2
  616. }
  617. }
  618. return {
  619. point: [best.x, best.y],
  620. distance: bestDist,
  621. length: (bl + al) / 2,
  622. t: (bl + al) / 2 / pathLen,
  623. }
  624. }
  625. export function det(
  626. a: number,
  627. b: number,
  628. c: number,
  629. d: number,
  630. e: number,
  631. f: number,
  632. g: number,
  633. h: number,
  634. i: number
  635. ) {
  636. return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g
  637. }
  638. /**
  639. * Get a circle from three points.
  640. * @param p0
  641. * @param p1
  642. * @param center
  643. * @returns
  644. */
  645. export function circleFromThreePoints(A: number[], B: number[], C: number[]) {
  646. const a = det(A[0], A[1], 1, B[0], B[1], 1, C[0], C[1], 1)
  647. const bx = -det(
  648. A[0] * A[0] + A[1] * A[1],
  649. A[1],
  650. 1,
  651. B[0] * B[0] + B[1] * B[1],
  652. B[1],
  653. 1,
  654. C[0] * C[0] + C[1] * C[1],
  655. C[1],
  656. 1
  657. )
  658. const by = det(
  659. A[0] * A[0] + A[1] * A[1],
  660. A[0],
  661. 1,
  662. B[0] * B[0] + B[1] * B[1],
  663. B[0],
  664. 1,
  665. C[0] * C[0] + C[1] * C[1],
  666. C[0],
  667. 1
  668. )
  669. const c = -det(
  670. A[0] * A[0] + A[1] * A[1],
  671. A[0],
  672. A[1],
  673. B[0] * B[0] + B[1] * B[1],
  674. B[0],
  675. B[1],
  676. C[0] * C[0] + C[1] * C[1],
  677. C[0],
  678. C[1]
  679. )
  680. return [
  681. -bx / (2 * a),
  682. -by / (2 * a),
  683. Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a)),
  684. ]
  685. }
  686. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  687. export function throttle<P extends any[], T extends (...args: P) => any>(
  688. fn: T,
  689. wait: number,
  690. preventDefault?: boolean
  691. ) {
  692. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  693. let inThrottle: boolean, lastFn: any, lastTime: number
  694. return function(...args: P) {
  695. if (preventDefault) args[0].preventDefault()
  696. // eslint-disable-next-line @typescript-eslint/no-this-alias
  697. const context = this
  698. if (!inThrottle) {
  699. fn.apply(context, args)
  700. lastTime = Date.now()
  701. inThrottle = true
  702. } else {
  703. clearTimeout(lastFn)
  704. lastFn = setTimeout(function() {
  705. if (Date.now() - lastTime >= wait) {
  706. fn.apply(context, args)
  707. lastTime = Date.now()
  708. }
  709. }, Math.max(wait - (Date.now() - lastTime), 0))
  710. }
  711. }
  712. }
  713. export function pointInRect(
  714. point: number[],
  715. minX: number,
  716. minY: number,
  717. maxX: number,
  718. maxY: number
  719. ) {
  720. return !(
  721. point[0] < minX ||
  722. point[0] > maxX ||
  723. point[1] < minY ||
  724. point[1] > maxY
  725. )
  726. }
  727. /**
  728. * Get the intersection of two rays, with origin points p0 and p1, and direction vectors n0 and n1.
  729. * @param p0 The origin point of the first ray
  730. * @param n0 The direction vector of the first ray
  731. * @param p1 The origin point of the second ray
  732. * @param n1 The direction vector of the second ray
  733. * @returns
  734. */
  735. export function getRayRayIntersection(
  736. p0: number[],
  737. n0: number[],
  738. p1: number[],
  739. n1: number[]
  740. ) {
  741. const p0e = vec.add(p0, n0),
  742. p1e = vec.add(p1, n1),
  743. m0 = (p0e[1] - p0[1]) / (p0e[0] - p0[0]),
  744. m1 = (p1e[1] - p1[1]) / (p1e[0] - p1[0]),
  745. b0 = p0[1] - m0 * p0[0],
  746. b1 = p1[1] - m1 * p1[0],
  747. x = (b1 - b0) / (m0 - m1),
  748. y = m0 * x + b0
  749. return [x, y]
  750. }
  751. export async function postJsonToEndpoint(
  752. endpoint: string,
  753. data: { [key: string]: unknown }
  754. ) {
  755. const d = await fetch(
  756. `${process.env.NEXT_PUBLIC_BASE_API_URL}/api/${endpoint}`,
  757. {
  758. method: "POST",
  759. headers: { "Content-Type": "application/json" },
  760. body: JSON.stringify(data),
  761. }
  762. )
  763. return await d.json()
  764. }
  765. export function getPointerEventInfo(
  766. e: PointerEvent | React.PointerEvent | WheelEvent
  767. ) {
  768. const { shiftKey, ctrlKey, metaKey, altKey } = e
  769. return { point: [e.clientX, e.clientY], shiftKey, ctrlKey, metaKey, altKey }
  770. }
  771. export function getKeyboardEventInfo(e: KeyboardEvent | React.KeyboardEvent) {
  772. const { shiftKey, ctrlKey, metaKey, altKey } = e
  773. return { key: e.key, shiftKey, ctrlKey, metaKey, altKey }
  774. }