Archimedean spiral


As I said in the summary of Spiral moving fish, when I published Moving fish, someone said: “Could you coil up the fish to allow more segments?” After attempting for a period, I created the work.

Spiral moving fish

Coiling up the fish or the heart chain is similar.

Text heart chain

From both works, you might have found. We need a spiral whose any ray from the origin intersects successive turnings of the spiral in points with a constant separation distance. The Archimedean spiral is what we want.

Archimedean spiral

As it said in Archimedean spiral, it can be described by the equation r = a + bθ and the constant separation distance is equal to 2πb if we measure θ in radians. The a and b are real numbers. Changing the parameter a will turn the spiral, while b controls the distance between successive turnings. We don't have to turn the spiral so here let a = 0 for simplification.

Archimedean spiral

Once you know the spiral equation is r = bθ, it's not hard to draw an Archimedean spiral. Just one thing. The trigonometric functions of OpenSCAD accept degrees, not radians, so you have to map radians to degrees by yourself before invoking these functions.

module line(point1, point2, width = 1, cap_round = true) {
    angle = 90 - atan((point2[1] - point1[1]) / (point2[0] - point1[0]));
    offset_x = 0.5 * width * cos(angle);
    offset_y = 0.5 * width * sin(angle);

    offset1 = [-offset_x, offset_y];
    offset2 = [offset_x, -offset_y];

    if(cap_round) {
        translate(point1) circle(d = width, $fn = 24);
        translate(point2) circle(d = width, $fn = 24);
    }

    polygon(points=[
        point1 + offset1, point2 + offset1,  
        point2 + offset2, point1 + offset2
    ]);
}

module polyline(points, width = 1) {
    module polyline_inner(points, index) {
        if(index < len(points)) {
            line(points[index - 1], points[index], width);
            polyline_inner(points, index + 1);
        }
    }

    polyline_inner(points, 1);
}

PI = 3.14159;
step = 0.25;
circles = 5;
arm_len = 10;

b = arm_len / 2 / PI;
// one radian is almost 57.2958 degrees
points = [for(theta = [0:step:2 * PI * circles])
    [b * theta * cos(theta * 57.2958), b * theta * sin(theta * 57.2958)]
];

polyline(points, 1);

It seems no problem.

Archimedean spiral

But if you increase the step value to 1 or more, what will happen?

Archimedean spiral

What? The distance between two successive points increases when you increase the radian. Why does it matter? If you place hearts on these points, the distance between hearts will also increase. For simplification, we just use circles here.

PI = 3.14159;
step = 0.2;
circles = 5;
arm_len = 10;

b = arm_len / 2 / PI;
for(theta = [0:step:2 * PI * circles]) {
    rotate(theta * 57.2958) 
        translate([b * theta, 0, 0]) 
            circle(1, $fn = 24);
}

Archimedean spiral

The result is not what we want. We not only need a constant separation distance between two turnings but also require a steady distance between two points whose angle are equal. Only when theses conditions are satisfied, things like a heart chain can fit together.

Simply put, you cannot only increase the radian with a constant increment. A bigger θ causes a longer r, and a longer r creates a greater arc in the condition of a constant increment of the radian. That's why the distance between points are longer and longer.

Finding the increment

To have a constant distance between points, we have to find every increment on the radian. Let's list the relationships we've known currently.

Archimedean spiral

Suppose the current angle is Ai radians, the length of a ray is Ri = b * Ai. If it needs to increase ad radians to make the distance L between points, the length of the ray is R = b * (Ai + ad). According to law of cosines, we have L * L = R * R + Ri * Ri - 2 * R * Ri * cos(ad * 180 / π).

Theoretically, using b * (Ai + ad) to substitute R can finally get what the ad is.

But, it's not easy to evaluate the ad value. What can we do now?

I use approximation here. Because the L value is small when coiling up the fish or the heart, the ad value is also small. That is, we can approximate R to Ri, so we can simplify the equation to L * L = Ri * Ri + Ri * Ri - 2 * Ri * Ri * cos(ad * 180 / π).

Then, we can get ad = acos((2 * Ri * Ri - L * L) / (2 * Ri * Ri)) / 180 * π.

Of course, the ad is not a constant value. In order to have a constant L value, the ad value decreases every time. Once we know the current ad, we can get the next Ri = b * (Ai + ad). After knowing the next Ri, we can get the next ad = acos((2 * Ri * Ri - L * L) / (2 * Ri * Ri)) / 180 * π.

When modeling, we have to evaluate all radians. It is done recursively by the find_radians function.

PI = 3.14159;

dots = 100;            // number of dots
dot_dist = 5;          // distance between points
arm_len = 5;           // ray length
init_radian = PI * 4;  // initial angle

b = arm_len / 2 / PI;

function r(b, theta) = b * theta;

function radian_step(b, theta, l) = 
    acos((2 * pow(r(b, theta), 2) - pow(l, 2)) / (2 * pow(r(b, theta), 2))) / 180 * PI;

function find_radians(b, l, radians, n, count = 1) =
    count == n ? radians : (
        find_radians(
            b, 
            l, 
            concat(
                radians, // current angle
                [radians[count - 1] + radian_step(b, radians[count - 1], l)] // angle after rotating
            ), 
            n,
        count + 1)
    );

for(theta = find_radians(b, dot_dist, [init_radian], dots)) {
    rotate(theta * 57.2958) 
        translate([b * theta, 0, 0]) 
            circle(1, $fn = 24);
}

Because we use the approximation, the initial value should be small to avoid a significant error. This approach is enough for coiling up the fish or the heart chain.

Let's see how the model is.

Archimedean spiral

Now, you can use your models to replace those circles. After adjusting some parameters, you can coil up anything you want. How about a snake?