Pythonのタートル・グラフィックス(10):canvasに木を描く

Pythonのタートル・グラフィックスはTkinterを基礎に作られているのでTkinterのcanvasウィジェット上にタートル・グラフィックスで描画を行うことは容易である。しかもTkinterのメニューなどの他のウィジェットも画面構成に使える。ここでは本物らしく木を描くタートル・グラフィックスをcanvas上で実行し、入力パラメタをentryウィジェットを使って入力するようにする。サンプル画像を以下に示す:

canvas上の木

左側がcanvasで、右側がentryを含むメニューである。「Turbo Graphics」(安齋利洋・伊吹龍著;1987年)では二種類(単子葉植物、双子葉植物)の木の描画アルゴリズムが載っているがここではメニューで種類を選べるようにしてある。

Pythonのプログラムはここにある。

Pythonのタートル・グラフィックス(9):ヒルベルト(Hirbert)曲線を描く

ヒルベルト曲線

中華どんぶりに描かれているような「一筆書き」である。このような描画はPythonのタートル・グラフィックスが得意とするものである。「Turbo Graphics」(安齋利洋・伊吹龍著;1987年)に載っているPascal風のプログラムをなぞって作ったのが以下のものだ(元のプログラムにあったkameの大域変数は関数の引数とするようにした)。


#coding: utf-8

import myturtle

depth = 7 #再帰の深さ
atom = 3 #最小の長さ

def deko(kame, level):

    if(level == 0):
        return

    else:
        kame.reverse()
        kame.rt(90)
        deko(kame, level-1)
        kame.reverse()
        kame.fd(atom)
        kame.rt(90)
        deko(kame, level-1)
        kame.fd(atom)
        deko(kame, level-1)
        kame.rt(90)
        kame.fd(atom)
        kame.reverse()
        deko(kame,level-1)
        kame.rt(90)
        kame.reverse()
         

if __name__ == '__main__':
      
    kame = myturtle.MyTurtle()
    kame.ht()
    kame.setposition(-300,-300)
    kame.clear()
    deko(kame, depth)

このプログラムでは二つの定数(depth, atom)が定義されているが、atomは描画が画面内に納まるようにするためのものである。定数depthは再帰の深さを示すものでこの曲線にとっては本質的なものである。

定数depthを大きくしたときの描画は実に奇妙である。以下depth=8の例を示す:

ヒルベルト曲線

一本の線分(両端がある)にも拘わらず平面を埋め尽くすような描画ができる。再帰の深さ(depth)を無限大にする極限でこの線分は平面を文字通り埋め尽くして平面を作ることが知られている。

Pythonのタートル・グラフィックス(8):コッホ(Koch)曲線を描く

有名なコッホ(Koch)曲線を描く。「Turbo Graphics」(安齋利洋・伊吹龍著;1987年)に載っているPascal風のプログラムをなぞって作ったのが以下のものだ:


#coding: utf-8

import myturtle

depth = 5 #再帰の深さ

def koch(length, level):
    global kame

    if(level > 0):
        len1 = length /3
        koch(len1, level-1)
        kame.lt(60)
        koch(len1, level-1)
        kame.rt(120)
        koch(len1, level-1)
        kame.lt(60)
        koch(len1, level-1)

    else:
        kame.fd(length)

         

if __name__ == '__main__':
    
        
    kame = myturtle.MyTurtle()
    kame.ht()
    kame.setposition(-320,-0)
    kame.clear()
    koch(640, depth)

【描画】

コッホ(Koch)曲線

複雑であるが折れ線による「一筆描き」である。このような「一筆描き」ではあるセグメントの折れ線ではこれまで描かれた折れ線の先端を起点として一つの折れ線を描き、その折れ線の先端は次の折れ線の起点となるような処理が必要となる。上のプログラムではこれをkameを大域変数(global)とすることで行っている。こうすることでkameが折れ線を描く毎にkameの属性(位置)が更新される。

このような事情を考慮して直接Pythonで書くと以下のようになる:


#coding: utf-8

import myturtle

depth = 5 #再帰の深さ

def koch(kame, length, level):

    if(level > 0):
        len1 = length /3
        koch(kame, len1, level-1)
        kame.lt(60)
        koch(kame, len1, level-1)
        kame.rt(120)
        koch(kame, len1, level-1)
        kame.lt(60)
        koch(kame, len1, level-1)

    else:
        kame.fd(length)

         

if __name__ == '__main__':
    
        
    kame = myturtle.MyTurtle()
    kame.ht()
    kame.setposition(-320,-0)
    kame.clear()
    koch(kame, 640, depth)

このプログラムではkameは関数kochの引数の一つとして関数kochに渡されるようになっている。このプログラムはPascal風の処理系では期待通りの結果は得られないが、Pythonでは上の描画と同じものが得られる。Pythonでは関数に渡されたオブジェクトkameは関数内の変更を受けて実引数のkameの属性も変更されるからだ。

Pythonではkameを大域変数とするプログラムも、kameを関数の引数として渡すプログラムも作れる。後者は少しトリッキーな気もするが。

Pythonのタートル・グラフィックス(7):本物らしく木を描く

「Turbo Graphics」(安齋利洋・伊吹龍著;1987年)には本物らしく木を描画するプログラムが紹介されている。オリジナルのアルゴリズムは本田久夫氏(「樹木の分枝」別冊『数理科学』形・フラクタル、p78)との注がある。

プログラムは描画のためのパラメターをファイルから読み込むなど凝ったものになっているがここでは描画のエッセンスだけを抜き出しPythonで書いてみた:


#coding: utf-8

import myturtle

#パラメター
xP=0
yP=-300
angleMiki=15
ratioMiki = 0.80
angleEda=45
ratioEda=0.70
length=150
ratioFutosa=0.7200
generation=10


class Seed:
    """木の表現のためのパラメター類"""

def ki(tane):

    def edawakare(mikiT, length, thickness, depth):
        if(depth ==0 or length < 2 or thickness < 0.1):
            return
        miki=mikiT.clone()
        miki.reverse()
        miki.fd(length)
        eda=miki.clone()
        miki.lt(tane.angleMiki)
        edawakare(miki, length*tane.ratioMiki, thickness, depth-1)
        eda.rt(tane.angleEda)
        edawakare(eda, length*tane.ratioEda, thickness*tane.ratioFutosa, \
                  depth-1)
        
    miki=myturtle.MyTurtle()
    miki.ht()
    miki.lt(90)
    miki.setposition(tane.xP, tane.yP)
    miki.clear()
    edawakare(miki, tane.length, 1, tane.generation)

if __name__ == '__main__':

    tane=Seed()
    tane.xP=xP
    tane.yP=yP
    tane.angleMiki=angleMiki
    tane.ratioMiki=ratioMiki
    tane.angleEda=angleEda
    tane.ratioEda=ratioEda
    tane.length=length
    tane.ratioFutosa=ratioFutosa
    tane.generation=generation
    
        
    ki( tane)

ここでは関数内ではクローンturtleを使っている。

【実行結果の描画】

タートル・グラフィックスによる木

Pythonのタートル・グラフィックス(6):木を描く

MYTurtleクラスを使って木を描くことにする。プログラムは以下のようになる:


import myturtle



angle = 60
ratio = 0.5
def ki( mikiT, length):
    if( length > 1):
        miki=mikiT.clone()
        miki.reverse()
        miki.fd(length)
        ki(miki, ratio*length)
        miki.rt(angle)
        ki(miki, ratio*length)
     

if __name__ == '__main__':
    
        
    miki = myturtle.MyTurtle()
    miki.clear()
    miki.ht()
    miki.lt(90)
    miki.setposition(0,-200)
    ki( miki, 200)

ここでは関数kiの引数に入るturtleオブジェクトがこのki関数で変化しないようにturtleオブジェクトのクローンを作って描画させている。

このクローンを使わない方法をとるにはki関数内でダミーな動きをさせて結果的にturtleオブジェクトの状態変化がなかったようにする:


import myturtle



angle = 60
ratio = 0.5
def ki( miki, length):
    if( length > 1):
        miki.reverse()
        miki.fd(length)
        ki(miki, ratio*length)
        miki.rt(angle)
        ki(miki, ratio*length)
        miki.lt(angle) #ダミー
        miki.bk(length) #ダミー
        miki.reverse() #ダミー
    

if __name__ == '__main__':
    
        
    miki = myturtle.MyTurtle()
    miki.clear()
    miki.ht()
    miki.lt(90)
    miki.setposition(0,-200)
    ki( miki, 200)

【描画の結果】

クローンkiなしのki

Pythonのタートル・グラフィックス(5):MyTurtleクラスの導入

「Turbo Graphics」(安齋利洋・伊吹龍著;1987年)にはタートルの動きに対する面白い命令がある。それがreverseである。これはタートルの右回転(rt)と左回転(lt)の役割を反転させる機能を持っている。Pythonのタートル・グラフィックスにはこの機能がない。Python上でこの機能を関数として定義してもよいが折角なのでreverseをタートルクラスの一つメソッドとして追加したMyTurtleクラスを作ることにする。

MyTurtleクラスは元々のモジュール(turtle)で定義されているTurtleクラスを継承して作る。クラスの定義は以下のようなものである:


import turtle

class MyTurtle(turtle.Turtle):
    def __init__(self):
        turtle.Turtle.__init__(self)
        self.phase = 1

    def rt(self, angle):
        self.angle = angle*self.phase
        turtle.Turtle.rt(self, self.angle)

    def lt(self, angle):
        self.angle = angle*self.phase
        turtle.Turtle.lt(self, self.angle)

    def rev(self):
        return self.phase

    def reverse(self):
        self.phase=self.phase*-1

    def clone(self):
        temp=turtle.Turtle.clone(self)
        temp.phase = self.phase
        return temp

このクラスではreverseの導入によって影響を受けるturtleモジュールの中のメッソドの書き換えは最小限に止めてある。

Pythonのタートル・グラフィックス(4):クローンkameなしの複雑再帰

先のkameの複雑な再帰描画をクローンを使わないで行うことを考える。まず問題の所在を明確にするためにプログラムにデバッグ用のprint文を追加した。プログラムの一部を載せる(再帰の深さは浅くしてある)。

def kame( position, size):
space = ' '*4*(int(200/size)-1)
print(space, 'draw kame',position.ycor(), size)
sugata( position, size)
if size > 75:
position.fd(size/2)
kame(position, size/2)
position.bk(size)
kame(position, size/2)
print(space, 'returning', position.ycor(), size)

この出力は以下のようになる:


 draw kame 0.0 200
     draw kame 100.0 100.0
             draw kame 150.0 50.0
             returning 150.0 50.0
             draw kame 50.0 50.0
             returning 50.0 50.0
     returning 50.0 100.0
     draw kame -150.0 100.0
             draw kame -100.0 50.0
             returning -100.0 50.0
             draw kame -200.0 50.0
             returning -200.0 50.0
     returning -200.0 100.0
 returning -200.0 200

draw kame 100に対応する(再帰のレベルが同じ)描画がdraw kame -150となっておりこれがずれの原因になる。

そこで関数kameに渡したオブジェクトpositionの属性がこのkame関数のなかで最終的に変化がなかったようにダミーの動きをいれる。プログラムは以下のようになる。


#coding: utf-8

import turtle

def sugata( center, size):
    for i in range(6):
        painter = center.clone()
        painter.fd(size)
        painter.rt(120)
        painter.fd(size)
        center.rt(60)

def kame( position, size):
    #space = ' '*4*(int(200/size)-1)
    #print(space, 'draw kame',position.ycor(), size)
    sugata( position, size)
    if size > 10:
        position.fd(size/2)
        kame(position, size/2)
        position.bk(size)
        kame(position, size/2)
        position.fd(size/2) #ダミーの動き
    #print(space, 'returning', position.ycor(), size)

    
if __name__ == '__main__':
    
    turtle.clearscreen()
    position = turtle.Turtle()
    position.ht()
    position.lt(90)
    kame( position, 200)

【結果の描画」

クローンkameなしの再帰描画の完成図

期待した通りの結果が得られた。

 

Pythonのタートル・グラフィックス(3):クローンkameによる複雑再帰

「Turbo Graphics」(安齋利洋・伊吹龍著;1987年)にはもう少し複雑なkameの再帰描画の例が載っている。プログラムをなぞってPythonで書くと以下のようになる:


#coding: utf-8

import turtle

def sugata( center, size):
    for i in range(6):
        painter = center.clone()
        painter.fd(size)
        painter.rt(120)
        painter.fd(size)
        center.rt(60)

def kame( position, size):
    sugata( position, size)
    if size > 10:
        position.fd(size/2)
        kame(position, size/2)
        position.bk(size)
        kame(position, size/2)

    
if __name__ == '__main__':
    
    turtle.clearscreen()
    position = turtle.Turtle()
    position.ht()
    position.lt(90)
    kame( position, 200)

描画は以下のようになる:

kameの再帰描画

ご覧のように再帰の描画がずれたものになっている。

このずれの原因として考えられるものはオブジェクトを関数に渡す仕様がTurboPascalとPythonとで異なることにあるように思われた。そこで

def kame( positionT, size):
position = positionT.clone()

として関数の中で実引数が影響を受けないようにした。結果の描画は以下のようになる:

kameの再帰描画

これで期待した通りになったが、描画のアルゴリズムを変えてクローンを使わないことにしたい。

Pythonのタートル・グラフィックス(2):kameの単純再帰描画

Pythonのタートル・グラフィックス(1)で採り上げたKameの単純な再帰プログラムを考える。プログラムは以下のようなものである:


#coding: utf-8

import turtle

def sugata( center, size):
    for i in range(6):
        painter = center.clone()
        painter.fd(size)
        painter.rt(120)
        painter.fd(size)
        center.rt(60)

def kame( position, size):
    sugata( position, size)
    if size > 10:
        kame(position, size/2)
    
if __name__ == '__main__':
    
    turtle.clearscreen()
    position = turtle.Turtle()
    position.ht()
    position.lt(90)
    kame( position, 200)

実行結果の描画

kame2の描画

期待した結果である。

Pythonのタートル・グラフィックス(1):kameによるカメの描画

LibreLogoに関するブログを書いていたらPythonのタートル・グラフィックスことが気になった。

書架を見たら「Turbo Graphics」(安齋利洋・伊吹龍著;1987年)という昔使った本が目についた。これはTurbo Pascalを使ったグラフィックスの本である。この中にタートル・グラフィックスの例題が沢山あるので今回はこれらをPythonのタートル・グラフィックスで書いてみた。なるべく忠実に例題をなぞるようにした。

例題1:kame


#coding: utf-8

import turtle

def sugata( center, size):
    for i in range(6):
        painter = center.clone()
        painter.fd(size)
        painter.rt(120)
        painter.fd(size)
        center.rt(60)

def kame( position, size):
    sugata( position, size)
    
if __name__ == '__main__':
    
    turtle.clearscreen()
    position = turtle.Turtle()
    position.ht()
    position.lt(90)
    kame( position, 200)

多少気になったのは以下の部分である:

painter = center.clone()

Pascalでは単なる代入文でpainter=centerとなっている。Pythonでは代入文によるオブジェクトの代入はできない。copyモジュールのcopyメソッドを使ってみたが描画の一部が消えてしまう。deepcopyではTkinterのエラーがでる。Pythonのタートル・グラフィックのドキュメントにあったclone()で期待した通りの描画が得られた。clone()とcopyメソッドの相違は不明。

【結果の描画】

kame

なお描画ではカメ印(?)は隠してある。