You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172
  1. import Vector from "lib/code/vector"
  2. import { getShapeUtils } from "lib/shapes"
  3. import React from "react"
  4. import { Data, Bounds, TransformEdge, TransformCorner, Shape } from "types"
  5. import * as svg from "./svg"
  6. import * as vec from "./vec"
  7. export function screenToWorld(point: number[], data: Data) {
  8. return vec.sub(vec.div(point, data.camera.zoom), data.camera.point)
  9. }
  10. /**
  11. * Get a bounding box that includes two bounding boxes.
  12. * @param a Bounding box
  13. * @param b Bounding box
  14. * @returns
  15. */
  16. export function getExpandedBounds(a: Bounds, b: Bounds) {
  17. const minX = Math.min(a.minX, b.minX),
  18. minY = Math.min(a.minY, b.minY),
  19. maxX = Math.max(a.maxX, b.maxX),
  20. maxY = Math.max(a.maxY, b.maxY),
  21. width = Math.abs(maxX - minX),
  22. height = Math.abs(maxY - minY)
  23. return { minX, minY, maxX, maxY, width, height }
  24. }
  25. /**
  26. * Get the common bounds of a group of bounds.
  27. * @returns
  28. */
  29. export function getCommonBounds(...b: Bounds[]) {
  30. if (b.length < 2) return b[0]
  31. let bounds = b[0]
  32. for (let i = 1; i < b.length; i++) {
  33. bounds = getExpandedBounds(bounds, b[i])
  34. }
  35. return bounds
  36. }
  37. // export function getBoundsFromPoints(a: number[], b: number[]) {
  38. // const minX = Math.min(a[0], b[0])
  39. // const maxX = Math.max(a[0], b[0])
  40. // const minY = Math.min(a[1], b[1])
  41. // const maxY = Math.max(a[1], b[1])
  42. // return {
  43. // minX,
  44. // maxX,
  45. // minY,
  46. // maxY,
  47. // width: maxX - minX,
  48. // height: maxY - minY,
  49. // }
  50. // }
  51. // A helper for getting tangents.
  52. export function getCircleTangentToPoint(
  53. A: number[],
  54. r0: number,
  55. P: number[],
  56. side: number
  57. ) {
  58. const B = vec.lrp(A, P, 0.5),
  59. r1 = vec.dist(A, B),
  60. delta = vec.sub(B, A),
  61. d = vec.len(delta)
  62. if (!(d <= r0 + r1 && d >= Math.abs(r0 - r1))) {
  63. return
  64. }
  65. const a = (r0 * r0 - r1 * r1 + d * d) / (2.0 * d),
  66. n = 1 / d,
  67. p = vec.add(A, vec.mul(delta, a * n)),
  68. h = Math.sqrt(r0 * r0 - a * a),
  69. k = vec.mul(vec.per(delta), h * n)
  70. return side === 0 ? vec.add(p, k) : vec.sub(p, k)
  71. }
  72. export function circleCircleIntersections(a: number[], b: number[]) {
  73. const R = a[2],
  74. r = b[2]
  75. let dx = b[0] - a[0],
  76. dy = b[1] - a[1]
  77. const d = Math.sqrt(dx * dx + dy * dy),
  78. x = (d * d - r * r + R * R) / (2 * d),
  79. y = Math.sqrt(R * R - x * x)
  80. dx /= d
  81. dy /= d
  82. return [
  83. [a[0] + dx * x - dy * y, a[1] + dy * x + dx * y],
  84. [a[0] + dx * x + dy * y, a[1] + dy * x - dx * y],
  85. ]
  86. }
  87. export function getClosestPointOnCircle(
  88. C: number[],
  89. r: number,
  90. P: number[],
  91. padding = 0
  92. ) {
  93. const v = vec.sub(C, P)
  94. return vec.sub(C, vec.mul(vec.div(v, vec.len(v)), r + padding))
  95. }
  96. export function projectPoint(p0: number[], a: number, d: number) {
  97. return [Math.cos(a) * d + p0[0], Math.sin(a) * d + p0[1]]
  98. }
  99. function shortAngleDist(a0: number, a1: number) {
  100. const max = Math.PI * 2
  101. const da = (a1 - a0) % max
  102. return ((2 * da) % max) - da
  103. }
  104. export function lerpAngles(a0: number, a1: number, t: number) {
  105. return a0 + shortAngleDist(a0, a1) * t
  106. }
  107. export function getBezierCurveSegments(points: number[][], tension = 0.4) {
  108. const len = points.length,
  109. cpoints: number[][] = [...points]
  110. if (len < 2) {
  111. throw Error("Curve must have at least two points.")
  112. }
  113. for (let i = 1; i < len - 1; i++) {
  114. const p0 = points[i - 1],
  115. p1 = points[i],
  116. p2 = points[i + 1]
  117. const pdx = p2[0] - p0[0],
  118. pdy = p2[1] - p0[1],
  119. pd = Math.hypot(pdx, pdy),
  120. nx = pdx / pd, // normalized x
  121. ny = pdy / pd, // normalized y
  122. dp = Math.hypot(p1[0] - p0[0], p1[1] - p0[1]), // Distance to previous
  123. dn = Math.hypot(p1[0] - p2[0], p1[1] - p2[1]) // Distance to next
  124. cpoints[i] = [
  125. // tangent start
  126. p1[0] - nx * dp * tension,
  127. p1[1] - ny * dp * tension,
  128. // tangent end
  129. p1[0] + nx * dn * tension,
  130. p1[1] + ny * dn * tension,
  131. // normal
  132. nx,
  133. ny,
  134. ]
  135. }
  136. // TODO: Reflect the nearest control points, not average them
  137. const d0 = Math.hypot(points[0][0] + cpoints[1][0])
  138. cpoints[0][2] = (points[0][0] + cpoints[1][0]) / 2
  139. cpoints[0][3] = (points[0][1] + cpoints[1][1]) / 2
  140. cpoints[0][4] = (cpoints[1][0] - points[0][0]) / d0
  141. cpoints[0][5] = (cpoints[1][1] - points[0][1]) / d0
  142. const d1 = Math.hypot(points[len - 1][1] + cpoints[len - 1][1])
  143. cpoints[len - 1][0] = (points[len - 1][0] + cpoints[len - 2][2]) / 2
  144. cpoints[len - 1][1] = (points[len - 1][1] + cpoints[len - 2][3]) / 2
  145. cpoints[len - 1][4] = (cpoints[len - 2][2] - points[len - 1][0]) / -d1
  146. cpoints[len - 1][5] = (cpoints[len - 2][3] - points[len - 1][1]) / -d1
  147. const results: {
  148. start: number[]
  149. tangentStart: number[]
  150. normalStart: number[]
  151. pressureStart: number
  152. end: number[]
  153. tangentEnd: number[]
  154. normalEnd: number[]
  155. pressureEnd: number
  156. }[] = []
  157. for (let i = 1; i < cpoints.length; i++) {
  158. results.push({
  159. start: points[i - 1].slice(0, 2),
  160. tangentStart: cpoints[i - 1].slice(2, 4),
  161. normalStart: cpoints[i - 1].slice(4, 6),
  162. pressureStart: 2 + ((i - 1) % 2 === 0 ? 1.5 : 0),
  163. end: points[i].slice(0, 2),
  164. tangentEnd: cpoints[i].slice(0, 2),
  165. normalEnd: cpoints[i].slice(4, 6),
  166. pressureEnd: 2 + (i % 2 === 0 ? 1.5 : 0),
  167. })
  168. }
  169. return results
  170. }
  171. export function cubicBezier(
  172. tx: number,
  173. x1: number,
  174. y1: number,
  175. x2: number,
  176. y2: number
  177. ) {
  178. // Inspired by Don Lancaster's two articles
  179. // http://www.tinaja.com/glib/cubemath.pdf
  180. // http://www.tinaja.com/text/bezmath.html
  181. // Set start and end point
  182. const x0 = 0,
  183. y0 = 0,
  184. x3 = 1,
  185. y3 = 1,
  186. // Convert the coordinates to equation space
  187. A = x3 - 3 * x2 + 3 * x1 - x0,
  188. B = 3 * x2 - 6 * x1 + 3 * x0,
  189. C = 3 * x1 - 3 * x0,
  190. D = x0,
  191. E = y3 - 3 * y2 + 3 * y1 - y0,
  192. F = 3 * y2 - 6 * y1 + 3 * y0,
  193. G = 3 * y1 - 3 * y0,
  194. H = y0,
  195. // Variables for the loop below
  196. iterations = 5
  197. let i: number,
  198. slope: number,
  199. x: number,
  200. t = tx
  201. // Loop through a few times to get a more accurate time value, according to the Newton-Raphson method
  202. // http://en.wikipedia.org/wiki/Newton's_method
  203. for (i = 0; i < iterations; i++) {
  204. // The curve's x equation for the current time value
  205. x = A * t * t * t + B * t * t + C * t + D
  206. // The slope we want is the inverse of the derivate of x
  207. slope = 1 / (3 * A * t * t + 2 * B * t + C)
  208. // Get the next estimated time value, which will be more accurate than the one before
  209. t -= (x - tx) * slope
  210. t = t > 1 ? 1 : t < 0 ? 0 : t
  211. }
  212. // Find the y value through the curve's y equation, with the now more accurate time value
  213. return Math.abs(E * t * t * t + F * t * t + G * t * H)
  214. }
  215. export function copyToClipboard(string: string) {
  216. let textarea: HTMLTextAreaElement
  217. let result: boolean
  218. try {
  219. navigator.clipboard.writeText(string)
  220. } catch (e) {
  221. try {
  222. textarea = document.createElement("textarea")
  223. textarea.setAttribute("position", "fixed")
  224. textarea.setAttribute("top", "0")
  225. textarea.setAttribute("readonly", "true")
  226. textarea.setAttribute("contenteditable", "true")
  227. textarea.style.position = "fixed" // prevent scroll from jumping to the bottom when focus is set.
  228. textarea.value = string
  229. document.body.appendChild(textarea)
  230. textarea.focus()
  231. textarea.select()
  232. const range = document.createRange()
  233. range.selectNodeContents(textarea)
  234. const sel = window.getSelection()
  235. sel.removeAllRanges()
  236. sel.addRange(range)
  237. textarea.setSelectionRange(0, textarea.value.length)
  238. result = document.execCommand("copy")
  239. } catch (err) {
  240. result = null
  241. } finally {
  242. document.body.removeChild(textarea)
  243. }
  244. }
  245. return !!result
  246. }
  247. /**
  248. * Get a bezier curve data to for a spline that fits an array of points.
  249. * @param points An array of points formatted as [x, y]
  250. * @param k Tension
  251. * @returns An array of points as [cp1x, cp1y, cp2x, cp2y, px, py].
  252. */
  253. export function getSpline(pts: number[][], k = 0.5) {
  254. let p0: number[],
  255. [p1, p2, p3] = pts
  256. const results: number[][] = []
  257. for (let i = 1, len = pts.length; i < len; i++) {
  258. p0 = p1
  259. p1 = p2
  260. p2 = p3
  261. p3 = pts[i + 2] ? pts[i + 2] : p2
  262. results.push([
  263. p1[0] + ((p2[0] - p0[0]) / 6) * k,
  264. p1[1] + ((p2[1] - p0[1]) / 6) * k,
  265. p2[0] - ((p3[0] - p1[0]) / 6) * k,
  266. p2[1] - ((p3[1] - p1[1]) / 6) * k,
  267. pts[i][0],
  268. pts[i][1],
  269. ])
  270. }
  271. return results
  272. }
  273. export function getCurvePoints(
  274. pts: number[][],
  275. tension = 0.5,
  276. isClosed = false,
  277. numOfSegments = 3
  278. ) {
  279. const _pts = [...pts],
  280. len = pts.length,
  281. res: number[][] = [] // results
  282. let t1x: number, // tension vectors
  283. t2x: number,
  284. t1y: number,
  285. t2y: number,
  286. c1: number, // cardinal points
  287. c2: number,
  288. c3: number,
  289. c4: number,
  290. st: number,
  291. st2: number,
  292. st3: number
  293. // The algorithm require a previous and next point to the actual point array.
  294. // Check if we will draw closed or open curve.
  295. // If closed, copy end points to beginning and first points to end
  296. // If open, duplicate first points to befinning, end points to end
  297. if (isClosed) {
  298. _pts.unshift(_pts[len - 1])
  299. _pts.push(_pts[0])
  300. } else {
  301. //copy 1. point and insert at beginning
  302. _pts.unshift(_pts[0])
  303. _pts.push(_pts[len - 1])
  304. // _pts.push(_pts[len - 1])
  305. }
  306. // For each point, calculate a segment
  307. for (let i = 1; i < _pts.length - 2; i++) {
  308. // Calculate points along segment and add to results
  309. for (let t = 0; t <= numOfSegments; t++) {
  310. // Step
  311. st = t / numOfSegments
  312. st2 = Math.pow(st, 2)
  313. st3 = Math.pow(st, 3)
  314. // Cardinals
  315. c1 = 2 * st3 - 3 * st2 + 1
  316. c2 = -(2 * st3) + 3 * st2
  317. c3 = st3 - 2 * st2 + st
  318. c4 = st3 - st2
  319. // Tension
  320. t1x = (_pts[i + 1][0] - _pts[i - 1][0]) * tension
  321. t2x = (_pts[i + 2][0] - _pts[i][0]) * tension
  322. t1y = (_pts[i + 1][1] - _pts[i - 1][1]) * tension
  323. t2y = (_pts[i + 2][1] - _pts[i][1]) * tension
  324. // Control points
  325. res.push([
  326. c1 * _pts[i][0] + c2 * _pts[i + 1][0] + c3 * t1x + c4 * t2x,
  327. c1 * _pts[i][1] + c2 * _pts[i + 1][1] + c3 * t1y + c4 * t2y,
  328. ])
  329. }
  330. }
  331. res.push(pts[pts.length - 1])
  332. return res
  333. }
  334. export function angleDelta(a0: number, a1: number) {
  335. return shortAngleDist(a0, a1)
  336. }
  337. /**
  338. * Rotate a point around a center.
  339. * @param x The x-axis coordinate of the point.
  340. * @param y The y-axis coordinate of the point.
  341. * @param cx The x-axis coordinate of the point to rotate round.
  342. * @param cy The y-axis coordinate of the point to rotate round.
  343. * @param angle The distance (in radians) to rotate.
  344. */
  345. export function rotatePoint(A: number[], B: number[], angle: number) {
  346. const s = Math.sin(angle)
  347. const c = Math.cos(angle)
  348. const px = A[0] - B[0]
  349. const py = A[1] - B[1]
  350. const nx = px * c - py * s
  351. const ny = px * s + py * c
  352. return [nx + B[0], ny + B[1]]
  353. }
  354. export function degreesToRadians(d: number) {
  355. return (d * Math.PI) / 180
  356. }
  357. export function radiansToDegrees(r: number) {
  358. return (r * 180) / Math.PI
  359. }
  360. export function getArcLength(C: number[], r: number, A: number[], B: number[]) {
  361. const sweep = getSweep(C, A, B)
  362. return r * (2 * Math.PI) * (sweep / (2 * Math.PI))
  363. }
  364. export function getArcDashOffset(
  365. C: number[],
  366. r: number,
  367. A: number[],
  368. B: number[],
  369. step: number
  370. ) {
  371. const del0 = getSweep(C, A, B)
  372. const len0 = getArcLength(C, r, A, B)
  373. const off0 = del0 < 0 ? len0 : 2 * Math.PI * C[2] - len0
  374. return -off0 / 2 + step
  375. }
  376. export function getEllipseDashOffset(A: number[], step: number) {
  377. const c = 2 * Math.PI * A[2]
  378. return -c / 2 + -step
  379. }
  380. export function getSweep(C: number[], A: number[], B: number[]) {
  381. return angleDelta(vec.angle(C, A), vec.angle(C, B))
  382. }
  383. export function deepCompareArrays<T>(a: T[], b: T[]) {
  384. if (a?.length !== b?.length) return false
  385. return deepCompare(a, b)
  386. }
  387. export function deepCompare<T>(a: T, b: T) {
  388. return a === b || JSON.stringify(a) === JSON.stringify(b)
  389. }
  390. /**
  391. * Get outer tangents of two circles.
  392. * @param x0
  393. * @param y0
  394. * @param r0
  395. * @param x1
  396. * @param y1
  397. * @param r1
  398. * @returns [lx0, ly0, lx1, ly1, rx0, ry0, rx1, ry1]
  399. */
  400. export function getOuterTangents(
  401. C0: number[],
  402. r0: number,
  403. C1: number[],
  404. r1: number
  405. ) {
  406. const a0 = vec.angle(C0, C1)
  407. const d = vec.dist(C0, C1)
  408. // Circles are overlapping, no tangents
  409. if (d < Math.abs(r1 - r0)) return
  410. const a1 = Math.acos((r0 - r1) / d),
  411. t0 = a0 + a1,
  412. t1 = a0 - a1
  413. return [
  414. [C0[0] + r0 * Math.cos(t1), C0[1] + r0 * Math.sin(t1)],
  415. [C1[0] + r1 * Math.cos(t1), C1[1] + r1 * Math.sin(t1)],
  416. [C0[0] + r0 * Math.cos(t0), C0[1] + r0 * Math.sin(t0)],
  417. [C1[0] + r1 * Math.cos(t0), C1[1] + r1 * Math.sin(t0)],
  418. ]
  419. }
  420. export function arrsIntersect<T, K>(
  421. a: T[],
  422. b: K[],
  423. fn?: (item: K) => T
  424. ): boolean
  425. export function arrsIntersect<T>(a: T[], b: T[]): boolean
  426. export function arrsIntersect<T>(
  427. a: T[],
  428. b: unknown[],
  429. fn?: (item: unknown) => T
  430. ) {
  431. return a.some((item) => b.includes(fn ? fn(item) : item))
  432. }
  433. // /**
  434. // * Will mutate an array to remove items.
  435. // * @param arr
  436. // * @param item
  437. // */
  438. // export function pull<T>(arr: T[], ...items: T[]) {
  439. // for (let item of items) {
  440. // arr.splice(arr.indexOf(item), 1)
  441. // }
  442. // return arr
  443. // }
  444. // /**
  445. // * Will mutate an array to remove items, based on a function
  446. // * @param arr
  447. // * @param fn
  448. // * @returns
  449. // */
  450. // export function pullWith<T>(arr: T[], fn: (item: T) => boolean) {
  451. // pull(arr, ...arr.filter((item) => fn(item)))
  452. // return arr
  453. // }
  454. // export function rectContainsRect(
  455. // x0: number,
  456. // y0: number,
  457. // x1: number,
  458. // y1: number,
  459. // box: { x: number; y: number; width: number; height: number }
  460. // ) {
  461. // return !(
  462. // x0 > box.x ||
  463. // x1 < box.x + box.width ||
  464. // y0 > box.y ||
  465. // y1 < box.y + box.height
  466. // )
  467. // }
  468. export function getTouchDisplay() {
  469. return (
  470. "ontouchstart" in window ||
  471. navigator.maxTouchPoints > 0 ||
  472. navigator.msMaxTouchPoints > 0
  473. )
  474. }
  475. const rounds = [1, 10, 100, 1000]
  476. export function round(n: number, p = 2) {
  477. return Math.floor(n * rounds[p]) / rounds[p]
  478. }
  479. /**
  480. * Linear interpolation betwen two numbers.
  481. * @param y1
  482. * @param y2
  483. * @param mu
  484. */
  485. export function lerp(y1: number, y2: number, mu: number) {
  486. mu = clamp(mu, 0, 1)
  487. return y1 * (1 - mu) + y2 * mu
  488. }
  489. /**
  490. * Modulate a value between two ranges.
  491. * @param value
  492. * @param rangeA from [low, high]
  493. * @param rangeB to [low, high]
  494. * @param clamp
  495. */
  496. export function modulate(
  497. value: number,
  498. rangeA: number[],
  499. rangeB: number[],
  500. clamp = false
  501. ) {
  502. const [fromLow, fromHigh] = rangeA
  503. const [v0, v1] = rangeB
  504. const result = v0 + ((value - fromLow) / (fromHigh - fromLow)) * (v1 - v0)
  505. return clamp
  506. ? v0 < v1
  507. ? Math.max(Math.min(result, v1), v0)
  508. : Math.max(Math.min(result, v0), v1)
  509. : result
  510. }
  511. /**
  512. * Clamp a value into a range.
  513. * @param n
  514. * @param min
  515. */
  516. export function clamp(n: number, min: number): number
  517. export function clamp(n: number, min: number, max: number): number
  518. export function clamp(n: number, min: number, max?: number): number {
  519. return Math.max(min, typeof max !== "undefined" ? Math.min(n, max) : n)
  520. }
  521. // CURVES
  522. // Mostly adapted from https://github.com/Pomax/bezierjs
  523. export function computePointOnCurve(t: number, points: number[][]) {
  524. // shortcuts
  525. if (t === 0) {
  526. return points[0]
  527. }
  528. const order = points.length - 1
  529. if (t === 1) {
  530. return points[order]
  531. }
  532. const mt = 1 - t
  533. let p = points // constant?
  534. if (order === 0) {
  535. return points[0]
  536. } // linear?
  537. if (order === 1) {
  538. return [mt * p[0][0] + t * p[1][0], mt * p[0][1] + t * p[1][1]]
  539. } // quadratic/cubic curve?
  540. if (order < 4) {
  541. const mt2 = mt * mt,
  542. t2 = t * t
  543. let a: number,
  544. b: number,
  545. c: number,
  546. d = 0
  547. if (order === 2) {
  548. p = [p[0], p[1], p[2], [0, 0]]
  549. a = mt2
  550. b = mt * t * 2
  551. c = t2
  552. } else if (order === 3) {
  553. a = mt2 * mt
  554. b = mt2 * t * 3
  555. c = mt * t2 * 3
  556. d = t * t2
  557. }
  558. return [
  559. a * p[0][0] + b * p[1][0] + c * p[2][0] + d * p[3][0],
  560. a * p[0][1] + b * p[1][1] + c * p[2][1] + d * p[3][1],
  561. ]
  562. } // higher order curves: use de Casteljau's computation
  563. }
  564. function distance2(p: DOMPoint, point: number[]) {
  565. const dx = p.x - point[0],
  566. dy = p.y - point[1]
  567. return dx * dx + dy * dy
  568. }
  569. /**
  570. * Find the closest point on a path to an off-path point.
  571. * @param pathNode
  572. * @param point
  573. * @returns
  574. */
  575. export function getClosestPointOnPath(
  576. pathNode: SVGPathElement,
  577. point: number[]
  578. ) {
  579. const pathLen = pathNode.getTotalLength()
  580. let p = 8,
  581. best: DOMPoint,
  582. bestLen: number,
  583. bestDist = Infinity,
  584. bl: number,
  585. al: number
  586. // linear scan for coarse approximation
  587. for (
  588. let scan: DOMPoint, scanLen = 0, scanDist: number;
  589. scanLen <= pathLen;
  590. scanLen += p
  591. ) {
  592. if (
  593. (scanDist = distance2(
  594. (scan = pathNode.getPointAtLength(scanLen)),
  595. point
  596. )) < bestDist
  597. ) {
  598. ;(best = scan), (bestLen = scanLen), (bestDist = scanDist)
  599. }
  600. }
  601. // binary search for precise estimate
  602. p /= 2
  603. while (p > 0.5) {
  604. let before: DOMPoint, after: DOMPoint, bd: number, ad: number
  605. if (
  606. (bl = bestLen - p) >= 0 &&
  607. (bd = distance2((before = pathNode.getPointAtLength(bl)), point)) <
  608. bestDist
  609. ) {
  610. ;(best = before), (bestLen = bl), (bestDist = bd)
  611. } else if (
  612. (al = bestLen + p) <= pathLen &&
  613. (ad = distance2((after = pathNode.getPointAtLength(al)), point)) <
  614. bestDist
  615. ) {
  616. ;(best = after), (bestLen = al), (bestDist = ad)
  617. } else {
  618. p /= 2
  619. }
  620. }
  621. return {
  622. point: [best.x, best.y],
  623. distance: bestDist,
  624. length: (bl + al) / 2,
  625. t: (bl + al) / 2 / pathLen,
  626. }
  627. }
  628. export function det(
  629. a: number,
  630. b: number,
  631. c: number,
  632. d: number,
  633. e: number,
  634. f: number,
  635. g: number,
  636. h: number,
  637. i: number
  638. ) {
  639. return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g
  640. }
  641. /**
  642. * Get a circle from three points.
  643. * @param p0
  644. * @param p1
  645. * @param center
  646. * @returns
  647. */
  648. export function circleFromThreePoints(A: number[], B: number[], C: number[]) {
  649. const a = det(A[0], A[1], 1, B[0], B[1], 1, C[0], C[1], 1)
  650. const bx = -det(
  651. A[0] * A[0] + A[1] * A[1],
  652. A[1],
  653. 1,
  654. B[0] * B[0] + B[1] * B[1],
  655. B[1],
  656. 1,
  657. C[0] * C[0] + C[1] * C[1],
  658. C[1],
  659. 1
  660. )
  661. const by = det(
  662. A[0] * A[0] + A[1] * A[1],
  663. A[0],
  664. 1,
  665. B[0] * B[0] + B[1] * B[1],
  666. B[0],
  667. 1,
  668. C[0] * C[0] + C[1] * C[1],
  669. C[0],
  670. 1
  671. )
  672. const c = -det(
  673. A[0] * A[0] + A[1] * A[1],
  674. A[0],
  675. A[1],
  676. B[0] * B[0] + B[1] * B[1],
  677. B[0],
  678. B[1],
  679. C[0] * C[0] + C[1] * C[1],
  680. C[0],
  681. C[1]
  682. )
  683. return [
  684. -bx / (2 * a),
  685. -by / (2 * a),
  686. Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a)),
  687. ]
  688. }
  689. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  690. export function throttle<P extends any[], T extends (...args: P) => any>(
  691. fn: T,
  692. wait: number,
  693. preventDefault?: boolean
  694. ) {
  695. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  696. let inThrottle: boolean, lastFn: any, lastTime: number
  697. return function (...args: P) {
  698. if (preventDefault) args[0].preventDefault()
  699. // eslint-disable-next-line @typescript-eslint/no-this-alias
  700. const context = this
  701. if (!inThrottle) {
  702. fn.apply(context, args)
  703. lastTime = Date.now()
  704. inThrottle = true
  705. } else {
  706. clearTimeout(lastFn)
  707. lastFn = setTimeout(function () {
  708. if (Date.now() - lastTime >= wait) {
  709. fn.apply(context, args)
  710. lastTime = Date.now()
  711. }
  712. }, Math.max(wait - (Date.now() - lastTime), 0))
  713. }
  714. }
  715. }
  716. export function pointInRect(
  717. point: number[],
  718. minX: number,
  719. minY: number,
  720. maxX: number,
  721. maxY: number
  722. ) {
  723. return !(
  724. point[0] < minX ||
  725. point[0] > maxX ||
  726. point[1] < minY ||
  727. point[1] > maxY
  728. )
  729. }
  730. /**
  731. * Get the intersection of two rays, with origin points p0 and p1, and direction vectors n0 and n1.
  732. * @param p0 The origin point of the first ray
  733. * @param n0 The direction vector of the first ray
  734. * @param p1 The origin point of the second ray
  735. * @param n1 The direction vector of the second ray
  736. * @returns
  737. */
  738. export function getRayRayIntersection(
  739. p0: number[],
  740. n0: number[],
  741. p1: number[],
  742. n1: number[]
  743. ) {
  744. const p0e = vec.add(p0, n0),
  745. p1e = vec.add(p1, n1),
  746. m0 = (p0e[1] - p0[1]) / (p0e[0] - p0[0]),
  747. m1 = (p1e[1] - p1[1]) / (p1e[0] - p1[0]),
  748. b0 = p0[1] - m0 * p0[0],
  749. b1 = p1[1] - m1 * p1[0],
  750. x = (b1 - b0) / (m0 - m1),
  751. y = m0 * x + b0
  752. return [x, y]
  753. }
  754. export async function postJsonToEndpoint(
  755. endpoint: string,
  756. data: { [key: string]: unknown }
  757. ) {
  758. const d = await fetch(
  759. `${process.env.NEXT_PUBLIC_BASE_API_URL}/api/${endpoint}`,
  760. {
  761. method: "POST",
  762. headers: { "Content-Type": "application/json" },
  763. body: JSON.stringify(data),
  764. }
  765. )
  766. return await d.json()
  767. }
  768. export function getKeyboardEventInfo(e: KeyboardEvent | React.KeyboardEvent) {
  769. const { shiftKey, ctrlKey, metaKey, altKey } = e
  770. return {
  771. key: e.key,
  772. shiftKey,
  773. ctrlKey,
  774. metaKey: isDarwin() ? metaKey : ctrlKey,
  775. altKey,
  776. }
  777. }
  778. export function isDarwin() {
  779. return /Mac|iPod|iPhone|iPad/.test(window.navigator.platform)
  780. }
  781. export function metaKey(e: KeyboardEvent | React.KeyboardEvent) {
  782. return isDarwin() ? e.metaKey : e.ctrlKey
  783. }
  784. export function getTransformAnchor(
  785. type: TransformEdge | TransformCorner,
  786. isFlippedX: boolean,
  787. isFlippedY: boolean
  788. ) {
  789. let anchor: TransformCorner | TransformEdge = type
  790. // Change corner anchors if flipped
  791. switch (type) {
  792. case TransformCorner.TopLeft: {
  793. if (isFlippedX && isFlippedY) {
  794. anchor = TransformCorner.BottomRight
  795. } else if (isFlippedX) {
  796. anchor = TransformCorner.TopRight
  797. } else if (isFlippedY) {
  798. anchor = TransformCorner.BottomLeft
  799. }
  800. break
  801. }
  802. case TransformCorner.TopRight: {
  803. if (isFlippedX && isFlippedY) {
  804. anchor = TransformCorner.BottomLeft
  805. } else if (isFlippedX) {
  806. anchor = TransformCorner.TopLeft
  807. } else if (isFlippedY) {
  808. anchor = TransformCorner.BottomRight
  809. }
  810. break
  811. }
  812. case TransformCorner.BottomRight: {
  813. if (isFlippedX && isFlippedY) {
  814. anchor = TransformCorner.TopLeft
  815. } else if (isFlippedX) {
  816. anchor = TransformCorner.BottomLeft
  817. } else if (isFlippedY) {
  818. anchor = TransformCorner.TopRight
  819. }
  820. break
  821. }
  822. case TransformCorner.BottomLeft: {
  823. if (isFlippedX && isFlippedY) {
  824. anchor = TransformCorner.TopRight
  825. } else if (isFlippedX) {
  826. anchor = TransformCorner.BottomRight
  827. } else if (isFlippedY) {
  828. anchor = TransformCorner.TopLeft
  829. }
  830. break
  831. }
  832. }
  833. return anchor
  834. }
  835. export function vectorToPoint(point: number[] | Vector | undefined) {
  836. if (typeof point === "undefined") {
  837. return [0, 0]
  838. }
  839. if (point instanceof Vector) {
  840. return [point.x, point.y]
  841. }
  842. return point
  843. }
  844. export function getBoundsFromPoints(points: number[][]): Bounds {
  845. let minX = Infinity
  846. let minY = Infinity
  847. let maxX = -Infinity
  848. let maxY = -Infinity
  849. for (let [x, y] of points) {
  850. minX = Math.min(x, minX)
  851. minY = Math.min(y, minY)
  852. maxX = Math.max(x, maxX)
  853. maxY = Math.max(y, maxY)
  854. }
  855. return {
  856. minX,
  857. minY,
  858. maxX,
  859. maxY,
  860. width: maxX - minX,
  861. height: maxY - minY,
  862. }
  863. }
  864. /**
  865. * Move a bounding box without recalculating it.
  866. * @param bounds
  867. * @param delta
  868. * @returns
  869. */
  870. export function translateBounds(bounds: Bounds, delta: number[]) {
  871. return {
  872. minX: bounds.minX + delta[0],
  873. minY: bounds.minY + delta[1],
  874. maxX: bounds.maxX + delta[0],
  875. maxY: bounds.maxY + delta[1],
  876. width: bounds.width,
  877. height: bounds.height,
  878. }
  879. }
  880. export function rotateBounds(
  881. bounds: Bounds,
  882. center: number[],
  883. rotation: number
  884. ) {
  885. const [minX, minY] = vec.rotWith([bounds.minX, bounds.minY], center, rotation)
  886. const [maxX, maxY] = vec.rotWith([bounds.maxX, bounds.maxY], center, rotation)
  887. return {
  888. minX,
  889. minY,
  890. maxX,
  891. maxY,
  892. width: bounds.width,
  893. height: bounds.height,
  894. }
  895. }
  896. export function getRotatedCorners(b: Bounds, rotation: number) {
  897. const center = [b.minX + b.width / 2, b.minY + b.height / 2]
  898. return [
  899. [b.minX, b.minY],
  900. [b.maxX, b.minY],
  901. [b.maxX, b.maxY],
  902. [b.minX, b.maxY],
  903. ].map((point) => vec.rotWith(point, center, rotation))
  904. }
  905. export function getTransformedBoundingBox(
  906. bounds: Bounds,
  907. handle: TransformCorner | TransformEdge,
  908. delta: number[],
  909. rotation = 0
  910. ) {
  911. // Create top left and bottom right corners.
  912. let [ax0, ay0] = [bounds.minX, bounds.minY]
  913. let [ax1, ay1] = [bounds.maxX, bounds.maxY]
  914. // Create a second set of corners for the result.
  915. let [bx0, by0] = [bounds.minX, bounds.minY]
  916. let [bx1, by1] = [bounds.maxX, bounds.maxY]
  917. // Counter rotate the delta. This lets us make changes as if
  918. // the (possibly rotated) boxes were axis aligned.
  919. const [dx, dy] = vec.rot(delta, -rotation)
  920. // Depending on the dragging handle (an edge or corner of
  921. // the bounding box), find the anchor corner and use the delta
  922. // to adjust the result's corners.
  923. let anchor: TransformCorner | TransformEdge
  924. switch (handle) {
  925. case TransformEdge.Top: {
  926. anchor = TransformCorner.BottomRight
  927. by0 += dy
  928. break
  929. }
  930. case TransformEdge.Right: {
  931. anchor = TransformCorner.TopLeft
  932. bx1 += dx
  933. break
  934. }
  935. case TransformEdge.Bottom: {
  936. anchor = TransformCorner.TopLeft
  937. by1 += dy
  938. break
  939. }
  940. case TransformEdge.Left: {
  941. anchor = TransformCorner.BottomRight
  942. bx0 += dx
  943. break
  944. }
  945. case TransformCorner.TopLeft: {
  946. anchor = TransformCorner.BottomRight
  947. bx0 += dx
  948. by0 += dy
  949. break
  950. }
  951. case TransformCorner.TopRight: {
  952. anchor = TransformCorner.BottomLeft
  953. bx1 += dx
  954. by0 += dy
  955. break
  956. }
  957. case TransformCorner.BottomRight: {
  958. anchor = TransformCorner.TopLeft
  959. bx1 += dx
  960. by1 += dy
  961. break
  962. }
  963. case TransformCorner.BottomLeft: {
  964. anchor = TransformCorner.TopRight
  965. bx0 += dx
  966. by1 += dy
  967. break
  968. }
  969. }
  970. // If the bounds are rotated, get a vector from the rotated anchor
  971. // corner in the inital bounds to the rotated anchor corner in the
  972. // result's bounds. Subtract this vector from the result's corners,
  973. // so that the two anchor points (initial and result) will be equal.
  974. if (rotation % (Math.PI * 2) !== 0) {
  975. let cv = [0, 0]
  976. const c0 = vec.med([ax0, ay0], [ax1, ay1])
  977. const c1 = vec.med([bx0, by0], [bx1, by1])
  978. switch (anchor) {
  979. case TransformCorner.TopLeft: {
  980. cv = vec.sub(
  981. vec.rotWith([bx0, by0], c1, rotation),
  982. vec.rotWith([ax0, ay0], c0, rotation)
  983. )
  984. break
  985. }
  986. case TransformCorner.TopRight: {
  987. cv = vec.sub(
  988. vec.rotWith([bx1, by0], c1, rotation),
  989. vec.rotWith([ax1, ay0], c0, rotation)
  990. )
  991. break
  992. }
  993. case TransformCorner.BottomRight: {
  994. cv = vec.sub(
  995. vec.rotWith([bx1, by1], c1, rotation),
  996. vec.rotWith([ax1, ay1], c0, rotation)
  997. )
  998. break
  999. }
  1000. case TransformCorner.BottomLeft: {
  1001. cv = vec.sub(
  1002. vec.rotWith([bx0, by1], c1, rotation),
  1003. vec.rotWith([ax0, ay1], c0, rotation)
  1004. )
  1005. break
  1006. }
  1007. }
  1008. ;[bx0, by0] = vec.sub([bx0, by0], cv)
  1009. ;[bx1, by1] = vec.sub([bx1, by1], cv)
  1010. }
  1011. // If the axes are flipped (e.g. if the right edge has been dragged
  1012. // left past the initial left edge) then swap points on that axis.
  1013. if (bx1 < bx0) {
  1014. ;[bx1, bx0] = [bx0, bx1]
  1015. }
  1016. if (by1 < by0) {
  1017. ;[by1, by0] = [by0, by1]
  1018. }
  1019. return {
  1020. minX: bx0,
  1021. minY: by0,
  1022. maxX: bx1,
  1023. maxY: by1,
  1024. width: bx1 - bx0,
  1025. height: by1 - by0,
  1026. }
  1027. }