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

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