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

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804
  1. import React from 'react'
  2. import { Bounds, Edge, Corner, BezierCurveSegment } from 'types'
  3. import { v4 as uuid } from 'uuid'
  4. import vec from './vec'
  5. import _isMobile from 'ismobilejs'
  6. import { intersectPolygonBounds } from './intersections'
  7. /* -------------------------------------------------- */
  8. /* Math & Geometry */
  9. /* -------------------------------------------------- */
  10. /**
  11. * Linear interpolation betwen two numbers.
  12. * @param y1
  13. * @param y2
  14. * @param mu
  15. */
  16. export function lerp(y1: number, y2: number, mu: number): number {
  17. mu = clamp(mu, 0, 1)
  18. return y1 * (1 - mu) + y2 * mu
  19. }
  20. /**
  21. * Modulate a value between two ranges.
  22. * @param value
  23. * @param rangeA from [low, high]
  24. * @param rangeB to [low, high]
  25. * @param clamp
  26. */
  27. export function modulate(
  28. value: number,
  29. rangeA: number[],
  30. rangeB: number[],
  31. clamp = false
  32. ): number {
  33. const [fromLow, fromHigh] = rangeA
  34. const [v0, v1] = rangeB
  35. const result = v0 + ((value - fromLow) / (fromHigh - fromLow)) * (v1 - v0)
  36. return clamp
  37. ? v0 < v1
  38. ? Math.max(Math.min(result, v1), v0)
  39. : Math.max(Math.min(result, v0), v1)
  40. : result
  41. }
  42. /**
  43. * Clamp a value into a range.
  44. * @param n
  45. * @param min
  46. */
  47. export function clamp(n: number, min: number): number
  48. export function clamp(n: number, min: number, max: number): number
  49. export function clamp(n: number, min: number, max?: number): number {
  50. return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n)
  51. }
  52. // TODO: replace with a string compression algorithm
  53. export function compress(s: string): string {
  54. return s
  55. }
  56. // TODO: replace with a string decompression algorithm
  57. export function decompress(s: string): string {
  58. return s
  59. }
  60. /**
  61. * Recursively clone an object or array.
  62. * @param obj
  63. */
  64. export function deepClone<T>(obj: T): T {
  65. if (obj === null) return null
  66. const clone: any = { ...obj }
  67. Object.keys(obj).forEach(
  68. (key) =>
  69. (clone[key] =
  70. typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key])
  71. )
  72. if (Array.isArray(obj)) {
  73. clone.length = obj.length
  74. return Array.from(clone) as any as T
  75. }
  76. return clone as T
  77. }
  78. /**
  79. * Seeded random number generator, using [xorshift](https://en.wikipedia.org/wiki/Xorshift).
  80. * The result will always be betweeen -1 and 1.
  81. *
  82. * Adapted from [seedrandom](https://github.com/davidbau/seedrandom).
  83. */
  84. export function rng(seed = ''): () => number {
  85. let x = 0
  86. let y = 0
  87. let z = 0
  88. let w = 0
  89. function next() {
  90. const t = x ^ (x << 11)
  91. ;(x = y), (y = z), (z = w)
  92. w ^= ((w >>> 19) ^ t ^ (t >>> 8)) >>> 0
  93. return w / 0x100000000
  94. }
  95. for (let k = 0; k < seed.length + 64; k++) {
  96. ;(x ^= seed.charCodeAt(k) | 0), next()
  97. }
  98. return next
  99. }
  100. /* --------------- Circles and Angles --------------- */
  101. /**
  102. * Get the outer of between a circle and a point.
  103. * @param C The circle's center.
  104. * @param r The circle's radius.
  105. * @param P The point.
  106. * @param side
  107. */
  108. export function getCircleTangentToPoint(
  109. C: number[],
  110. r: number,
  111. P: number[],
  112. side: number
  113. ): number[] {
  114. const B = vec.lrp(C, P, 0.5),
  115. r1 = vec.dist(C, B),
  116. delta = vec.sub(B, C),
  117. d = vec.len(delta)
  118. if (!(d <= r + r1 && d >= Math.abs(r - r1))) {
  119. return
  120. }
  121. const a = (r * r - r1 * r1 + d * d) / (2.0 * d),
  122. n = 1 / d,
  123. p = vec.add(C, vec.mul(delta, a * n)),
  124. h = Math.sqrt(r * r - a * a),
  125. k = vec.mul(vec.per(delta), h * n)
  126. return side === 0 ? vec.add(p, k) : vec.sub(p, k)
  127. }
  128. /**
  129. * Get outer tangents of two circles.
  130. * @param x0
  131. * @param y0
  132. * @param r0
  133. * @param x1
  134. * @param y1
  135. * @param r1
  136. * @returns [lx0, ly0, lx1, ly1, rx0, ry0, rx1, ry1]
  137. */
  138. export function getOuterTangentsOfCircles(
  139. C0: number[],
  140. r0: number,
  141. C1: number[],
  142. r1: number
  143. ): number[][] {
  144. const a0 = vec.angle(C0, C1)
  145. const d = vec.dist(C0, C1)
  146. // Circles are overlapping, no tangents
  147. if (d < Math.abs(r1 - r0)) return
  148. const a1 = Math.acos((r0 - r1) / d),
  149. t0 = a0 + a1,
  150. t1 = a0 - a1
  151. return [
  152. [C0[0] + r0 * Math.cos(t1), C0[1] + r0 * Math.sin(t1)],
  153. [C1[0] + r1 * Math.cos(t1), C1[1] + r1 * Math.sin(t1)],
  154. [C0[0] + r0 * Math.cos(t0), C0[1] + r0 * Math.sin(t0)],
  155. [C1[0] + r1 * Math.cos(t0), C1[1] + r1 * Math.sin(t0)],
  156. ]
  157. }
  158. /**
  159. * Get the closest point on the perimeter of a circle to a given point.
  160. * @param C The circle's center.
  161. * @param r The circle's radius.
  162. * @param P The point.
  163. */
  164. export function getClosestPointOnCircle(
  165. C: number[],
  166. r: number,
  167. P: number[]
  168. ): number[] {
  169. const v = vec.sub(C, P)
  170. return vec.sub(C, vec.mul(vec.div(v, vec.len(v)), r))
  171. }
  172. function det(
  173. a: number,
  174. b: number,
  175. c: number,
  176. d: number,
  177. e: number,
  178. f: number,
  179. g: number,
  180. h: number,
  181. i: number
  182. ): number {
  183. return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g
  184. }
  185. /**
  186. * Get a circle from three points.
  187. * @param A
  188. * @param B
  189. * @param C
  190. * @returns [x, y, r]
  191. */
  192. export function circleFromThreePoints(
  193. A: number[],
  194. B: number[],
  195. C: number[]
  196. ): number[] {
  197. const a = det(A[0], A[1], 1, B[0], B[1], 1, C[0], C[1], 1)
  198. const bx = -det(
  199. A[0] * A[0] + A[1] * A[1],
  200. A[1],
  201. 1,
  202. B[0] * B[0] + B[1] * B[1],
  203. B[1],
  204. 1,
  205. C[0] * C[0] + C[1] * C[1],
  206. C[1],
  207. 1
  208. )
  209. const by = det(
  210. A[0] * A[0] + A[1] * A[1],
  211. A[0],
  212. 1,
  213. B[0] * B[0] + B[1] * B[1],
  214. B[0],
  215. 1,
  216. C[0] * C[0] + C[1] * C[1],
  217. C[0],
  218. 1
  219. )
  220. const c = -det(
  221. A[0] * A[0] + A[1] * A[1],
  222. A[0],
  223. A[1],
  224. B[0] * B[0] + B[1] * B[1],
  225. B[0],
  226. B[1],
  227. C[0] * C[0] + C[1] * C[1],
  228. C[0],
  229. C[1]
  230. )
  231. const x = -bx / (2 * a)
  232. const y = -by / (2 * a)
  233. const r = Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a))
  234. return [x, y, r]
  235. }
  236. /**
  237. * Find the approximate perimeter of an ellipse.
  238. * @param rx
  239. * @param ry
  240. */
  241. export function perimeterOfEllipse(rx: number, ry: number): number {
  242. const h = Math.pow(rx - ry, 2) / Math.pow(rx + ry, 2)
  243. const p = Math.PI * (rx + ry) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h)))
  244. return p
  245. }
  246. /**
  247. * Get the short angle distance between two angles.
  248. * @param a0
  249. * @param a1
  250. */
  251. export function shortAngleDist(a0: number, a1: number): number {
  252. const max = Math.PI * 2
  253. const da = (a1 - a0) % max
  254. return ((2 * da) % max) - da
  255. }
  256. /**
  257. * Get the long angle distance between two angles.
  258. * @param a0
  259. * @param a1
  260. */
  261. export function longAngleDist(a0: number, a1: number): number {
  262. return Math.PI * 2 - shortAngleDist(a0, a1)
  263. }
  264. /**
  265. * Interpolate an angle between two angles.
  266. * @param a0
  267. * @param a1
  268. * @param t
  269. */
  270. export function lerpAngles(a0: number, a1: number, t: number): number {
  271. return a0 + shortAngleDist(a0, a1) * t
  272. }
  273. /**
  274. * Get the short distance between two angles.
  275. * @param a0
  276. * @param a1
  277. */
  278. export function angleDelta(a0: number, a1: number): number {
  279. return shortAngleDist(a0, a1)
  280. }
  281. /**
  282. * Get the "sweep" or short distance between two points on a circle's perimeter.
  283. * @param C
  284. * @param A
  285. * @param B
  286. */
  287. export function getSweep(C: number[], A: number[], B: number[]): number {
  288. return angleDelta(vec.angle(C, A), vec.angle(C, B))
  289. }
  290. /**
  291. * Rotate a point around a center.
  292. * @param x The x-axis coordinate of the point.
  293. * @param y The y-axis coordinate of the point.
  294. * @param cx The x-axis coordinate of the point to rotate round.
  295. * @param cy The y-axis coordinate of the point to rotate round.
  296. * @param angle The distance (in radians) to rotate.
  297. */
  298. export function rotatePoint(A: number[], B: number[], angle: number): number[] {
  299. const s = Math.sin(angle)
  300. const c = Math.cos(angle)
  301. const px = A[0] - B[0]
  302. const py = A[1] - B[1]
  303. const nx = px * c - py * s
  304. const ny = px * s + py * c
  305. return [nx + B[0], ny + B[1]]
  306. }
  307. /**
  308. * Clamp radians within 0 and 2PI
  309. * @param r
  310. */
  311. export function clampRadians(r: number): number {
  312. return (Math.PI * 2 + r) % (Math.PI * 2)
  313. }
  314. /**
  315. * Clamp rotation to even segments.
  316. * @param r
  317. * @param segments
  318. */
  319. export function clampToRotationToSegments(r: number, segments: number): number {
  320. const seg = (Math.PI * 2) / segments
  321. return Math.floor((clampRadians(r) + seg / 2) / seg) * seg
  322. }
  323. /**
  324. * Is angle c between angles a and b?
  325. * @param a
  326. * @param b
  327. * @param c
  328. */
  329. export function isAngleBetween(a: number, b: number, c: number): boolean {
  330. if (c === a || c === b) return true
  331. const PI2 = Math.PI * 2
  332. const AB = (b - a + PI2) % PI2
  333. const AC = (c - a + PI2) % PI2
  334. return AB <= Math.PI !== AC > AB
  335. }
  336. /**
  337. * Convert degrees to radians.
  338. * @param d
  339. */
  340. export function degreesToRadians(d: number): number {
  341. return (d * Math.PI) / 180
  342. }
  343. /**
  344. * Convert radians to degrees.
  345. * @param r
  346. */
  347. export function radiansToDegrees(r: number): number {
  348. return (r * 180) / Math.PI
  349. }
  350. /**
  351. * Get the length of an arc between two points on a circle's perimeter.
  352. * @param C
  353. * @param r
  354. * @param A
  355. * @param B
  356. */
  357. export function getArcLength(
  358. C: number[],
  359. r: number,
  360. A: number[],
  361. B: number[]
  362. ): number {
  363. const sweep = getSweep(C, A, B)
  364. return r * (2 * Math.PI) * (sweep / (2 * Math.PI))
  365. }
  366. /**
  367. * Get balanced dash-strokearray and dash-strokeoffset properties for a path of a given length.
  368. * @param length The length of the path.
  369. * @param strokeWidth The shape's stroke-width property.
  370. * @param style The stroke's style: "dashed" or "dotted" (default "dashed").
  371. * @param snap An interval for dashes (e.g. 4 will produce arrays with 4, 8, 16, etc dashes).
  372. */
  373. export function getPerfectDashProps(
  374. length: number,
  375. strokeWidth: number,
  376. style: 'dashed' | 'dotted' = 'dashed',
  377. snap = 1
  378. ): {
  379. strokeDasharray: string
  380. strokeDashoffset: string
  381. } {
  382. let dashLength: number
  383. let strokeDashoffset: string
  384. let ratio: number
  385. if (style === 'dashed') {
  386. dashLength = strokeWidth * 2
  387. ratio = 1
  388. strokeDashoffset = (dashLength / 2).toString()
  389. } else {
  390. dashLength = strokeWidth / 100
  391. ratio = 100
  392. strokeDashoffset = '0'
  393. }
  394. let dashes = Math.floor(length / dashLength / (2 * ratio))
  395. dashes -= dashes % snap
  396. if (dashes === 0) dashes = 1
  397. const gapLength = (length - dashes * dashLength) / dashes
  398. return {
  399. strokeDasharray: [dashLength, gapLength].join(' '),
  400. strokeDashoffset,
  401. }
  402. }
  403. /**
  404. * Get a dash offset for an arc, based on its length.
  405. * @param C
  406. * @param r
  407. * @param A
  408. * @param B
  409. * @param step
  410. */
  411. export function getArcDashOffset(
  412. C: number[],
  413. r: number,
  414. A: number[],
  415. B: number[],
  416. step: number
  417. ): number {
  418. const del0 = getSweep(C, A, B)
  419. const len0 = getArcLength(C, r, A, B)
  420. const off0 = del0 < 0 ? len0 : 2 * Math.PI * C[2] - len0
  421. return -off0 / 2 + step
  422. }
  423. /**
  424. * Get a dash offset for an ellipse, based on its length.
  425. * @param A
  426. * @param step
  427. */
  428. export function getEllipseDashOffset(A: number[], step: number): number {
  429. const c = 2 * Math.PI * A[2]
  430. return -c / 2 + -step
  431. }
  432. /* --------------- Curves and Splines --------------- */
  433. /**
  434. * Get bezier curve segments that pass through an array of points.
  435. * @param points
  436. * @param tension
  437. */
  438. export function getBezierCurveSegments(
  439. points: number[][],
  440. tension = 0.4
  441. ): BezierCurveSegment[] {
  442. const len = points.length,
  443. cpoints: number[][] = [...points]
  444. if (len < 2) {
  445. throw Error('Curve must have at least two points.')
  446. }
  447. for (let i = 1; i < len - 1; i++) {
  448. const p0 = points[i - 1],
  449. p1 = points[i],
  450. p2 = points[i + 1]
  451. const pdx = p2[0] - p0[0],
  452. pdy = p2[1] - p0[1],
  453. pd = Math.hypot(pdx, pdy),
  454. nx = pdx / pd, // normalized x
  455. ny = pdy / pd, // normalized y
  456. dp = Math.hypot(p1[0] - p0[0], p1[1] - p0[1]), // Distance to previous
  457. dn = Math.hypot(p1[0] - p2[0], p1[1] - p2[1]) // Distance to next
  458. cpoints[i] = [
  459. // tangent start
  460. p1[0] - nx * dp * tension,
  461. p1[1] - ny * dp * tension,
  462. // tangent end
  463. p1[0] + nx * dn * tension,
  464. p1[1] + ny * dn * tension,
  465. // normal
  466. nx,
  467. ny,
  468. ]
  469. }
  470. // TODO: Reflect the nearest control points, not average them
  471. const d0 = Math.hypot(points[0][0] + cpoints[1][0])
  472. cpoints[0][2] = (points[0][0] + cpoints[1][0]) / 2
  473. cpoints[0][3] = (points[0][1] + cpoints[1][1]) / 2
  474. cpoints[0][4] = (cpoints[1][0] - points[0][0]) / d0
  475. cpoints[0][5] = (cpoints[1][1] - points[0][1]) / d0
  476. const d1 = Math.hypot(points[len - 1][1] + cpoints[len - 1][1])
  477. cpoints[len - 1][0] = (points[len - 1][0] + cpoints[len - 2][2]) / 2
  478. cpoints[len - 1][1] = (points[len - 1][1] + cpoints[len - 2][3]) / 2
  479. cpoints[len - 1][4] = (cpoints[len - 2][2] - points[len - 1][0]) / -d1
  480. cpoints[len - 1][5] = (cpoints[len - 2][3] - points[len - 1][1]) / -d1
  481. const results: BezierCurveSegment[] = []
  482. for (let i = 1; i < cpoints.length; i++) {
  483. results.push({
  484. start: points[i - 1].slice(0, 2),
  485. tangentStart: cpoints[i - 1].slice(2, 4),
  486. normalStart: cpoints[i - 1].slice(4, 6),
  487. pressureStart: 2 + ((i - 1) % 2 === 0 ? 1.5 : 0),
  488. end: points[i].slice(0, 2),
  489. tangentEnd: cpoints[i].slice(0, 2),
  490. normalEnd: cpoints[i].slice(4, 6),
  491. pressureEnd: 2 + (i % 2 === 0 ? 1.5 : 0),
  492. })
  493. }
  494. return results
  495. }
  496. /**
  497. * Find a point along a curve segment, via pomax.
  498. * @param t
  499. * @param points [cpx1, cpy1, cpx2, cpy2, px, py][]
  500. */
  501. export function computePointOnCurve(t: number, points: number[][]): number[] {
  502. // shortcuts
  503. if (t === 0) {
  504. return points[0]
  505. }
  506. const order = points.length - 1
  507. if (t === 1) {
  508. return points[order]
  509. }
  510. const mt = 1 - t
  511. let p = points // constant?
  512. if (order === 0) {
  513. return points[0]
  514. } // linear?
  515. if (order === 1) {
  516. return [mt * p[0][0] + t * p[1][0], mt * p[0][1] + t * p[1][1]]
  517. } // quadratic/cubic curve?
  518. if (order < 4) {
  519. const mt2 = mt * mt,
  520. t2 = t * t
  521. let a: number,
  522. b: number,
  523. c: number,
  524. d = 0
  525. if (order === 2) {
  526. p = [p[0], p[1], p[2], [0, 0]]
  527. a = mt2
  528. b = mt * t * 2
  529. c = t2
  530. } else if (order === 3) {
  531. a = mt2 * mt
  532. b = mt2 * t * 3
  533. c = mt * t2 * 3
  534. d = t * t2
  535. }
  536. return [
  537. a * p[0][0] + b * p[1][0] + c * p[2][0] + d * p[3][0],
  538. a * p[0][1] + b * p[1][1] + c * p[2][1] + d * p[3][1],
  539. ]
  540. } // higher order curves: use de Casteljau's computation
  541. }
  542. /**
  543. * Evaluate a 2d cubic bezier at a point t on the x axis.
  544. * @param tx
  545. * @param x1
  546. * @param y1
  547. * @param x2
  548. * @param y2
  549. */
  550. export function cubicBezier(
  551. tx: number,
  552. x1: number,
  553. y1: number,
  554. x2: number,
  555. y2: number
  556. ): number {
  557. // Inspired by Don Lancaster's two articles
  558. // http://www.tinaja.com/glib/cubemath.pdf
  559. // http://www.tinaja.com/text/bezmath.html
  560. // Set start and end point
  561. const x0 = 0,
  562. y0 = 0,
  563. x3 = 1,
  564. y3 = 1,
  565. // Convert the coordinates to equation space
  566. A = x3 - 3 * x2 + 3 * x1 - x0,
  567. B = 3 * x2 - 6 * x1 + 3 * x0,
  568. C = 3 * x1 - 3 * x0,
  569. D = x0,
  570. E = y3 - 3 * y2 + 3 * y1 - y0,
  571. F = 3 * y2 - 6 * y1 + 3 * y0,
  572. G = 3 * y1 - 3 * y0,
  573. H = y0,
  574. // Variables for the loop below
  575. iterations = 5
  576. let i: number,
  577. slope: number,
  578. x: number,
  579. t = tx
  580. // Loop through a few times to get a more accurate time value, according to the Newton-Raphson method
  581. // http://en.wikipedia.org/wiki/Newton's_method
  582. for (i = 0; i < iterations; i++) {
  583. // The curve's x equation for the current time value
  584. x = A * t * t * t + B * t * t + C * t + D
  585. // The slope we want is the inverse of the derivate of x
  586. slope = 1 / (3 * A * t * t + 2 * B * t + C)
  587. // Get the next estimated time value, which will be more accurate than the one before
  588. t -= (x - tx) * slope
  589. t = t > 1 ? 1 : t < 0 ? 0 : t
  590. }
  591. // Find the y value through the curve's y equation, with the now more accurate time value
  592. return Math.abs(E * t * t * t + F * t * t + G * t * H)
  593. }
  594. /**
  595. * Get a bezier curve data for a spline that fits an array of points.
  596. * @param points An array of points formatted as [x, y]
  597. * @param k Tension
  598. */
  599. export function getSpline(
  600. pts: number[][],
  601. k = 0.5
  602. ): {
  603. cp1x: number
  604. cp1y: number
  605. cp2x: number
  606. cp2y: number
  607. px: number
  608. py: number
  609. }[] {
  610. let p0: number[]
  611. let [p1, p2, p3] = pts
  612. const results: {
  613. cp1x: number
  614. cp1y: number
  615. cp2x: number
  616. cp2y: number
  617. px: number
  618. py: number
  619. }[] = []
  620. for (let i = 1, len = pts.length; i < len; i++) {
  621. p0 = p1
  622. p1 = p2
  623. p2 = p3
  624. p3 = pts[i + 2] ? pts[i + 2] : p2
  625. results.push({
  626. cp1x: p1[0] + ((p2[0] - p0[0]) / 6) * k,
  627. cp1y: p1[1] + ((p2[1] - p0[1]) / 6) * k,
  628. cp2x: p2[0] - ((p3[0] - p1[0]) / 6) * k,
  629. cp2y: p2[1] - ((p3[1] - p1[1]) / 6) * k,
  630. px: pts[i][0],
  631. py: pts[i][1],
  632. })
  633. }
  634. return results
  635. }
  636. /**
  637. * Get a bezier curve data for a spline that fits an array of points.
  638. * @param pts
  639. * @param tension
  640. * @param isClosed
  641. * @param numOfSegments
  642. */
  643. export function getCurvePoints(
  644. pts: number[][],
  645. tension = 0.5,
  646. isClosed = false,
  647. numOfSegments = 3
  648. ): number[][] {
  649. const _pts = [...pts],
  650. len = pts.length,
  651. res: number[][] = [] // results
  652. let t1x: number, // tension vectors
  653. t2x: number,
  654. t1y: number,
  655. t2y: number,
  656. c1: number, // cardinal points
  657. c2: number,
  658. c3: number,
  659. c4: number,
  660. st: number,
  661. st2: number,
  662. st3: number
  663. // The algorithm require a previous and next point to the actual point array.
  664. // Check if we will draw closed or open curve.
  665. // If closed, copy end points to beginning and first points to end
  666. // If open, duplicate first points to befinning, end points to end
  667. if (isClosed) {
  668. _pts.unshift(_pts[len - 1])
  669. _pts.push(_pts[0])
  670. } else {
  671. //copy 1. point and insert at beginning
  672. _pts.unshift(_pts[0])
  673. _pts.push(_pts[len - 1])
  674. // _pts.push(_pts[len - 1])
  675. }
  676. // For each point, calculate a segment
  677. for (let i = 1; i < _pts.length - 2; i++) {
  678. // Calculate points along segment and add to results
  679. for (let t = 0; t <= numOfSegments; t++) {
  680. // Step
  681. st = t / numOfSegments
  682. st2 = Math.pow(st, 2)
  683. st3 = Math.pow(st, 3)
  684. // Cardinals
  685. c1 = 2 * st3 - 3 * st2 + 1
  686. c2 = -(2 * st3) + 3 * st2
  687. c3 = st3 - 2 * st2 + st
  688. c4 = st3 - st2
  689. // Tension
  690. t1x = (_pts[i + 1][0] - _pts[i - 1][0]) * tension
  691. t2x = (_pts[i + 2][0] - _pts[i][0]) * tension
  692. t1y = (_pts[i + 1][1] - _pts[i - 1][1]) * tension
  693. t2y = (_pts[i + 2][1] - _pts[i][1]) * tension
  694. // Control points
  695. res.push([
  696. c1 * _pts[i][0] + c2 * _pts[i + 1][0] + c3 * t1x + c4 * t2x,
  697. c1 * _pts[i][1] + c2 * _pts[i + 1][1] + c3 * t1y + c4 * t2y,
  698. ])
  699. }
  700. }
  701. res.push(pts[pts.length - 1])
  702. return res
  703. }
  704. /**
  705. * Simplify a line (using Ramer-Douglas-Peucker algorithm).
  706. * @param points An array of points as [x, y, ...][]
  707. * @param tolerance The minimum line distance (also called epsilon).
  708. * @returns Simplified array as [x, y, ...][]
  709. */
  710. export function simplify(points: number[][], tolerance = 1): number[][] {
  711. const len = points.length,
  712. a = points[0],
  713. b = points[len - 1],
  714. [x1, y1] = a,
  715. [x2, y2] = b
  716. if (len > 2) {
  717. let distance = 0
  718. let index = 0
  719. const max = Math.hypot(y2 - y1, x2 - x1)
  720. for (let i = 1; i < len - 1; i++) {
  721. const [x0, y0] = points[i],
  722. d = Math.abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1) / max
  723. if (distance > d) continue
  724. distance = d
  725. index = i
  726. }
  727. if (distance > tolerance) {
  728. const l0 = simplify(points.slice(0, index + 1), tolerance)
  729. const l1 = simplify(points.slice(index + 1), tolerance)
  730. return l0.concat(l1.slice(1))
  731. }
  732. }
  733. return [a, b]
  734. }
  735. /**
  736. * Get whether a point is inside of a circle.
  737. * @param A
  738. * @param b
  739. * @returns
  740. */
  741. export function pointInCircle(A: number[], C: number[], r: number): boolean {
  742. return vec.dist(A, C) <= r
  743. }
  744. /**
  745. * Get whether a point is inside of an ellipse.
  746. * @param point
  747. * @param center
  748. * @param rx
  749. * @param ry
  750. * @param rotation
  751. * @returns
  752. */
  753. export function pointInEllipse(
  754. A: number[],
  755. C: number[],
  756. rx: number,
  757. ry: number,
  758. rotation = 0
  759. ): boolean {
  760. rotation = rotation || 0
  761. const cos = Math.cos(rotation)
  762. const sin = Math.sin(rotation)
  763. const delta = vec.sub(A, C)
  764. const tdx = cos * delta[0] + sin * delta[1]
  765. const tdy = sin * delta[0] - cos * delta[1]
  766. return (tdx * tdx) / (rx * rx) + (tdy * tdy) / (ry * ry) <= 1
  767. }
  768. /**
  769. * Get whether a point is inside of a rectangle.
  770. * @param A
  771. * @param point
  772. * @param size
  773. */
  774. export function pointInRect(
  775. A: number[],
  776. point: number[],
  777. size: number[]
  778. ): boolean {
  779. return !(
  780. point[0] < point[0] ||
  781. point[0] > point[0] + size[0] ||
  782. point[1] < point[1] ||
  783. point[1] > point[1] + size[1]
  784. )
  785. }
  786. /* --------------------- Bounds --------------------- */
  787. /**
  788. * Get whether a point is inside of a bounds.
  789. * @param A
  790. * @param b
  791. * @returns
  792. */
  793. export function pointInBounds(A: number[], b: Bounds): boolean {
  794. return !(A[0] < b.minX || A[0] > b.maxX || A[1] < b.minY || A[1] > b.maxY)
  795. }
  796. /**
  797. * Get whether two bounds collide.
  798. * @param a Bounds
  799. * @param b Bounds
  800. * @returns
  801. */
  802. export function boundsCollide(a: Bounds, b: Bounds): boolean {
  803. return !(
  804. a.maxX < b.minX ||
  805. a.minX > b.maxX ||
  806. a.maxY < b.minY ||
  807. a.minY > b.maxY
  808. )
  809. }
  810. /**
  811. * Get whether the bounds of A contain the bounds of B. A perfect match will return true.
  812. * @param a Bounds
  813. * @param b Bounds
  814. * @returns
  815. */
  816. export function boundsContain(a: Bounds, b: Bounds): boolean {
  817. return (
  818. a.minX < b.minX && a.minY < b.minY && a.maxY > b.maxY && a.maxX > b.maxX
  819. )
  820. }
  821. /**
  822. * Get whether the bounds of A are contained by the bounds of B.
  823. * @param a Bounds
  824. * @param b Bounds
  825. * @returns
  826. */
  827. export function boundsContained(a: Bounds, b: Bounds): boolean {
  828. return boundsContain(b, a)
  829. }
  830. /**
  831. * Get whether a set of points are all contained by a bounding box.
  832. * @returns
  833. */
  834. export function boundsContainPolygon(a: Bounds, points: number[][]): boolean {
  835. return points.every((point) => pointInBounds(point, a))
  836. }
  837. /**
  838. * Get whether a polygon collides a bounding box.
  839. * @param points
  840. * @param b
  841. */
  842. export function boundsCollidePolygon(a: Bounds, points: number[][]): boolean {
  843. return intersectPolygonBounds(points, a).length > 0
  844. }
  845. /**
  846. * Get whether two bounds are identical.
  847. * @param a Bounds
  848. * @param b Bounds
  849. * @returns
  850. */
  851. export function boundsAreEqual(a: Bounds, b: Bounds): boolean {
  852. return !(
  853. b.maxX !== a.maxX ||
  854. b.minX !== a.minX ||
  855. b.maxY !== a.maxY ||
  856. b.minY !== a.minY
  857. )
  858. }
  859. /**
  860. * Find a bounding box from an array of points.
  861. * @param points
  862. * @param rotation (optional) The bounding box's rotation.
  863. */
  864. export function getBoundsFromPoints(points: number[][], rotation = 0): Bounds {
  865. let minX = Infinity
  866. let minY = Infinity
  867. let maxX = -Infinity
  868. let maxY = -Infinity
  869. if (points.length < 2) {
  870. minX = 0
  871. minY = 0
  872. maxX = 1
  873. maxY = 1
  874. } else {
  875. for (const [x, y] of points) {
  876. minX = Math.min(x, minX)
  877. minY = Math.min(y, minY)
  878. maxX = Math.max(x, maxX)
  879. maxY = Math.max(y, maxY)
  880. }
  881. }
  882. if (rotation !== 0) {
  883. return getBoundsFromPoints(
  884. points.map((pt) =>
  885. vec.rotWith(pt, [(minX + maxX) / 2, (minY + maxY) / 2], rotation)
  886. )
  887. )
  888. }
  889. return {
  890. minX,
  891. minY,
  892. maxX,
  893. maxY,
  894. width: Math.max(1, maxX - minX),
  895. height: Math.max(1, maxY - minY),
  896. }
  897. }
  898. /**
  899. * Move a bounding box without recalculating it.
  900. * @param bounds
  901. * @param delta
  902. * @returns
  903. */
  904. export function translateBounds(bounds: Bounds, delta: number[]): Bounds {
  905. return {
  906. minX: bounds.minX + delta[0],
  907. minY: bounds.minY + delta[1],
  908. maxX: bounds.maxX + delta[0],
  909. maxY: bounds.maxY + delta[1],
  910. width: bounds.width,
  911. height: bounds.height,
  912. }
  913. }
  914. /**
  915. * Rotate a bounding box.
  916. * @param bounds
  917. * @param center
  918. * @param rotation
  919. */
  920. export function rotateBounds(
  921. bounds: Bounds,
  922. center: number[],
  923. rotation: number
  924. ): Bounds {
  925. const [minX, minY] = vec.rotWith([bounds.minX, bounds.minY], center, rotation)
  926. const [maxX, maxY] = vec.rotWith([bounds.maxX, bounds.maxY], center, rotation)
  927. return {
  928. minX,
  929. minY,
  930. maxX,
  931. maxY,
  932. width: bounds.width,
  933. height: bounds.height,
  934. }
  935. }
  936. /**
  937. * Get the rotated bounds of an ellipse.
  938. * @param x
  939. * @param y
  940. * @param rx
  941. * @param ry
  942. * @param rotation
  943. */
  944. export function getRotatedEllipseBounds(
  945. x: number,
  946. y: number,
  947. rx: number,
  948. ry: number,
  949. rotation: number
  950. ): Bounds {
  951. const c = Math.cos(rotation)
  952. const s = Math.sin(rotation)
  953. const w = Math.hypot(rx * c, ry * s)
  954. const h = Math.hypot(rx * s, ry * c)
  955. return {
  956. minX: x + rx - w,
  957. minY: y + ry - h,
  958. maxX: x + rx + w,
  959. maxY: y + ry + h,
  960. width: w * 2,
  961. height: h * 2,
  962. }
  963. }
  964. /**
  965. * Get a bounding box that includes two bounding boxes.
  966. * @param a Bounding box
  967. * @param b Bounding box
  968. * @returns
  969. */
  970. export function getExpandedBounds(a: Bounds, b: Bounds): Bounds {
  971. const minX = Math.min(a.minX, b.minX),
  972. minY = Math.min(a.minY, b.minY),
  973. maxX = Math.max(a.maxX, b.maxX),
  974. maxY = Math.max(a.maxY, b.maxY),
  975. width = Math.abs(maxX - minX),
  976. height = Math.abs(maxY - minY)
  977. return { minX, minY, maxX, maxY, width, height }
  978. }
  979. /**
  980. * Get the common bounds of a group of bounds.
  981. * @returns
  982. */
  983. export function getCommonBounds(...b: Bounds[]): Bounds {
  984. if (b.length < 2) return b[0]
  985. let bounds = b[0]
  986. for (let i = 1; i < b.length; i++) {
  987. bounds = getExpandedBounds(bounds, b[i])
  988. }
  989. return bounds
  990. }
  991. export function getRotatedCorners(b: Bounds, rotation: number): number[][] {
  992. const center = [b.minX + b.width / 2, b.minY + b.height / 2]
  993. return [
  994. [b.minX, b.minY],
  995. [b.maxX, b.minY],
  996. [b.maxX, b.maxY],
  997. [b.minX, b.maxY],
  998. ].map((point) => vec.rotWith(point, center, rotation))
  999. }
  1000. export function getTransformedBoundingBox(
  1001. bounds: Bounds,
  1002. handle: Corner | Edge | 'center',
  1003. delta: number[],
  1004. rotation = 0,
  1005. isAspectRatioLocked = false
  1006. ): Bounds & { scaleX: number; scaleY: number } {
  1007. // Create top left and bottom right corners.
  1008. const [ax0, ay0] = [bounds.minX, bounds.minY]
  1009. const [ax1, ay1] = [bounds.maxX, bounds.maxY]
  1010. // Create a second set of corners for the new box.
  1011. let [bx0, by0] = [bounds.minX, bounds.minY]
  1012. let [bx1, by1] = [bounds.maxX, bounds.maxY]
  1013. // If the drag is on the center, just translate the bounds.
  1014. if (handle === 'center') {
  1015. return {
  1016. minX: bx0 + delta[0],
  1017. minY: by0 + delta[1],
  1018. maxX: bx1 + delta[0],
  1019. maxY: by1 + delta[1],
  1020. width: bx1 - bx0,
  1021. height: by1 - by0,
  1022. scaleX: 1,
  1023. scaleY: 1,
  1024. }
  1025. }
  1026. // Counter rotate the delta. This lets us make changes as if
  1027. // the (possibly rotated) boxes were axis aligned.
  1028. const [dx, dy] = vec.rot(delta, -rotation)
  1029. /*
  1030. 1. Delta
  1031. Use the delta to adjust the new box by changing its corners.
  1032. The dragging handle (corner or edge) will determine which
  1033. corners should change.
  1034. */
  1035. switch (handle) {
  1036. case Edge.Top:
  1037. case Corner.TopLeft:
  1038. case Corner.TopRight: {
  1039. by0 += dy
  1040. break
  1041. }
  1042. case Edge.Bottom:
  1043. case Corner.BottomLeft:
  1044. case Corner.BottomRight: {
  1045. by1 += dy
  1046. break
  1047. }
  1048. }
  1049. switch (handle) {
  1050. case Edge.Left:
  1051. case Corner.TopLeft:
  1052. case Corner.BottomLeft: {
  1053. bx0 += dx
  1054. break
  1055. }
  1056. case Edge.Right:
  1057. case Corner.TopRight:
  1058. case Corner.BottomRight: {
  1059. bx1 += dx
  1060. break
  1061. }
  1062. }
  1063. const aw = ax1 - ax0
  1064. const ah = ay1 - ay0
  1065. const scaleX = (bx1 - bx0) / aw
  1066. const scaleY = (by1 - by0) / ah
  1067. const flipX = scaleX < 0
  1068. const flipY = scaleY < 0
  1069. const bw = Math.abs(bx1 - bx0)
  1070. const bh = Math.abs(by1 - by0)
  1071. /*
  1072. 2. Aspect ratio
  1073. If the aspect ratio is locked, adjust the corners so that the
  1074. new box's aspect ratio matches the original aspect ratio.
  1075. */
  1076. if (isAspectRatioLocked) {
  1077. const ar = aw / ah
  1078. const isTall = ar < bw / bh
  1079. const tw = bw * (scaleY < 0 ? 1 : -1) * (1 / ar)
  1080. const th = bh * (scaleX < 0 ? 1 : -1) * ar
  1081. switch (handle) {
  1082. case Corner.TopLeft: {
  1083. if (isTall) by0 = by1 + tw
  1084. else bx0 = bx1 + th
  1085. break
  1086. }
  1087. case Corner.TopRight: {
  1088. if (isTall) by0 = by1 + tw
  1089. else bx1 = bx0 - th
  1090. break
  1091. }
  1092. case Corner.BottomRight: {
  1093. if (isTall) by1 = by0 - tw
  1094. else bx1 = bx0 - th
  1095. break
  1096. }
  1097. case Corner.BottomLeft: {
  1098. if (isTall) by1 = by0 - tw
  1099. else bx0 = bx1 + th
  1100. break
  1101. }
  1102. case Edge.Bottom:
  1103. case Edge.Top: {
  1104. const m = (bx0 + bx1) / 2
  1105. const w = bh * ar
  1106. bx0 = m - w / 2
  1107. bx1 = m + w / 2
  1108. break
  1109. }
  1110. case Edge.Left:
  1111. case Edge.Right: {
  1112. const m = (by0 + by1) / 2
  1113. const h = bw / ar
  1114. by0 = m - h / 2
  1115. by1 = m + h / 2
  1116. break
  1117. }
  1118. }
  1119. }
  1120. /*
  1121. 3. Rotation
  1122. If the bounds are rotated, get a vector from the rotated anchor
  1123. corner in the inital bounds to the rotated anchor corner in the
  1124. result's bounds. Subtract this vector from the result's corners,
  1125. so that the two anchor points (initial and result) will be equal.
  1126. */
  1127. if (rotation % (Math.PI * 2) !== 0) {
  1128. let cv = [0, 0]
  1129. const c0 = vec.med([ax0, ay0], [ax1, ay1])
  1130. const c1 = vec.med([bx0, by0], [bx1, by1])
  1131. switch (handle) {
  1132. case Corner.TopLeft: {
  1133. cv = vec.sub(
  1134. vec.rotWith([bx1, by1], c1, rotation),
  1135. vec.rotWith([ax1, ay1], c0, rotation)
  1136. )
  1137. break
  1138. }
  1139. case Corner.TopRight: {
  1140. cv = vec.sub(
  1141. vec.rotWith([bx0, by1], c1, rotation),
  1142. vec.rotWith([ax0, ay1], c0, rotation)
  1143. )
  1144. break
  1145. }
  1146. case Corner.BottomRight: {
  1147. cv = vec.sub(
  1148. vec.rotWith([bx0, by0], c1, rotation),
  1149. vec.rotWith([ax0, ay0], c0, rotation)
  1150. )
  1151. break
  1152. }
  1153. case Corner.BottomLeft: {
  1154. cv = vec.sub(
  1155. vec.rotWith([bx1, by0], c1, rotation),
  1156. vec.rotWith([ax1, ay0], c0, rotation)
  1157. )
  1158. break
  1159. }
  1160. case Edge.Top: {
  1161. cv = vec.sub(
  1162. vec.rotWith(vec.med([bx0, by1], [bx1, by1]), c1, rotation),
  1163. vec.rotWith(vec.med([ax0, ay1], [ax1, ay1]), c0, rotation)
  1164. )
  1165. break
  1166. }
  1167. case Edge.Left: {
  1168. cv = vec.sub(
  1169. vec.rotWith(vec.med([bx1, by0], [bx1, by1]), c1, rotation),
  1170. vec.rotWith(vec.med([ax1, ay0], [ax1, ay1]), c0, rotation)
  1171. )
  1172. break
  1173. }
  1174. case Edge.Bottom: {
  1175. cv = vec.sub(
  1176. vec.rotWith(vec.med([bx0, by0], [bx1, by0]), c1, rotation),
  1177. vec.rotWith(vec.med([ax0, ay0], [ax1, ay0]), c0, rotation)
  1178. )
  1179. break
  1180. }
  1181. case Edge.Right: {
  1182. cv = vec.sub(
  1183. vec.rotWith(vec.med([bx0, by0], [bx0, by1]), c1, rotation),
  1184. vec.rotWith(vec.med([ax0, ay0], [ax0, ay1]), c0, rotation)
  1185. )
  1186. break
  1187. }
  1188. }
  1189. ;[bx0, by0] = vec.sub([bx0, by0], cv)
  1190. ;[bx1, by1] = vec.sub([bx1, by1], cv)
  1191. }
  1192. /*
  1193. 4. Flips
  1194. If the axes are flipped (e.g. if the right edge has been dragged
  1195. left past the initial left edge) then swap points on that axis.
  1196. */
  1197. if (bx1 < bx0) {
  1198. ;[bx1, bx0] = [bx0, bx1]
  1199. }
  1200. if (by1 < by0) {
  1201. ;[by1, by0] = [by0, by1]
  1202. }
  1203. return {
  1204. minX: bx0,
  1205. minY: by0,
  1206. maxX: bx1,
  1207. maxY: by1,
  1208. width: bx1 - bx0,
  1209. height: by1 - by0,
  1210. scaleX: ((bx1 - bx0) / (ax1 - ax0 || 1)) * (flipX ? -1 : 1),
  1211. scaleY: ((by1 - by0) / (ay1 - ay0 || 1)) * (flipY ? -1 : 1),
  1212. }
  1213. }
  1214. export function getTransformAnchor(
  1215. type: Edge | Corner,
  1216. isFlippedX: boolean,
  1217. isFlippedY: boolean
  1218. ): Corner | Edge {
  1219. let anchor: Corner | Edge = type
  1220. // Change corner anchors if flipped
  1221. switch (type) {
  1222. case Corner.TopLeft: {
  1223. if (isFlippedX && isFlippedY) {
  1224. anchor = Corner.BottomRight
  1225. } else if (isFlippedX) {
  1226. anchor = Corner.TopRight
  1227. } else if (isFlippedY) {
  1228. anchor = Corner.BottomLeft
  1229. } else {
  1230. anchor = Corner.BottomRight
  1231. }
  1232. break
  1233. }
  1234. case Corner.TopRight: {
  1235. if (isFlippedX && isFlippedY) {
  1236. anchor = Corner.BottomLeft
  1237. } else if (isFlippedX) {
  1238. anchor = Corner.TopLeft
  1239. } else if (isFlippedY) {
  1240. anchor = Corner.BottomRight
  1241. } else {
  1242. anchor = Corner.BottomLeft
  1243. }
  1244. break
  1245. }
  1246. case Corner.BottomRight: {
  1247. if (isFlippedX && isFlippedY) {
  1248. anchor = Corner.TopLeft
  1249. } else if (isFlippedX) {
  1250. anchor = Corner.BottomLeft
  1251. } else if (isFlippedY) {
  1252. anchor = Corner.TopRight
  1253. } else {
  1254. anchor = Corner.TopLeft
  1255. }
  1256. break
  1257. }
  1258. case Corner.BottomLeft: {
  1259. if (isFlippedX && isFlippedY) {
  1260. anchor = Corner.TopRight
  1261. } else if (isFlippedX) {
  1262. anchor = Corner.BottomRight
  1263. } else if (isFlippedY) {
  1264. anchor = Corner.TopLeft
  1265. } else {
  1266. anchor = Corner.TopRight
  1267. }
  1268. break
  1269. }
  1270. }
  1271. return anchor
  1272. }
  1273. /**
  1274. * Get the relative bounds (usually a child) within a transformed bounding box.
  1275. * @param bounds
  1276. * @param initialBounds
  1277. * @param initialShapeBounds
  1278. * @param isFlippedX
  1279. * @param isFlippedY
  1280. */
  1281. export function getRelativeTransformedBoundingBox(
  1282. bounds: Bounds,
  1283. initialBounds: Bounds,
  1284. initialShapeBounds: Bounds,
  1285. isFlippedX: boolean,
  1286. isFlippedY: boolean
  1287. ): Bounds {
  1288. const nx =
  1289. (isFlippedX
  1290. ? initialBounds.maxX - initialShapeBounds.maxX
  1291. : initialShapeBounds.minX - initialBounds.minX) / initialBounds.width
  1292. const ny =
  1293. (isFlippedY
  1294. ? initialBounds.maxY - initialShapeBounds.maxY
  1295. : initialShapeBounds.minY - initialBounds.minY) / initialBounds.height
  1296. const nw = initialShapeBounds.width / initialBounds.width
  1297. const nh = initialShapeBounds.height / initialBounds.height
  1298. const minX = bounds.minX + bounds.width * nx
  1299. const minY = bounds.minY + bounds.height * ny
  1300. const width = bounds.width * nw
  1301. const height = bounds.height * nh
  1302. return {
  1303. minX,
  1304. minY,
  1305. maxX: minX + width,
  1306. maxY: minY + height,
  1307. width,
  1308. height,
  1309. }
  1310. }
  1311. /**
  1312. * Get the size of a rotated box.
  1313. * @param size : ;
  1314. * @param rotation
  1315. */
  1316. export function getRotatedSize(size: number[], rotation: number): number[] {
  1317. const center = vec.div(size, 2)
  1318. const points = [[0, 0], [size[0], 0], size, [0, size[1]]].map((point) =>
  1319. vec.rotWith(point, center, rotation)
  1320. )
  1321. const bounds = getBoundsFromPoints(points)
  1322. return [bounds.width, bounds.height]
  1323. }
  1324. /**
  1325. * Get the center of a bounding box.
  1326. * @param bounds
  1327. */
  1328. export function getBoundsCenter(bounds: Bounds): number[] {
  1329. return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
  1330. }
  1331. /* -------------------------------------------------- */
  1332. /* Lists and Collections */
  1333. /* -------------------------------------------------- */
  1334. /**
  1335. * Get a unique string id.
  1336. */
  1337. export function uniqueId(): string {
  1338. return uuid()
  1339. }
  1340. /**
  1341. * Shuffle the contents of an array.
  1342. * @param arr
  1343. * @param offset
  1344. */
  1345. export function shuffleArr<T>(arr: T[], offset: number): T[] {
  1346. return arr.map((_, i) => arr[(i + offset) % arr.length])
  1347. }
  1348. /**
  1349. * Deep compare two arrays.
  1350. * @param a
  1351. * @param b
  1352. */
  1353. export function deepCompareArrays<T>(a: T[], b: T[]): boolean {
  1354. if (a?.length !== b?.length) return false
  1355. return deepCompare(a, b)
  1356. }
  1357. /**
  1358. * Deep compare any values.
  1359. * @param a
  1360. * @param b
  1361. */
  1362. export function deepCompare<T>(a: T, b: T): boolean {
  1363. return a === b || JSON.stringify(a) === JSON.stringify(b)
  1364. }
  1365. /**
  1366. * Find whether two arrays intersect.
  1367. * @param a
  1368. * @param b
  1369. * @param fn An optional function to apply to the items of a; will check if b includes the result.
  1370. */
  1371. export function arrsIntersect<T, K>(
  1372. a: T[],
  1373. b: K[],
  1374. fn?: (item: K) => T
  1375. ): boolean
  1376. export function arrsIntersect<T>(a: T[], b: T[]): boolean
  1377. export function arrsIntersect<T>(
  1378. a: T[],
  1379. b: unknown[],
  1380. fn?: (item: unknown) => T
  1381. ): boolean {
  1382. return a.some((item) => b.includes(fn ? fn(item) : item))
  1383. }
  1384. /**
  1385. * Get the unique values from an array of strings or numbers.
  1386. * @param items
  1387. */
  1388. export function uniqueArray<T extends string | number>(...items: T[]): T[] {
  1389. return Array.from(new Set(items).values())
  1390. }
  1391. /**
  1392. * Convert a set to an array.
  1393. * @param set
  1394. */
  1395. export function setToArray<T>(set: Set<T>): T[] {
  1396. return Array.from(set.values())
  1397. }
  1398. /* -------------------------------------------------- */
  1399. /* Browser and DOM */
  1400. /* -------------------------------------------------- */
  1401. /**
  1402. * Find whether the current display is a touch display.
  1403. */
  1404. export function isTouchDisplay(): boolean {
  1405. return (
  1406. 'ontouchstart' in window ||
  1407. navigator.maxTouchPoints > 0 ||
  1408. navigator.msMaxTouchPoints > 0
  1409. )
  1410. }
  1411. /**
  1412. * Find whether the current device is a Mac / iOS / iPadOS.
  1413. */
  1414. export function isDarwin(): boolean {
  1415. return /Mac|iPod|iPhone|iPad/.test(window.navigator.platform)
  1416. }
  1417. /**
  1418. * Get whether the current device is a mobile device.
  1419. */
  1420. export function isMobile(): boolean {
  1421. return _isMobile().any
  1422. }
  1423. /**
  1424. * Get whether an event is command (mac) or control (pc).
  1425. * @param e
  1426. */
  1427. export function metaKey(e: KeyboardEvent | React.KeyboardEvent): boolean {
  1428. return isDarwin() ? e.metaKey : e.ctrlKey
  1429. }
  1430. /**
  1431. * Find the closest point on a SVG path to an off-path point.
  1432. * @param pathNode
  1433. * @param point
  1434. * @returns
  1435. */
  1436. export function getClosestPointOnSVGPath(
  1437. pathNode: SVGPathElement,
  1438. point: number[]
  1439. ): {
  1440. point: number[]
  1441. distance: number
  1442. length: number
  1443. t: number
  1444. } {
  1445. function distance2(p: DOMPoint, point: number[]) {
  1446. const dx = p.x - point[0],
  1447. dy = p.y - point[1]
  1448. return dx * dx + dy * dy
  1449. }
  1450. const pathLen = pathNode.getTotalLength()
  1451. let p = 8,
  1452. best: DOMPoint,
  1453. bestLen: number,
  1454. bestDist = Infinity,
  1455. bl: number,
  1456. al: number
  1457. // linear scan for coarse approximation
  1458. for (
  1459. let scan: DOMPoint, scanLen = 0, scanDist: number;
  1460. scanLen <= pathLen;
  1461. scanLen += p
  1462. ) {
  1463. if (
  1464. (scanDist = distance2(
  1465. (scan = pathNode.getPointAtLength(scanLen)),
  1466. point
  1467. )) < bestDist
  1468. ) {
  1469. ;(best = scan), (bestLen = scanLen), (bestDist = scanDist)
  1470. }
  1471. }
  1472. // binary search for precise estimate
  1473. p /= 2
  1474. while (p > 0.5) {
  1475. let before: DOMPoint, after: DOMPoint, bd: number, ad: number
  1476. if (
  1477. (bl = bestLen - p) >= 0 &&
  1478. (bd = distance2((before = pathNode.getPointAtLength(bl)), point)) <
  1479. bestDist
  1480. ) {
  1481. ;(best = before), (bestLen = bl), (bestDist = bd)
  1482. } else if (
  1483. (al = bestLen + p) <= pathLen &&
  1484. (ad = distance2((after = pathNode.getPointAtLength(al)), point)) <
  1485. bestDist
  1486. ) {
  1487. ;(best = after), (bestLen = al), (bestDist = ad)
  1488. } else {
  1489. p /= 2
  1490. }
  1491. }
  1492. return {
  1493. point: [best.x, best.y],
  1494. distance: bestDist,
  1495. length: (bl + al) / 2,
  1496. t: (bl + al) / 2 / pathLen,
  1497. }
  1498. }
  1499. /**
  1500. * Send data to one of the current project's API endpoints.
  1501. * @param endpoint
  1502. * @param data
  1503. */
  1504. export async function postJsonToEndpoint(
  1505. endpoint: string,
  1506. data: { [key: string]: unknown }
  1507. ): Promise<{ [key: string]: any }> {
  1508. const d = await fetch(
  1509. `${process.env.NEXT_PUBLIC_BASE_API_URL}/api/${endpoint}`,
  1510. {
  1511. method: 'POST',
  1512. headers: { 'Content-Type': 'application/json' },
  1513. body: JSON.stringify(data),
  1514. }
  1515. )
  1516. return await d.json()
  1517. }
  1518. /**
  1519. * Turn an array of points into a path of quadradic curves.
  1520. * @param stroke ;
  1521. */
  1522. export function getSvgPathFromStroke(stroke: number[][]): string {
  1523. if (!stroke.length) return ''
  1524. const d = stroke.reduce(
  1525. (acc, [x0, y0], i, arr) => {
  1526. const [x1, y1] = arr[(i + 1) % arr.length]
  1527. acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2)
  1528. return acc
  1529. },
  1530. ['M', ...stroke[0], 'Q']
  1531. )
  1532. d.push('Z')
  1533. return d.join(' ')
  1534. }
  1535. export function debounce<T extends (...args: unknown[]) => unknown>(
  1536. callback: T,
  1537. waitFor: number
  1538. ): (...args: Parameters<T>) => ReturnType<T> {
  1539. let timeout: ReturnType<typeof setTimeout>
  1540. return (...args: Parameters<T>): ReturnType<T> => {
  1541. let result: any
  1542. timeout && clearTimeout(timeout)
  1543. timeout = setTimeout(() => {
  1544. result = callback(...args)
  1545. }, waitFor)
  1546. return result
  1547. }
  1548. }
  1549. /**
  1550. * Get a precise point from an event.
  1551. * @param e
  1552. */
  1553. export function getPoint(
  1554. e: PointerEvent | React.PointerEvent | Touch | React.Touch | WheelEvent
  1555. ): number[] {
  1556. return [
  1557. Number(e.clientX.toPrecision(5)),
  1558. Number(e.clientY.toPrecision(5)),
  1559. 'pressure' in e ? Number(e.pressure.toPrecision(5)) || 0.5 : 0.5,
  1560. ]
  1561. }
  1562. export function commandKey(): string {
  1563. return isDarwin() ? '⌘' : 'Ctrl'
  1564. }
  1565. // function getResizeOffset(a: Bounds, b: Bounds): number[] {
  1566. // const { minX: x0, minY: y0, width: w0, height: h0 } = a
  1567. // const { minX: x1, minY: y1, width: w1, height: h1 } = b
  1568. // let delta: number[]
  1569. // if (h0 === h1 && w0 !== w1) {
  1570. // if (x0 !== x1) {
  1571. // // moving left edge, pin right edge
  1572. // delta = vec.sub([x1, y1 + h1 / 2], [x0, y0 + h0 / 2])
  1573. // } else {
  1574. // // moving right edge, pin left edge
  1575. // delta = vec.sub([x1 + w1, y1 + h1 / 2], [x0 + w0, y0 + h0 / 2])
  1576. // }
  1577. // } else if (h0 !== h1 && w0 === w1) {
  1578. // if (y0 !== y1) {
  1579. // // moving top edge, pin bottom edge
  1580. // delta = vec.sub([x1 + w1 / 2, y1], [x0 + w0 / 2, y0])
  1581. // } else {
  1582. // // moving bottom edge, pin top edge
  1583. // delta = vec.sub([x1 + w1 / 2, y1 + h1], [x0 + w0 / 2, y0 + h0])
  1584. // }
  1585. // } else if (x0 !== x1) {
  1586. // if (y0 !== y1) {
  1587. // // moving top left, pin bottom right
  1588. // delta = vec.sub([x1, y1], [x0, y0])
  1589. // } else {
  1590. // // moving bottom left, pin top right
  1591. // delta = vec.sub([x1, y1 + h1], [x0, y0 + h0])
  1592. // }
  1593. // } else if (y0 !== y1) {
  1594. // // moving top right, pin bottom left
  1595. // delta = vec.sub([x1 + w1, y1], [x0 + w0, y0])
  1596. // } else {
  1597. // // moving bottom right, pin top left
  1598. // delta = vec.sub([x1 + w1, y1 + h1], [x0 + w0, y0 + h0])
  1599. // }
  1600. // return delta
  1601. // }