|
| 1 | +import type Point from './Point'; |
| 2 | + |
1 | 3 | /**
|
2 |
| - * Given two points and a length, calculate and return a catenary curve. |
3 |
| - * |
4 |
| - * JavaScript implementation: |
5 |
| - * Copyright (c) 2018 Jan Hug <me@dulnan.net> |
6 |
| - * Released under the MIT license. |
7 |
| - * |
8 |
| - * @example |
9 |
| - * // Initialize the catenary |
10 |
| - * const chain = new Catenary(); |
11 |
| - * // Calculate an svg catenary curve path |
12 |
| - * const path = chain.getPath() |
| 4 | + * Get an SVG quadratic bézier curve path based simulating a catenary curve |
| 5 | + * @param p1 - Line Start point |
| 6 | + * @param p2 - Line End point |
13 | 7 | *
|
14 | 8 | * @category Helpers
|
15 | 9 | */
|
16 |
| -import Point from './Point'; |
17 |
| - |
18 |
| -class Catenary { |
19 |
| - private p1 = new Point(0, 0); |
20 |
| - private p2 = new Point(0, 0); |
21 |
| - private segments: number; |
22 |
| - private iterationLimit: number; |
23 |
| - |
24 |
| - constructor(segments = 50, iterationLimit = 100) { |
25 |
| - this.segments = segments; |
26 |
| - this.iterationLimit = iterationLimit; |
27 |
| - } |
28 |
| - |
29 |
| - /** |
30 |
| - * Draws a catenary given two points |
31 |
| - */ |
32 |
| - public getPath(point1: Point, point2: Point): string { |
33 |
| - this.p1.update(point1); |
34 |
| - this.p2.update(point2); |
35 |
| - |
36 |
| - const isFlipped = this.p1.x > this.p2.x; |
37 |
| - |
38 |
| - const p1 = isFlipped ? this.p2 : this.p1; |
39 |
| - const p2 = isFlipped ? this.p1 : this.p2; |
40 |
| - |
41 |
| - const distance = p1.getDistanceTo(p2); |
42 |
| - |
43 |
| - let curveData: number[][] | number[] = []; |
44 |
| - let isStraight = true; |
45 |
| - |
46 |
| - let chainLength = 300; |
47 |
| - |
48 |
| - switch (true) { |
49 |
| - case distance < 400: |
50 |
| - chainLength = 420; |
51 |
| - break; |
52 |
| - case distance < 900: |
53 |
| - chainLength = 940; |
54 |
| - break; |
55 |
| - case distance < 1400: |
56 |
| - chainLength = 1440; |
57 |
| - break; |
58 |
| - default: |
59 |
| - chainLength = distance * 1.05; |
60 |
| - } |
61 |
| - |
62 |
| - if (distance < chainLength) { |
63 |
| - const diff = p2.x - p1.x; |
64 |
| - |
65 |
| - if (diff > 0.01) { |
66 |
| - const h = p2.x - p1.x; |
67 |
| - const v = p2.y - p1.y; |
68 |
| - const a = -this.getCatenaryParameter(h, v, chainLength, this.iterationLimit); |
69 |
| - const x = (a * Math.log((chainLength + v) / (chainLength - v)) - h) * 0.5; |
70 |
| - const y = a * Math.cosh(x / a); |
71 |
| - const offsetX = p1.x - x; |
72 |
| - const offsetY = p1.y - y; |
73 |
| - curveData = this.getCurve(a, p1, p2, offsetX, offsetY, this.segments); |
74 |
| - isStraight = false; |
75 |
| - } else { |
76 |
| - const mx = (p1.x + p2.x) * 0.5; |
77 |
| - const my = (p1.y + p2.y + chainLength) * 0.5; |
78 |
| - |
79 |
| - curveData = [ |
80 |
| - [p1.x, p1.y], |
81 |
| - [mx, my], |
82 |
| - [p2.x, p2.y], |
83 |
| - ]; |
84 |
| - } |
85 |
| - } else { |
86 |
| - curveData = [ |
87 |
| - [p1.x, p1.y], |
88 |
| - [p2.x, p2.y], |
89 |
| - ]; |
90 |
| - } |
91 |
| - |
92 |
| - if (isStraight) { |
93 |
| - return this.drawLine(curveData as number[][]); |
94 |
| - } else { |
95 |
| - return this.drawCurve(curveData as number[]); |
96 |
| - } |
| 10 | +export const getCatenaryPath = (p1: Point, p2: Point): string => { |
| 11 | + const distance = p1.getDistanceTo(p2); |
| 12 | + |
| 13 | + let length = 100; |
| 14 | + |
| 15 | + switch (true) { |
| 16 | + case distance < 400: |
| 17 | + length = 420; |
| 18 | + break; |
| 19 | + case distance < 900: |
| 20 | + length = 940; |
| 21 | + break; |
| 22 | + case distance < 1400: |
| 23 | + length = 1440; |
| 24 | + break; |
| 25 | + default: |
| 26 | + length = distance * 1.05; |
97 | 27 | }
|
98 | 28 |
|
99 |
| - /** |
100 |
| - * Determines catenary parameter |
101 |
| - */ |
102 |
| - private getCatenaryParameter(h: number, v: number, length: number, limit: number) { |
103 |
| - const m = Math.sqrt(length * length - v * v) / h; |
104 |
| - let x = Math.acosh(m) + 1; |
105 |
| - let prevx = -1; |
106 |
| - let count = 0; |
107 |
| - |
108 |
| - while (Math.abs(x - prevx) > 1e-6 && count < limit) { |
109 |
| - prevx = x; |
110 |
| - x = x - (Math.sinh(x) - m * x) / (Math.cosh(x) - m); |
111 |
| - count++; |
112 |
| - } |
113 |
| - |
114 |
| - return h / (2 * x); |
115 |
| - } |
116 |
| - |
117 |
| - /** |
118 |
| - * Calculate the catenary curve. |
119 |
| - * Increasing the segments value will produce a catenary closer |
120 |
| - * to reality, but will require more calcluations. |
121 |
| - */ |
122 |
| - private getCurve(a: number, p1: Point, p2: Point, offsetX: number, offsetY: number, segments: number) { |
123 |
| - const data = [p1.x, a * Math.cosh((p1.x - offsetX) / a) + offsetY]; |
124 |
| - |
125 |
| - const d = p2.x - p1.x; |
126 |
| - const length = segments - 1; |
127 |
| - |
128 |
| - for (let i = 0; i < length; i++) { |
129 |
| - const x = p1.x + (d * (i + 0.5)) / length; |
130 |
| - const y = a * Math.cosh((x - offsetX) / a) + offsetY; |
131 |
| - data.push(x, y); |
132 |
| - } |
133 |
| - |
134 |
| - data.push(p2.x, a * Math.cosh((p2.x - offsetX) / a) + offsetY); |
135 |
| - |
136 |
| - return data; |
137 |
| - } |
138 |
| - |
139 |
| - /** |
140 |
| - * Draws a straight line between two points. |
141 |
| - */ |
142 |
| - private drawLine(data: number[][]) { |
143 |
| - let result = ''; |
144 |
| - for (let i = 0; i < data.length - 1; i++) { |
145 |
| - result = result + `M${data[i][0]},${data[i][1]} L${data[i + 1][0]},${data[i + 1][1]}`; |
146 |
| - } |
147 |
| - return result; |
148 |
| - } |
149 |
| - |
150 |
| - /** |
151 |
| - * Draws a quadratic curve between every calculated catenary segment, |
152 |
| - * so that the segments don't look like straight lines. |
153 |
| - */ |
154 |
| - private drawCurve(data: number[]) { |
155 |
| - let result = ''; |
156 |
| - let length = data.length * 0.5 - 1; |
157 |
| - let ox = data[2]; |
158 |
| - let oy = data[3]; |
159 |
| - |
160 |
| - const temp: number[][] = []; |
161 |
| - |
162 |
| - result = result + `M${data[0]},${data[1]}`; |
163 |
| - |
164 |
| - for (let i = 2; i < length; i++) { |
165 |
| - const x = data[i * 2]; |
166 |
| - const y = data[i * 2 + 1]; |
167 |
| - const mx = (x + ox) * 0.5; |
168 |
| - const my = (y + oy) * 0.5; |
169 |
| - temp.push([ox, oy, mx, my]); |
170 |
| - result = result + `Q${ox},${oy} ${mx},${my} `; |
171 |
| - ox = x; |
172 |
| - oy = y; |
173 |
| - } |
174 |
| - |
175 |
| - length = data.length; |
176 |
| - result = result + `Q${data[length - 4]},${data[length - 3]} ${data[length - 2]},${data[length - 1]}`; |
177 |
| - |
178 |
| - return result; |
179 |
| - } |
180 |
| -} |
| 29 | + const controlX = Math.round((p1.x + p2.x) / 2); |
| 30 | + const controlY = Math.round(Math.max(p1.y, p2.y) + length - distance * 0.5); |
181 | 31 |
|
182 |
| -export default Catenary; |
| 32 | + return `M ${p1.x} ${p1.y} Q ${controlX} ${controlY} ${p2.x} ${p2.y}`; |
| 33 | +}; |
0 commit comments