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.
Coiling up the fish or the heart chain is similar.
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.
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.
But if you increase the step
value to 1 or more, what will happen?
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);
}
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.
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.
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?