海龜螺旋


之前談過螺線,像是〈黃金螺線〉、〈阿 基米德螺線〉,我們使用了公式求出每個點,然後連接起來。

使用海龜的話,也能創造螺線,只要海龜一邊前進、一邊轉動,隨著前進的長度不同、轉動的角度不同,就可以創造出特別的圖樣,Turtle Spiral Generator 就是個例子:

Turtle Spiral Generator

前進與旋轉

海龜螺旋非常地簡單,每次前進的長度是 leng,而後轉動 angle 角度,如果 leng 不變,angle 是 90,那麼畫出來會是正方形,如果 leng 不變,angle 為 120,那麼畫出來會是三角形,如果 leng 不變,angle 為 144,那畫出來會是什麼呢?

function turtle(x, y, angle) = [[x, y], angle];

function get_x(turtle) = turtle[0][0];
function get_y(turtle) = turtle[0][1];
function get_xy(turtle) = turtle[0];
function get_angle(turtle) = turtle[1];

function set_point(turtle, point) = [point, get_angle(turtle)];

function set_x(turtle, x) = [[x, get_y(turtle)], get_angle(turtle)];
function set_y(turtle, y) = [[get_x(turtle), y], get_angle(turtle)];
function set_angle(turtle, angle) = [get_xy(turtle), angle];

function forward(turtle, leng) = 
    turtle(
        get_x(turtle) + leng * cos(get_angle(turtle)), 
        get_y(turtle) + leng * sin(get_angle(turtle)), 
        get_angle(turtle)
    );

function turn(turtle, angle) = [get_xy(turtle), get_angle(turtle) + angle];

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

// 以上為之前實作的海龜繪圖

side_leng = 10;
angle = 144;
width = 1;

t1 = turtle(0, 0, 0);

t2 = forward(t1, side_leng);
polyline([get_xy(t1), get_xy(t2)], width);

t3 = forward(turn(t2, angle), side_leng);
polyline([get_xy(t2), get_xy(t3)], width);

t4 = forward(turn(t3, angle), side_leng);
polyline([get_xy(t3), get_xy(t4)], width);

t5 = forward(turn(t4, angle), side_leng);
polyline([get_xy(t4), get_xy(t5)], width);

t6 = forward(turn(t5, angle), side_leng);
polyline([get_xy(t5), get_xy(t6)], width);

結果會是個五芒星,因為它的每個尖角為 36,180 減去 36 就是 144:

海龜螺旋

來個遞迴

當然,像上面這樣寫其實很差勁,不過,你也不能改成迴圈,因為 OpenSCAD 是 Functional programming 典範,想要能重複進行前進、旋轉,你要使用遞迴:

// 略...
// 以上必須用到先前實作的海龜繪圖

module turtle_spiral(t_before, times, side_leng, angle, width) {
    if(times != 0) {
        t_after = forward(turn(t_before, angle), side_leng);
        polyline([get_xy(t_before), get_xy(t_after)], width);
        turtle_spiral(t_after, times - 1, side_leng, angle, width);
    }

}

side_leng = 10;
angle = 144;
width = 1;
times = 5;

turtle_spiral(turtle(0, 0, 0), times, side_leng, angle, width);

遞迴看來很難,其實沒什麼,會難其實是你要追蹤太多變數的狀態,通常這代表了你每次遞迴同時做了太多事,在 Imperative programming 風格的程式語言中,你很容易犯下這類的毛病。

然而在 Functional programming 的語言中,進行遞迴反而容易,因為變數或向量等狀態無法變動,這會迫使你寫程式時,一件事一件事來,因而很容易看出重複的模式,就像一開始畫五芒星時,一直重複出現的模式 就是…

t_after = forward(turn(t_before, angle), side_leng);
polyline([get_xy(t_before), get_xy(t_after)], width);

因此,你只要將這個寫在模組中,表示每次只做以上這些事,下一次做什麼?管它呢!直接再呼叫一次模組就是了,當然,我們必須能停止遞迴,在一開 始的五芒星實作中,其實做了五次重複的動作,因此「次數」就是停止的條件,因此後來的 turtle_spiral 模組,多了一個 times 參數,這一次執行表示已經完成一次動作了,因此接下來剩 times - 1 次動作,這也就是為什麼要 turtle_spiral(t_after, times - 1, side_leng, angle, width)

加點變化

除了每次前進固定距離、旋轉固定角度,我們還可以加點變化性,例如,下一次前進時減少 step 長度,並且在前進的長度小於某個長度時,讓海龜停下來:

// 略...
// 以上必須用到先前實作的海龜繪圖

module turtle_spiral(t_before, side_leng, d_step, min_leng, angle, width) {
    if(side_leng > min_leng) {
        t_after = forward(turn(t_before, angle), side_leng);
        polyline([get_xy(t_before), get_xy(t_after)], width);
        turtle_spiral(t_after, side_leng - d_step, d_step, min_leng, angle, width);
    }

}

side_leng = 50;
d_step = 1;
min_leng = 1;
angle = 90;
width = 1;

turtle_spiral(
    turtle(0, 0, 0), 
    side_leng, 
    d_step, 
    min_leng, 
    angle, 
    width
);

這會產生什麼結果呢?

海龜螺旋

感覺很像〈阿基米德螺線〉對吧!事實上,如果你使用以下的參數:

side_leng = 50;
d_step = 0.1;
min_leng = 1;
angle = 15;
width = 1;

無疑地,就是〈阿基米德螺線〉了:

海龜螺旋

如同〈阿基米德螺線〉中第一個範例,我們每次都是轉固定角度,至 於為什麼臂長會等距?因為每次都減少固定長度,這從剛剛每次轉 90 度的圖,比較容易解釋,從下圖應該可以清楚看到,每個臂長都會是固定的 2d

海龜螺旋

從這出發,可以證明在每次轉動的角度不少於 90 的情況下,畫出來的圖案,臂長也都是等距的,自己試試看吧!

你也可以試著調整 side_lengd_step 等參數,試著找出 Turtle Spiral Generator 的圖是用哪些參數來構成的。

黃金螺線〉使用海龜繪圖的話,也是畫得出來的,不過在每次的前進距離與角 度上比較麻煩一些,我就不解釋了,當成題目讓你試著想想看怎麼做吧!以下我實作的版本:

// 略...
// 以上必須用到先前實作的海龜繪圖

side_leng = 30;
min_leng = 0.2;
angle = 15;
width = 1;

module turtle_spiral_by_times(t_before, times, side_leng, angle, width) {
    if(times != 0) {
        t_after = forward(turn(t_before, angle), side_leng * angle / 180 * 3.14159);
        polyline([get_xy(t_before), get_xy(t_after)], width);
        
        turtle_spiral_by_times(t_after, times - 1, side_leng, angle, width);
    } else {
        turtle_spiral(t_before, side_leng / 1.618, min_leng, angle, width);
    }

}
    
module turtle_spiral(t_before, side_leng, min_leng, angle, width) {
    if(side_leng > min_leng) {
        times = 90 / angle;
        turtle_spiral_by_times(t_before, 90 / angle, side_leng, angle, width);
    }
}

turtle_spiral(
    turtle(0, 0, 0), 
    side_leng, 
    min_leng, 
    angle, 
    width
);

確實是黃金螺線呢!

海龜螺旋





=