Explorar el Código

Another good stopping point

main
Steve Ruiz hace 4 años
padre
commit
e7a52dd70f

+ 34
- 0
components/canvas/canvas.tsx Ver fichero

@@ -0,0 +1,34 @@
1
+import styled from "styles"
2
+import { useRef } from "react"
3
+import useZoomEvents from "hooks/useZoomEvents"
4
+import useZoomPanEffect from "hooks/useZoomPanEffect"
5
+
6
+export default function Canvas() {
7
+  const rCanvas = useRef<SVGSVGElement>(null)
8
+  const rGroup = useRef<SVGGElement>(null)
9
+  const events = useZoomEvents(rCanvas)
10
+
11
+  useZoomPanEffect(rGroup)
12
+
13
+  return (
14
+    <MainSVG ref={rCanvas} {...events}>
15
+      <MainGroup ref={rGroup}>
16
+        <circle cx={100} cy={100} r={50} />
17
+        <circle cx={500} cy={500} r={200} />
18
+        <circle cx={200} cy={800} r={100} />
19
+      </MainGroup>
20
+    </MainSVG>
21
+  )
22
+}
23
+
24
+const MainSVG = styled("svg", {
25
+  position: "fixed",
26
+  top: 0,
27
+  left: 0,
28
+  width: "100%",
29
+  height: "100%",
30
+  touchAction: "none",
31
+  zIndex: 100,
32
+})
33
+
34
+const MainGroup = styled("g", {})

+ 11
- 0
components/editor.tsx Ver fichero

@@ -0,0 +1,11 @@
1
+import Canvas from "./canvas/canvas"
2
+import StatusBar from "./status-bar"
3
+
4
+export default function Editor() {
5
+  return (
6
+    <>
7
+      <Canvas />
8
+      <StatusBar />
9
+    </>
10
+  )
11
+}

+ 62
- 0
components/status-bar.tsx Ver fichero

@@ -0,0 +1,62 @@
1
+import { useStateDesigner } from "@state-designer/react"
2
+import state from "state"
3
+import styled from "styles"
4
+import { useRef } from "react"
5
+
6
+export default function StatusBar() {
7
+  const local = useStateDesigner(state)
8
+  const { count, time } = useRenderCount()
9
+
10
+  const active = local.active.slice(1).map((s) => s.split("root.")[1])
11
+  const log = local.log[0]
12
+
13
+  return (
14
+    <StatusBarContainer>
15
+      <States>{active.join(" | ")}</States>
16
+      <Section>| {log}</Section>
17
+      <Section title="Renders | Time">
18
+        {count} | {time.toString().padStart(3, "0")}
19
+      </Section>
20
+    </StatusBarContainer>
21
+  )
22
+}
23
+
24
+const StatusBarContainer = styled("div", {
25
+  position: "absolute",
26
+  bottom: 0,
27
+  left: 0,
28
+  width: "100%",
29
+  height: 40,
30
+  userSelect: "none",
31
+  borderTop: "1px solid black",
32
+  gridArea: "status",
33
+  display: "grid",
34
+  gridTemplateColumns: "auto 1fr auto",
35
+  alignItems: "center",
36
+  backgroundColor: "white",
37
+  gap: 8,
38
+  padding: "0 16px",
39
+  zIndex: 200,
40
+})
41
+
42
+const Section = styled("div", {
43
+  whiteSpace: "nowrap",
44
+  overflow: "hidden",
45
+})
46
+
47
+const States = styled("div", {})
48
+
49
+function useRenderCount() {
50
+  const rTime = useRef(Date.now())
51
+  const rCounter = useRef(0)
52
+
53
+  rCounter.current++
54
+  const now = Date.now()
55
+  let time = now - rTime.current
56
+  if (time > 100) {
57
+    time = 0
58
+  }
59
+  rTime.current = now
60
+
61
+  return { count: rCounter.current, time }
62
+}

+ 61
- 0
hooks/useZoomEvents.ts Ver fichero

@@ -0,0 +1,61 @@
1
+import React, { useEffect, useRef } from "react"
2
+import state from "state"
3
+import * as vec from "utils/vec"
4
+
5
+export default function useZoomEvents(
6
+  ref: React.MutableRefObject<SVGSVGElement>
7
+) {
8
+  const rTouchDist = useRef(0)
9
+
10
+  useEffect(() => {
11
+    const element = ref.current
12
+
13
+    if (!element) return
14
+
15
+    function handleWheel(e: WheelEvent) {
16
+      e.preventDefault()
17
+
18
+      if (e.ctrlKey) {
19
+        state.send("ZOOMED_CAMERA", {
20
+          delta: e.deltaY,
21
+          point: [e.pageX, e.pageY],
22
+        })
23
+        return
24
+      }
25
+
26
+      state.send("PANNED_CAMERA", {
27
+        delta: [e.deltaX, e.deltaY],
28
+        point: [e.pageX, e.pageY],
29
+      })
30
+    }
31
+
32
+    function handleTouchMove(e: TouchEvent) {
33
+      if (e.ctrlKey) {
34
+        e.preventDefault()
35
+      }
36
+
37
+      if (e.touches.length === 2) {
38
+        const { clientX: x0, clientY: y0 } = e.touches[0]
39
+        const { clientX: x1, clientY: y1 } = e.touches[1]
40
+
41
+        const dist = vec.dist([x0, y0], [x1, y1])
42
+
43
+        state.send("WHEELED", { delta: [0, dist - rTouchDist.current] })
44
+
45
+        rTouchDist.current = dist
46
+      }
47
+    }
48
+
49
+    element.addEventListener("wheel", handleWheel)
50
+    element.addEventListener("touchstart", handleTouchMove)
51
+    element.addEventListener("touchmove", handleTouchMove)
52
+
53
+    return () => {
54
+      element.removeEventListener("wheel", handleWheel)
55
+      element.removeEventListener("touchstart", handleTouchMove)
56
+      element.removeEventListener("touchmove", handleTouchMove)
57
+    }
58
+  }, [ref])
59
+
60
+  return {}
61
+}

+ 32
- 0
hooks/useZoomPanEffect.ts Ver fichero

@@ -0,0 +1,32 @@
1
+import React, { useEffect } from "react"
2
+import state from "state"
3
+
4
+/**
5
+ * When the state's camera changes, update the transform of
6
+ * the SVG group to reflect the correct zoom and pan.
7
+ * @param ref
8
+ */
9
+export default function useZoomPanEffect(
10
+  ref: React.MutableRefObject<SVGGElement>
11
+) {
12
+  useEffect(() => {
13
+    let { camera } = state.data
14
+
15
+    return state.onUpdate(({ data }) => {
16
+      const g = ref.current
17
+      if (!g) return
18
+
19
+      const { point, zoom } = data.camera
20
+
21
+      if (point !== camera.point || zoom !== camera.zoom) {
22
+        console.log("changed!")
23
+        g.setAttribute(
24
+          "transform",
25
+          `scale(${zoom}) translate(${point[0]} ${point[1]})`
26
+        )
27
+      }
28
+
29
+      camera = data.camera
30
+    })
31
+  }, [state])
32
+}

+ 1
- 0
pages/_app.tsx Ver fichero

@@ -1,5 +1,6 @@
1 1
 import { AppProps } from "next/app"
2 2
 import { globalStyles } from "styles"
3
+import "styles/globals.css"
3 4
 
4 5
 function MyApp({ Component, pageProps }: AppProps) {
5 6
   globalStyles()

+ 1
- 3
pages/_document.tsx Ver fichero

@@ -16,9 +16,7 @@ class MyDocument extends Document {
16 16
   render() {
17 17
     return (
18 18
       <Html>
19
-        <Head>
20
-          <title>ScribbleScript</title>
21
-        </Head>
19
+        <Head />
22 20
         <body className={dark}>
23 21
           <Main />
24 22
           <NextScript />

+ 6
- 1
pages/index.tsx Ver fichero

@@ -1,6 +1,11 @@
1 1
 import Head from "next/head"
2 2
 import Image from "next/image"
3
+import Editor from "components/editor"
3 4
 
4 5
 export default function Home() {
5
-  return <div></div>
6
+  return (
7
+    <div>
8
+      <Editor />
9
+    </div>
10
+  )
6 11
 }

+ 5
- 0
state/index.ts Ver fichero

@@ -0,0 +1,5 @@
1
+import state, { useSelector } from "./state"
2
+
3
+export default state
4
+
5
+export { useSelector }

+ 41
- 1
state/state.ts Ver fichero

@@ -1,6 +1,46 @@
1 1
 import { createSelectorHook, createState } from "@state-designer/react"
2
+import * as vec from "utils/vec"
3
+import { clamp, screenToWorld } from "utils/utils"
4
+import { IData } from "types"
2 5
 
3
-const state = createState({})
6
+const initialData: IData = {
7
+  camera: {
8
+    point: [0, 0],
9
+    zoom: 1,
10
+  },
11
+}
12
+
13
+const state = createState({
14
+  data: initialData,
15
+  on: {
16
+    ZOOMED_CAMERA: {
17
+      do: "zoomCamera",
18
+    },
19
+    PANNED_CAMERA: {
20
+      do: "panCamera",
21
+    },
22
+  },
23
+  actions: {
24
+    zoomCamera(data, payload: { delta: number; point: number[] }) {
25
+      const { camera } = data
26
+      const p0 = screenToWorld(payload.point, data)
27
+      camera.zoom = clamp(
28
+        camera.zoom - (payload.delta / 100) * camera.zoom,
29
+        0.5,
30
+        3
31
+      )
32
+      const p1 = screenToWorld(payload.point, data)
33
+      camera.point = vec.add(camera.point, vec.sub(p1, p0))
34
+    },
35
+    panCamera(data, payload: { delta: number[]; point: number[] }) {
36
+      const { camera } = data
37
+      data.camera.point = vec.sub(
38
+        camera.point,
39
+        vec.div(payload.delta, camera.zoom)
40
+      )
41
+    },
42
+  },
43
+})
4 44
 
5 45
 export default state
6 46
 

+ 10
- 0
styles/globals.css Ver fichero

@@ -0,0 +1,10 @@
1
+* {
2
+  box-sizing: border-box;
3
+}
4
+
5
+html,
6
+body {
7
+  margin: 0;
8
+  padding: 0;
9
+  overscroll-behavior: none;
10
+}

+ 5
- 0
styles/stitches.config.ts Ver fichero

@@ -25,6 +25,11 @@ const dark = theme({})
25 25
 
26 26
 const globalStyles = global({
27 27
   "*": { boxSizing: "border-box" },
28
+  "html, body": {
29
+    padding: "0",
30
+    margin: "0",
31
+    overscrollBehavior: "none",
32
+  },
28 33
 })
29 34
 
30 35
 export default styled

+ 6
- 0
types.ts Ver fichero

@@ -0,0 +1,6 @@
1
+export interface IData {
2
+  camera: {
3
+    point: number[]
4
+    zoom: number
5
+  }
6
+}

+ 63
- 0
utils/svg.ts Ver fichero

@@ -0,0 +1,63 @@
1
+// Some helpers for drawing SVGs.
2
+
3
+import * as vec from "./vec"
4
+import { getSweep } from "utils/utils"
5
+
6
+// General
7
+
8
+export function ellipse(A: number[], r: number) {
9
+  return `M ${A[0] - r},${A[1]}
10
+      a ${r},${r} 0 1,0 ${r * 2},0
11
+      a ${r},${r} 0 1,0 -${r * 2},0 `
12
+}
13
+
14
+export function moveTo(v: number[]) {
15
+  return `M ${v[0]},${v[1]} `
16
+}
17
+
18
+export function lineTo(v: number[]) {
19
+  return `L ${v[0]},${v[1]} `
20
+}
21
+
22
+export function line(a: number[], ...pts: number[][]) {
23
+  return moveTo(a) + pts.map((p) => lineTo(p)).join()
24
+}
25
+
26
+export function hLineTo(v: number[]) {
27
+  return `H ${v[0]},${v[1]} `
28
+}
29
+
30
+export function vLineTo(v: number[]) {
31
+  return `V ${v[0]},${v[1]} `
32
+}
33
+
34
+export function bezierTo(A: number[], B: number[], C: number[]) {
35
+  return `C ${A[0]},${A[1]} ${B[0]},${B[1]} ${C[0]},${C[1]} `
36
+}
37
+
38
+export function arcTo(C: number[], r: number, A: number[], B: number[]) {
39
+  return [
40
+    // moveTo(A),
41
+    "A",
42
+    r,
43
+    r,
44
+    0,
45
+    getSweep(C, A, B) > 0 ? "1" : "0",
46
+    0,
47
+    B[0],
48
+    B[1],
49
+  ].join(" ")
50
+}
51
+
52
+export function closePath() {
53
+  return "Z"
54
+}
55
+
56
+export function rectTo(A: number[]) {
57
+  return ["R", A[0], A[1]].join(" ")
58
+}
59
+
60
+export function getPointAtLength(path: SVGPathElement, length: number) {
61
+  const point = path.getPointAtLength(length)
62
+  return [point.x, point.y]
63
+}

+ 829
- 0
utils/utils.ts Ver fichero

@@ -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
+}

+ 459
- 0
utils/vec.ts Ver fichero

@@ -0,0 +1,459 @@
1
+/**
2
+ * Clamp a value into a range.
3
+ * @param n
4
+ * @param min
5
+ */
6
+export function clamp(n: number, min: number): number
7
+export function clamp(n: number, min: number, max: number): number
8
+export function clamp(n: number, min: number, max?: number): number {
9
+  return Math.max(min, typeof max !== "undefined" ? Math.min(n, max) : n)
10
+}
11
+
12
+/**
13
+ * Negate a vector.
14
+ * @param A
15
+ */
16
+export function neg(A: number[]) {
17
+  return [-A[0], -A[1]]
18
+}
19
+
20
+/**
21
+ * Add vectors.
22
+ * @param A
23
+ * @param B
24
+ */
25
+export function add(A: number[], B: number[]) {
26
+  return [A[0] + B[0], A[1] + B[1]]
27
+}
28
+
29
+/**
30
+ * Subtract vectors.
31
+ * @param A
32
+ * @param B
33
+ */
34
+export function sub(A: number[], B: number[]) {
35
+  return [A[0] - B[0], A[1] - B[1]]
36
+}
37
+
38
+/**
39
+ * Get the vector from vectors A to B.
40
+ * @param A
41
+ * @param B
42
+ */
43
+export function vec(A: number[], B: number[]) {
44
+  // A, B as vectors get the vector from A to B
45
+  return [B[0] - A[0], B[1] - A[1]]
46
+}
47
+
48
+/**
49
+ * Vector multiplication by scalar
50
+ * @param A
51
+ * @param n
52
+ */
53
+export function mul(A: number[], n: number) {
54
+  return [A[0] * n, A[1] * n]
55
+}
56
+
57
+export function mulV(A: number[], B: number[]) {
58
+  return [A[0] * B[0], A[1] * B[1]]
59
+}
60
+
61
+/**
62
+ * Vector division by scalar.
63
+ * @param A
64
+ * @param n
65
+ */
66
+export function div(A: number[], n: number) {
67
+  return [A[0] / n, A[1] / n]
68
+}
69
+
70
+/**
71
+ * Vector division by vector.
72
+ * @param A
73
+ * @param n
74
+ */
75
+export function divV(A: number[], B: number[]) {
76
+  return [A[0] / B[0], A[1] / B[1]]
77
+}
78
+
79
+/**
80
+ * Perpendicular rotation of a vector A
81
+ * @param A
82
+ */
83
+export function per(A: number[]) {
84
+  return [A[1], -A[0]]
85
+}
86
+
87
+/**
88
+ * Dot product
89
+ * @param A
90
+ * @param B
91
+ */
92
+export function dpr(A: number[], B: number[]) {
93
+  return A[0] * B[0] + A[1] * B[1]
94
+}
95
+
96
+/**
97
+ * Cross product (outer product) | A X B |
98
+ * @param A
99
+ * @param B
100
+ */
101
+export function cpr(A: number[], B: number[]) {
102
+  return A[0] * B[1] - B[0] * A[1]
103
+}
104
+
105
+/**
106
+ * Length of the vector squared
107
+ * @param A
108
+ */
109
+export function len2(A: number[]) {
110
+  return A[0] * A[0] + A[1] * A[1]
111
+}
112
+
113
+/**
114
+ * Length of the vector
115
+ * @param A
116
+ */
117
+export function len(A: number[]) {
118
+  return Math.hypot(A[0], A[1])
119
+}
120
+
121
+/**
122
+ * Project A over B
123
+ * @param A
124
+ * @param B
125
+ */
126
+export function pry(A: number[], B: number[]) {
127
+  return dpr(A, B) / len(B)
128
+}
129
+
130
+/**
131
+ * Get normalized / unit vector.
132
+ * @param A
133
+ */
134
+export function uni(A: number[]) {
135
+  return div(A, len(A))
136
+}
137
+
138
+/**
139
+ * Get normalized / unit vector.
140
+ * @param A
141
+ */
142
+export function normalize(A: number[]) {
143
+  return uni(A)
144
+}
145
+
146
+/**
147
+ * Get the tangent between two vectors.
148
+ * @param A
149
+ * @param B
150
+ * @returns
151
+ */
152
+export function tangent(A: number[], B: number[]) {
153
+  return normalize(sub(A, B))
154
+}
155
+
156
+/**
157
+ * Dist length from A to B squared.
158
+ * @param A
159
+ * @param B
160
+ */
161
+export function dist2(A: number[], B: number[]) {
162
+  return len2(sub(A, B))
163
+}
164
+
165
+/**
166
+ * Dist length from A to B
167
+ * @param A
168
+ * @param B
169
+ */
170
+export function dist(A: number[], B: number[]) {
171
+  return Math.hypot(A[1] - B[1], A[0] - B[0])
172
+}
173
+
174
+/**
175
+ * A faster, though less accurate method for testing distances. Maybe faster?
176
+ * @param A
177
+ * @param B
178
+ * @returns
179
+ */
180
+export function fastDist(A: number[], B: number[]) {
181
+  const V = [B[0] - A[0], B[1] - A[1]]
182
+  const aV = [Math.abs(V[0]), Math.abs(V[1])]
183
+  let r = 1 / Math.max(aV[0], aV[1])
184
+  r = r * (1.29289 - (aV[0] + aV[1]) * r * 0.29289)
185
+  return [V[0] * r, V[1] * r]
186
+}
187
+
188
+/**
189
+ * Angle between vector A and vector B in radians
190
+ * @param A
191
+ * @param B
192
+ */
193
+export function ang(A: number[], B: number[]) {
194
+  return Math.atan2(cpr(A, B), dpr(A, B))
195
+}
196
+
197
+/**
198
+ * Angle between vector A and vector B in radians
199
+ * @param A
200
+ * @param B
201
+ */
202
+export function angle(A: number[], B: number[]) {
203
+  return Math.atan2(B[1] - A[1], B[0] - A[0])
204
+}
205
+
206
+/**
207
+ * Mean between two vectors or mid vector between two vectors
208
+ * @param A
209
+ * @param B
210
+ */
211
+export function med(A: number[], B: number[]) {
212
+  return mul(add(A, B), 0.5)
213
+}
214
+
215
+/**
216
+ * Vector rotation by r (radians)
217
+ * @param A
218
+ * @param r rotation in radians
219
+ */
220
+export function rot(A: number[], r: number) {
221
+  return [
222
+    A[0] * Math.cos(r) - A[1] * Math.sin(r),
223
+    A[0] * Math.sin(r) + A[1] * Math.cos(r),
224
+  ]
225
+}
226
+
227
+/**
228
+ * Rotate a vector around another vector by r (radians)
229
+ * @param A vector
230
+ * @param C center
231
+ * @param r rotation in radians
232
+ */
233
+export function rotWith(A: number[], C: number[], r: number) {
234
+  const s = Math.sin(r)
235
+  const c = Math.cos(r)
236
+
237
+  const px = A[0] - C[0]
238
+  const py = A[1] - C[1]
239
+
240
+  const nx = px * c - py * s
241
+  const ny = px * s + py * c
242
+
243
+  return [nx + C[0], ny + C[1]]
244
+}
245
+
246
+/**
247
+ * Check of two vectors are identical.
248
+ * @param A
249
+ * @param B
250
+ */
251
+export function isEqual(A: number[], B: number[]) {
252
+  return A[0] === B[0] && A[1] === B[1]
253
+}
254
+
255
+/**
256
+ * Interpolate vector A to B with a scalar t
257
+ * @param A
258
+ * @param B
259
+ * @param t scalar
260
+ */
261
+export function lrp(A: number[], B: number[], t: number) {
262
+  return add(A, mul(vec(A, B), t))
263
+}
264
+
265
+/**
266
+ * Interpolate from A to B when curVAL goes fromVAL => to
267
+ * @param A
268
+ * @param B
269
+ * @param from Starting value
270
+ * @param to Ending value
271
+ * @param s Strength
272
+ */
273
+export function int(A: number[], B: number[], from: number, to: number, s = 1) {
274
+  const t = (clamp(from, to) - from) / (to - from)
275
+  return add(mul(A, 1 - t), mul(B, s))
276
+}
277
+
278
+/**
279
+ * Get the angle between the three vectors A, B, and C.
280
+ * @param p1
281
+ * @param pc
282
+ * @param p2
283
+ */
284
+export function ang3(p1: number[], pc: number[], p2: number[]) {
285
+  // this,
286
+  const v1 = vec(pc, p1)
287
+  const v2 = vec(pc, p2)
288
+  return ang(v1, v2)
289
+}
290
+
291
+/**
292
+ * Absolute value of a vector.
293
+ * @param A
294
+ * @returns
295
+ */
296
+export function abs(A: number[]) {
297
+  return [Math.abs(A[0]), Math.abs(A[1])]
298
+}
299
+
300
+export function rescale(a: number[], n: number) {
301
+  const l = len(a)
302
+  return [(n * a[0]) / l, (n * a[1]) / l]
303
+}
304
+
305
+/**
306
+ * Get whether p1 is left of p2, relative to pc.
307
+ * @param p1
308
+ * @param pc
309
+ * @param p2
310
+ */
311
+export function isLeft(p1: number[], pc: number[], p2: number[]) {
312
+  //  isLeft: >0 for counterclockwise
313
+  //          =0 for none (degenerate)
314
+  //          <0 for clockwise
315
+  return (pc[0] - p1[0]) * (p2[1] - p1[1]) - (p2[0] - p1[0]) * (pc[1] - p1[1])
316
+}
317
+
318
+export function clockwise(p1: number[], pc: number[], p2: number[]) {
319
+  return isLeft(p1, pc, p2) > 0
320
+}
321
+
322
+const rounds = [1, 10, 100, 1000]
323
+
324
+export function round(a: number[], d = 2) {
325
+  return [
326
+    Math.round(a[0] * rounds[d]) / rounds[d],
327
+    Math.round(a[1] * rounds[d]) / rounds[d],
328
+  ]
329
+}
330
+
331
+/**
332
+ * Get the minimum distance from a point P to a line with a segment AB.
333
+ * @param A The start of the line.
334
+ * @param B The end of the line.
335
+ * @param P A point.
336
+ * @returns
337
+ */
338
+// export function distanceToLine(A: number[], B: number[], P: number[]) {
339
+//   const delta = sub(B, A)
340
+//   const angle = Math.atan2(delta[1], delta[0])
341
+//   const dir = rot(sub(P, A), -angle)
342
+//   return dir[1]
343
+// }
344
+
345
+/**
346
+ * Get the nearest point on a line segment AB.
347
+ * @param A The start of the line.
348
+ * @param B The end of the line.
349
+ * @param P A point.
350
+ * @param clamp Whether to clamp the resulting point to the segment.
351
+ * @returns
352
+ */
353
+// export function nearestPointOnLine(
354
+//   A: number[],
355
+//   B: number[],
356
+//   P: number[],
357
+//   clamp = true
358
+// ) {
359
+//   const delta = sub(B, A)
360
+//   const length = len(delta)
361
+//   const angle = Math.atan2(delta[1], delta[0])
362
+//   const dir = rot(sub(P, A), -angle)
363
+
364
+//   if (clamp) {
365
+//     if (dir[0] < 0) return A
366
+//     if (dir[0] > length) return B
367
+//   }
368
+
369
+//   return add(A, div(mul(delta, dir[0]), length))
370
+// }
371
+
372
+/**
373
+ * Get the nearest point on a line with a known unit vector that passes through point A
374
+ * @param A Any point on the line
375
+ * @param u The unit vector for the line.
376
+ * @param P A point not on the line to test.
377
+ * @returns
378
+ */
379
+export function nearestPointOnLineThroughPoint(
380
+  A: number[],
381
+  u: number[],
382
+  P: number[]
383
+) {
384
+  return add(A, mul(u, pry(sub(P, A), u)))
385
+}
386
+
387
+/**
388
+ * Distance between a point and a line with a known unit vector that passes through a point.
389
+ * @param A Any point on the line
390
+ * @param u The unit vector for the line.
391
+ * @param P A point not on the line to test.
392
+ * @returns
393
+ */
394
+export function distanceToLineThroughPoint(
395
+  A: number[],
396
+  u: number[],
397
+  P: number[]
398
+) {
399
+  return dist(P, nearestPointOnLineThroughPoint(A, u, P))
400
+}
401
+
402
+/**
403
+ * Get the nearest point on a line segment between A and B
404
+ * @param A The start of the line segment
405
+ * @param B The end of the line segment
406
+ * @param P The off-line point
407
+ * @param clamp Whether to clamp the point between A and B.
408
+ * @returns
409
+ */
410
+export function nearestPointOnLineSegment(
411
+  A: number[],
412
+  B: number[],
413
+  P: number[],
414
+  clamp = true
415
+) {
416
+  const delta = sub(B, A)
417
+  const length = len(delta)
418
+  const u = div(delta, length)
419
+
420
+  const pt = add(A, mul(u, pry(sub(P, A), u)))
421
+
422
+  if (clamp) {
423
+    const da = dist(A, pt)
424
+    const db = dist(B, pt)
425
+
426
+    if (db < da && da > length) return B
427
+    if (da < db && db > length) return A
428
+  }
429
+
430
+  return pt
431
+}
432
+
433
+/**
434
+ * Distance between a point and the nearest point on a line segment between A and B
435
+ * @param A The start of the line segment
436
+ * @param B The end of the line segment
437
+ * @param P The off-line point
438
+ * @param clamp Whether to clamp the point between A and B.
439
+ * @returns
440
+ */
441
+export function distanceToLineSegment(
442
+  A: number[],
443
+  B: number[],
444
+  P: number[],
445
+  clamp = true
446
+) {
447
+  return dist(P, nearestPointOnLineSegment(A, B, P, clamp))
448
+}
449
+
450
+/**
451
+ * Get a vector d distance from A towards B.
452
+ * @param A
453
+ * @param B
454
+ * @param d
455
+ * @returns
456
+ */
457
+export function nudge(A: number[], B: number[], d: number) {
458
+  return add(A, mul(uni(vec(A, B)), d))
459
+}

Loading…
Cancelar
Guardar