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

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