3

ドラゴン曲線を描く

2342
0

ドラゴン曲線を描画する話。
 まず、ドラゴン曲線とは次のようなもの。
ドラゴン曲線 ドラゴン曲線
左下の端っこが途切れているところからスタートして、方向を変化させながら進んでいく感じ(canvas2Dでの描画なので回転方向が時計回り、まあどっちでもいいんだけど)。
とりあえずオリジナルの定義。0,1,,7で、8つの等分された方向をあらわすとする。つまり、k
(coskπ4, sinkπ4)
があらわすベクトルの方向に点が進むことを表現する。このときたとえば、
[0,2,4,2]
という配列は、右、上、左、上という順番で進むことをあらわす。つまりmod 8で考えているのでそのように計算する(8=0とか4+5=1とか)。

ドラゴン曲線

方向列[0]からスタートして次の生成規則で配列を作っていく:
[a0,a1,,an1]  [a01, a11, ,an11,an1+1,, a1+1, a0+1].
すなわち前半を元の方向列に対し45°回転させ、後半は元の方向列を逆からたどってそれぞれ45°回転させる感じ。これを実行するとこんな風になる。
ドラゴン曲線2 ドラゴン曲線2

フラクタル図形としてのドラゴン曲線はこうじゃないけど、すべてのセグメントが同じ長さとしたときのものを考えているのでこんな感じになる。
 この定義だとどんどん長くしていくのが困難で規則も見出しづらいので、スタートが0になるように定義を改造する。すなわち、前半の1をやめて後半を+2するように変更する。
[a0,a1,,an1]  [a0,a1,,an1,an1+2,, a1+2, a0+2].
すなわち後半は逆配列にして+2する。+2とは90°回転に相当する。
 これを2回行うとどうなるのか見るため、長い部分をギリシャ文字で置き換えて考えてみる。配列αの逆並びをαのように書く。あと逆配列ですべて+2したものをα+2と書くなど。α+2=α+2とか明らかね。
[α]  [α, α+2]  [α, α+2, α+2+2, α+2]
となるがここで
α+2+2=α+4=α+4
なので方向が真逆になる。これをαと書くことにすると、結局、
[α, β]  [α, β, α, β]
のようになっている。これは一般的に成り立つ。なぜならどの段階においてもβの部分はαの部分に対してあのような表記になっているから。
 このことを用いると、項数を使うことでどの方向に曲がるのかを計算できる。第n項を考える。n=0,1ならば0,2になる。最初の4項は、
[0, 2, 4, 2].
n2とする。nに対し、2mn<2m+1となるmを取る。このときn2m+1まで作った配列の後半に入っている。上記の議論で言うと後ろの[α, β]の方。だからそこでもし2mn<2m+2m1に入るならこれは第n2m項目を反転させたもの(4を足したもの)になるし、2m+2m1n<2m+1ならそのまま第n2m項目の値になる。計算としては、n2進数展開において一番頭の1を取り去る処理に当たる。
 これを繰り返すといずれ01になってmが取れなくなる。そこまでに何回反転をしたかで、第n項が何であったかが分かる。反転回数をtとすると、最後に0になるなら4tだし、1になるなら4t+2ということになる。ところでこの2mを取り去る行為において後半のどちらに入っているかというのは、頭の1の下が01かということに相当する。だから、11の並びなら反転しないし、10の並びなら反転させるというわけ。

ドラゴン曲線の進行方向

初期方向を0、すなわち水平右とするとき、n番目のドラゴン曲線上の進行方向は、n2進展開して上から順繰りに見ていったときに現れる「10」の並びの個数をtとして、末尾が0の場合は4t ,末尾が1の場合は4t+2.

例えば
154=10011010(2),   155=10011011(2)
なので154番目の曲がる方向は43=12=4でまっすぐ左。155番目は42+2=10=2だから上方向。canvas2Dなら下だけど。
 こうなるともう8分割にこだわる必要がないので、90°ベースで考える。また、このあとコードを載せたいので回転方向は時計回りにする。
 自分はよくp5.jsという描画ライブラリを使うので、それによる描画コードを載せる:

      let a = 0;
let b = 0;
let f = 0;
function setup(){
    createCanvas(640, 640);
    background(0);
    stroke(255);
}

function draw(){
    translate(320, 320);
    let dir = f & 1;
    let h = 1;
    while(f >= h){
        if(!(f & h) && (f & (h * 2))){ dir += 2; }
        h *= 2;
    }
    let s = 8 * cos(dir * PI / 2);
    let t = 8 * sin(dir * PI / 2);
    line(a, b, a + s, b + t);
    a += s;
    b += t;
    f++;
}
    

作品のリンク
描画結果:
ドラゴン曲線3 ドラゴン曲線3
中央上の切れたところからスタートしている。点(a, b)が指定された方向に毎フレーム移動する感じ。次に進む方向を(s, t)というかdirが与える。90°ベースなのでdirπ/2を掛ける。項数はフレームのfが担う。fの初期値は2進数展開での1の位だから1と&を取って出す。そのあとはhというのが1から始まって次第に2倍される、これがf以下の範囲で。h2hはそれぞれf2進展開での2つの連続した位に相当してて、2hの位が1hの位が0のときにdir180°の分だけずらしている(すなわち+2している)。これが「10」の並びの数だけ反転、に相当するから、ちゃんと上で述べた通りの描画になっている。
短く書くとこんな感じ:

      w=640;a=b=f=k=0;c=320
setup=()=>{createCanvas(w,w);background(0);k=PI/2}
draw=()=>{translate(c,c);e=f&1;h=1;while(f>=h){if(!(f&h)&&(f&(h*2))){e+=2}h*=2}s=8*cos(k*e);t=8*sin(k*e);stroke(w);line(a,b,a+=s,b+=t);f++}
    

これは実は4つの方向に進ませるとすべての格子で平面を覆うことができる。そのようにしたコードがこちら。

      w=640;a=b=f=k=0;c=320
setup=()=>{createCanvas(w,w);background(0);k=PI/2}
draw=()=>{translate(c,c);e=f&1;h=1;while(f>=h){if(!(f&h)&&(f&(h*2))){e+=2}h*=2}s=8*cos(k*e);t=8*sin(k*e);r=4;while(r--){rotate(k);stroke(r*90,(r&1)*w,w);line(a,b,a+s,b+t)}a+=s;b+=t;f++}
    

作品のリンク
実行結果:
ドラゴン曲線4 ドラゴン曲線4
すなわちすべての隣接格子点間のセグメントを1回だけ通るので、それを、そのセグメントを対角線とする正方形で置き換えれば完全に平面充填できる。それを実行したコードがこちら。

          let w = 640;
    let a = 320;
    let b = 320;
    let f = 0;
    function setup(){
        createCanvas(w, w);
        colorMode(HSB, 64);
    }
    function draw(){
        let e = (f & 1) * 2 - 1;
        let h = 1;
        while (f >= h) {
            if (!(f & h) && (f & (h * 2))) {
                e += 4;
            }
            h *= 2;
        }
        let s = 9 * cos(PI * e / 4);
        let t = 9 * sin(PI * e / 4);
        let r = 5;
        while (--r) {
            applyMatrix(0, -1, 1, 0, 0, w);
            fill(r * 12, w, w);
            quad(a, b, a, b + t, a + s, b + t, a + s, b);
        }
        a += s;
        b += t;
        f++;
    }
    

ドラゴン曲線6 ドラゴン曲線6

曲線を描くだけなら最終的にこのくらい短く書ける(p5js)。

DragonCurve

      a=b=180;f=0;draw=_=>{k=PI/2;if(!f){createCanvas(640,640)}e=f&1;h=1;while(f>=h){if(!(f&h)&&(f&(h*2))){e+=2}h*=2}line(a,b,a-=8*cos(k*e),b+=8*sin(k*e));f++}
    

差分に着目したらもっと短くなった。

      x=y=180;u=8;v=f=0;draw=_=>{if(!f){createCanvas(640,640)}line(x,y,x-=u,y+=v);s=++f;while(1-s%2){s/=2}k=1-(s&2);z=u;u=-k*v;v=k*z}
    
投稿日:20201125
OptHub AI Competition

この記事を高評価した人

高評価したユーザはいません

この記事に送られたバッジ

バッジはありません。
バッチを贈って投稿者を応援しよう

バッチを贈ると投稿者に現金やAmazonのギフトカードが還元されます。

投稿者

黒狐
黒狐
34
4950
数学ちょっと好きです!

コメント

他の人のコメント

コメントはありません。
読み込み中...
読み込み中