Skip to content

Commit ca78d66

Browse files
Simplify catenary function to draw single quadratic bezier curves instead of calculating actual catenary curves (#19)
1 parent 17c0f7e commit ca78d66

File tree

3 files changed

+38
-192
lines changed

3 files changed

+38
-192
lines changed

core/src/helpers/Catenary.ts

Lines changed: 26 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -1,182 +1,33 @@
1+
import type Point from './Point';
2+
13
/**
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
137
*
148
* @category Helpers
159
*/
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;
9727
}
9828

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);
18131

182-
export default Catenary;
32+
return `M ${p1.x} ${p1.y} Q ${controlX} ${controlY} ${p2.x} ${p2.y}`;
33+
};

core/src/helpers/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { default as Catenary } from './Catenary';
1+
export { getCatenaryPath } from './Catenary';
22
export { default as Point } from './Point';
33
export { default as getEnergy } from './energy';
44

core/src/rack/Cables.svelte

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts">
22
import patches from '../state/patches';
33
import modules from '../state/modules';
4-
import Catenary from '../helpers/Catenary';
4+
import { getCatenaryPath } from '../helpers/Catenary';
55
import Point from '../helpers/Point';
66
import { BAR_HEIGHT } from '../contstants';
77
import type { Patch } from '../types';
@@ -47,11 +47,9 @@
4747
boxY.top - 50 + Math.round(elY.offsetHeight / 2) + 2 + scrollY
4848
);
4949
50-
let chain = new Catenary();
51-
5250
if (patch.selected) {
5351
activeCable = {
54-
path: chain.getPath(p1, p2),
52+
path: getCatenaryPath(p1, p2),
5553
color: patch.color,
5654
point: patch.selected === patch.input ? p2 : p1,
5755
};
@@ -62,7 +60,7 @@
6260
}
6361
6462
return {
65-
path: chain.getPath(p1, p2),
63+
path: getCatenaryPath(p1, p2),
6664
color: patch.color,
6765
};
6866
})
@@ -88,9 +86,8 @@
8886
const clientY = 'clientY' in event ? event.clientY : event.touches[0].clientY;
8987
9088
const point = new Point(clientX + scrollX, clientY + scrollY - BAR_HEIGHT);
91-
const chain = new Catenary();
9289
93-
const path = chain.getPath(activeCable.point, point);
90+
const path = getCatenaryPath(activeCable.point, point);
9491
9592
activeCable = {
9693
...activeCable,
@@ -121,22 +118,20 @@
121118
opacity: 1;
122119
z-index: var(--zindex-cables);
123120
}
121+
svg path {
122+
stroke-linecap: round;
123+
fill: none;
124+
}
124125
</style>
125126

126127
<svelte:body on:mousemove={onMove} on:touchmove={onMove} />
127128

128129
<svg>
129130
{#each cables as cable}
130-
<path stroke={darken(cable.color, -40)} stroke-linecap="round" stroke-width="5" fill="none" d={cable.path} />
131-
<path stroke={cable.color} stroke-linecap="round" stroke-width="3" fill="none" d={cable.path} />
131+
<path stroke={darken(cable.color, -40)} stroke-width="5" d={cable.path} />
132+
<path stroke={cable.color} stroke-width="3" d={cable.path} />
132133
{/each}
133134
{#if activeCable}
134-
<path
135-
stroke={darken(activeCable.color, -40)}
136-
stroke-linecap="round"
137-
stroke-width="5"
138-
fill="none"
139-
d={activeCable.path}
140-
/>
135+
<path stroke={darken(activeCable.color, -40)} stroke-width="5" fill="none" d={activeCable.path} />
141136
{/if}
142137
</svg>

0 commit comments

Comments
 (0)