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