apulSoft Blog

Feb 24th, 2021 - gui math cpp

Cable Dangle Catenary

Many audio applications display virtual cables on screen. A frictionless rope between two points affected by gravity forms a catenary curve. Many people (including audio software programmers) believe it is a parabola or a bezier curve of sorts, but this is not true. It is always part of a $x$-$y$ scaled $cosh()$ function, which can be proven with physics and integration as done on Wikipedia here.

A general formula for a catenary curve has the form

$y = a*cosh((x - b)/a)+c$

$b$ and $c$ are the x/y offsets and $a$ is the curve parameter, f.i. equivalent to the radius of the smallest circle that fits inside the curve.

A catenary is uniquely defined by two points $(x_0,y_0)$ and $(x_1,y_1)$ and a length of rope $L$ greater than the distance between the points. We require $x_1 > x_0$ and calculate $x_d =x_1-x_0>0$ and $y_d=y_1-y_0$. Inspired by a German math paper, finding a solution boils down to defining:

$x_f = sqrt(L^2 - y_d^2) / x_d$

and then solving the following equation for $xi$

$sinh(xi)/xi - x_f = 0$

From that $a,b$ & $c$ can be calculated:

$a = x_d/(2xi) $

$b = (x_0 + x_1)/2\ - a * sinh^-1( y_d / (2a*sinh(xi))) $

$c = y_0 - a cosh((x_0 - b)/a) $

The $xi$ equation needs to be solved numerically. Code I found online uses Newton-Raphson iterations, but using a good initial guess is tricky because the equation has extremely steep slopes for $xi$ > 2 sometimes causing numerical instability for high guesses. Unprecise initial values lead to many iterations which gets slow because of the trigonometric calls involved.

I ended up creating a two-fold approximation to the solution:

For $xi <= 2$, a Taylor series approximation works well:

$1 + xi^2/6 + xi^4/120 + xi^6/5040 ~~ x_f $

Symmetry and $x_f>1$ lead to just one real solution:

$xi ~~ sqrt(t - 84/t - 14) $ with $t = root(3)(sqrt(15680)*sqrt(405x_f^2 + 198x_f + 62) + 2520x_f + 616) $

For $xi > 2 $, I came up with a manually fitted logarithmical curve. As $sinh(x)/x = (e^x-e^-x)/(2x)$, for large $x$, $e^x$ is going to dominate the curve shape. I ended up with:

$xi ~~ 1.16*ln(x_f - 0.75)+1.9$

I first do the log approximation and if the result is smaller than 2, I replace it with the one from the inverse Taylor series. This is good enough for visual presentation on a screen. Even if $xi$ is slightly off, the curve will still go through the two points, the length just won't be exact.
If very precise results are required, one or two Newton iterations can be applied. When drawing a curve, the drawing coordinate-system needs to be considered to not get cables that hang upwards.
The case $x0=x1$ needs to be handled separately. The curve becomes a vertical line with $0.5(L - "distance")$ overhang at the bottom.

I wrote all this as a c++17 header file and put it online on GitHub. Example Usage:

#include "Catenary.h"

plap::Catenary c(10.f, 20.f, 200.f, 30.f, 20.f); // 20 pixel extra distance
for (float x = 10.f; x <= 200.f; x += 1.f) {
    float y = c.calcY(x);
    // Draw point (x|y)