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 22KB

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