您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

utils.ts 43KB

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