Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

utils.ts 42KB

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