Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

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