Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

utils.ts 20KB

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