Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

utils.ts 43KB

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