一、前言
作爲一名python語言的初學者,在看其官方文檔時,看到標準庫裏有一個海龜畫圖,感覺挺好玩的。於是研究了一下,便依葫蘆畫瓢,自己畫一個簡單的棒棒糖 🍭(畫了哄小孩子的)。只是這個海龜畫圖好象有些眼熟,努力回憶,想起自己好幾年前看過一本《分形算法與程序設計–Java實現》(2003年出版的,孫博文編著),裏面介紹了一種LS文法,和這很相似。那本書比較老,因此裏面的代碼也是比較古老的,還是使用的applet
。那本書裏介紹了幾種分形算法,其中LS文法與海龜繪圖原理一樣。既然要自學python,那就要編碼實踐,所以打算使用python的海龜繪圖來實現這個LS文法繪製。
二、LS文法
文法構圖算法是仿照語言學中的語法生成方法來構造圖形的一種算法。
美國著名語言學家喬姆斯基(N.Chomsky)在20世紀50年代給出了遞歸生成語法的方法:指定一個或者幾個初始字母和一組“生成規則”,將生成規則反覆作用到初始字母和新生成的字母上,產生出整個語言。這就是由“生成語法”定義的形式語言,例如:
字母表: L,R
生成規則: L -> R,R -> LR
初始字母: R
則有 R -> LR -> RLR -> LRRLR -> RLRLRRLR -> LRRLRRLRLRRLR ->…
LS文法於1984年由A.R.Smith首次引入到計算機圖形學領域。
在二維平面上,LS文法的圖形生成過程,類似於海龜在沙灘上行走。海龜行走的每一時刻的狀態定義爲當前位置矢量T與前進方向角δ的集合(T,δ),則二維LS文法字母表的繪圖規則如下:
F
:在當前方向前進一步,並畫線f
:在當前方向前進一步,不畫線+
:逆時針旋轉一個角度δ-
:順時針旋轉一個角度δ[
:將當前信息壓棧]
:將[
時刻的信息出棧
舉一個擬在後面實現的例子:Koch曲線。它的LS文法如下:
- ω:F
- δ:60°
- P:F -> F+F–F+F
則有:
- 步驟0:F
- 步驟1:F+F–F+F
- 步驟2:F+F–F+F+F+F–F+F–F+F–F+F+F+F–F+F
- 步驟3:F+F–F+F+F+F–F+F–F+F–F+F+F+F–F+F+F+F–F+F+F+F–F+F–F+F–F+F+F+F–F+F–F+F–F+F+F+F–F+F–F+F–F+F+F+F–F+F+F+F–F+F+F+F–F+F–F+F–F+F+F+F–F+F
三、python下簡單實現
python實現需要利用海龜繪圖這個模塊。因爲LS文法可以畫很多圖形,所以爲了複用,先寫LS方法模塊。
打開IDLE
,新建一個文件,輸入如下代碼:
# LS文法生成圖形的庫(公用)
from turtle import *
# 初始化原點及畫筆
def init(x,y,_speed):
ht()
up()
speed(_speed)
setx(x)
sety(y)
color('red')
down()
# 在當前方向向前走一步,並畫線
def drawF(step):
forward(step)
# 在當前方向向前走一步,不畫線
def drawf(step):
up()
forward(step)
down()
# 反時針旋轉a度
def drawPlus(a):
left(a)
# 順時針旋轉a度
def drawMinus(a):
right(a)
# 壓棧
def push():
pass
# 出棧
def pop():
pass
def draw(ls,step,angel,x,y,_speed):
init(x,y,_speed)
for c in ls:
if c == 'F':
drawF(step)
elif c == 'f':
drawf(step)
elif c == '+':
drawPlus(angel)
elif c == '-':
drawMinus(angel)
elif c == '[':
push()
elif c == ']':
pop()
else:
print("invalid char:",c)
done()
# LS文案生成,返回一個list
# 參數分別爲初始條件(list),規則(字典)和迭代次數
def getLs(origin,rule,n):
L = []
for key in origin:
if key in rule:
des = rule[key]
L += list(des)
else:
L.append(key)
if n == 1:
return L
else:
return getLs(L,rule,n-1)
Draw,GetLs = (draw,getLs)
這其中getLs
是遞歸調用來生成LS文法字符串,壓棧和出棧操作現在還暫缺。第一現階段沒有用到[
和]
,第二作爲一個初學者,沒有反覆嘗試、實際運行和來回修改我也沒辦法憑空寫出相應的代碼。
將文件保存爲drawLs.py,然後再新建一個文件,用來定義Koch曲線的初始規則和條件。
# Koch 曲線LS方法生成
from drawLs import Draw,GetLs
# 定義初始變量
step = 20 # 步長
n = 3 #迭代次數
# 初始條件
origin = 'F'
angel = 60
# 迭代規則
rule = { 'F' : "F+F--F+F" }
# 初始座標
x = -300
y = 0
# 動畫速度
speed = 'normal'
if __name__ == "__main__":
ls = GetLs(list(origin),rule,n)
Draw(ls,step,angel,x,y,speed)
保存爲同一目錄下的Koch_ls.py。然後點擊IDLE
菜單上的run
,Run Module
,一條紅色的Koch曲線就慢慢的畫出來了。
改變初始條件或者初始規則就可以得到不同的圖形,有興趣的讀者可以多做嘗試。
比如下例條件:
# 初始條件
origin = 'F++F++F'
angel = 60
# 迭代規則
rule = { 'F' : "F+F--F+F" }
畫出的圖形如下:
四、增加壓棧和出棧操作
上面的Koch曲線是一條連續的曲線,可以利用一隻畫筆一氣呵成不間斷畫完。但是如果我們想畫一個如下的樹,有主幹有分叉,一隻畫筆不間斷繪畫就無法完成了。
這裏我們需要在分支的地方保存當前的畫筆狀態,也就是將畫筆入棧。在分支畫完以後取出棧中的畫筆作爲主幹畫筆再接着畫。
一起來看這個分形樹的初始條件和規則:
- ω:F
- δ:25°
- P:F -> F[-F]F[+F]F
首先需要改寫drawLs.py,加入當前畫筆和堆棧相關的操作,完成後的代碼如下:
# LS文法生成圖形的庫(公用)
from turtle import *
# 全局變量
stack = []
class Pen:
current:None
# 初始化原點及畫筆
def init(x,y,_speed):
ht()
up()
Pen.current = getpen()
speed(_speed)
setx(x)
sety(y)
color('red')
down()
# 在當前方向向前走一步,並畫線
def drawF(step):
Pen.current.forward(step)
# 在當前方向向前走一步,不畫線
def drawf(step):
Pen.current.up()
Pen.current.forward(step)
Pen.current.down()
# 反時針旋轉a度
def drawPlus(a):
Pen.current.left(a)
# 順時針旋轉a度
def drawMinus(a):
Pen.current.right(a)
# 壓棧
def push():
pen = Pen.current.clone()
stack.append(pen)
# 出棧
def pop():
Pen.current = stack.pop()
def draw(ls,step,angel,x,y,_speed):
init(x,y,_speed)
for c in ls:
if c == 'F':
drawF(step)
elif c == 'f':
drawf(step)
elif c == '+':
drawPlus(angel)
elif c == '-':
drawMinus(angel)
elif c == '[':
push()
elif c == ']':
pop()
else:
print("invalid char:",c)
done()
# LS文案生成,返回一個list
# 參數分別爲初始條件(list),規則(字典)和迭代次數
def getLs(origin,rule,n):
L = []
for key in origin:
if key in rule:
des = rule[key]
L += list(des)
else:
L.append(key)
if n == 1:
return L
else:
return getLs(L,rule,n-1)
Draw,GetLs = (draw,getLs)
可以看到,改寫後對外的接口沒有變化,運行Koch_ls.py,你仍然會得到相同的結果。代碼中使用了一個全局變量來記錄當前畫筆,壓棧時就克隆當前畫筆然後保存,出棧時就將彈出的畫筆作爲當前畫筆。作爲一個python初學者,我在全局變量引用這裏也耽誤了一點時間(因爲我也沒有仔細看過文檔)。全局變量在函數中除非聲明爲global
,否則只能讀取而不能直接賦值改變(可以間接改變)。直接給全局變量賦值相當於創建了一個同名的局部變量,這一點和其它語言不相同,還是有些不習慣。
接着實現上面的分形樹,再新建一個Tree_ls.py,代碼如下:
# 分形樹實現
from drawLs import Draw,GetLs
# 定義初始變量
step = 10 # 步長
n = 3 #迭代次數
# 初始條件
origin = 'F'
angel = 25
# 迭代規則
rule = { 'F' : "F[-F]F[+F]F" }
# 初始座標
x = 0
y = 0
# 動畫速度
speed = 'fast'
if __name__ == "__main__":
ls = GetLs(list(origin),rule,n)
Draw(ls,step,angel,x,y,speed)
畫出的樹如下圖:
可以看到,我們的樹變成橫向生長了,這是因爲我們的畫筆(海龜)的初始方向是水平的,接下來我們對它進行完善。
五、進一步完善
計劃完善的地方有:
- 可以提供初始方向(角度)
- 可以增加渲染速度
我們給drawLs.py中的draw()
方法增加兩個參數,head
和_tracer
,分別代表初始方向和渲染的條件。相應的也要修改該文件的其它部分代碼,修改完成後代碼如下:
# LS文法生成圖形的庫(公用)
from turtle import *
# 全局變量
stack = []
class Pen:
current:None
# 初始化原點及畫筆
def init(x,y,_speed,head,_tracer=None):
ht()
up()
Pen.current = getpen()
speed(_speed)
if _tracer:
tracer(_tracer,0)
setx(x)
sety(y)
color('red')
seth(head)
down()
# 在當前方向向前走一步,並畫線
def drawF(step):
Pen.current.forward(step)
# 在當前方向向前走一步,不畫線
def drawf(step):
Pen.current.up()
Pen.current.forward(step)
Pen.current.down()
# 反時針旋轉a度
def drawPlus(a):
Pen.current.left(a)
# 順時針旋轉a度
def drawMinus(a):
Pen.current.right(a)
# 壓棧
def push():
pen = Pen.current.clone()
stack.append(pen)
# 出棧
def pop():
Pen.current = stack.pop()
def draw(ls,step,angel,x,y,_speed,head,_tracer):
init(x,y,_speed,head,_tracer)
for c in ls:
if c == 'F':
drawF(step)
elif c == 'f':
drawf(step)
elif c == '+':
drawPlus(angel)
elif c == '-':
drawMinus(angel)
elif c == '[':
push()
elif c == ']':
pop()
else:
print("invalid char:",c)
if _tracer:
update()
done()
# LS文案生成,返回一個list
# 參數分別爲初始條件(list),規則(字典)和迭代次數
def getLs(origin,rule,n):
L = []
for key in origin:
if key in rule:
des = rule[key]
L += list(des)
else:
L.append(key)
if n == 1:
return L
else:
return getLs(L,rule,n-1)
Draw,GetLs = (draw,getLs)
我們在Tree_ls.py中也增加這兩個初始條件:
# 分形樹實現
from drawLs import Draw,GetLs
# 定義初始變量
step = 6 # 步長
n = 4 #迭代次數
# 初始條件
origin = 'F'
angel = 25
# 迭代規則
rule = { 'F' : "F[-F]F[+F]F" }
# 初始座標
x = 0
y = -200
# 動畫速度
speed = 'fast'
# 初始角度
head = 90
# 渲染速度
tracer = 5
if __name__ == "__main__":
ls = GetLs(list(origin),rule,n)
Draw(ls,step,angel,x,y,speed,head,tracer)
我們將初始角度設定爲正北方,並且加快了渲染速度,生成圖形如下:
然而這裏面也遇到了一個坑,如果你使用了trace()
方法來設定渲染條件,那麼只有符合條件的繪製渲染出來了,未符合條件的繪製並沒有顯示。作爲一個初學者,我也是有些蒙圈😂😂😂。後來仔細看了一下文檔,看到介紹有update()
方法,試了下,果然如此🤝🤝🤝。它的作用是在你設置渲染條件的情況下強制刷新一下,這樣所有的部分都會顯示出來。
同樣,我們修改Koch_ls.py的代碼來增加這兩個初始條件:
# Koch 曲線LS方法生成
from drawLs import Draw,GetLs
# 定義初始變量
step = 10 # 步長
n = 4 #迭代次數
# 初始條件
origin = 'F++F++F'
angel = 60
# 迭代規則
rule = { 'F' : "F+F--F+F" }
# 初始座標
x = -300
y = -350
# 動畫速度
speed = 'fast'
# 初始角度
head = 0
# 渲染速度
tracer = 5
if __name__ == "__main__":
ls = GetLs(list(origin),rule,n)
Draw(ls,step,angel,x,y,speed,head,tracer)
六、繪製其它圖形
讓我們任意改變一下分形樹的規則,將 F -> F[-F]F[+F]F
後面加一個F改成 F -> F[-F]F[+F]FF
。生成的圖形如下:
接着上面的修改,將初始條件變成origin = 'F[+F][-F]F'
,生成的圖形如下圖:
柳枝的繪製:
- ω:F
- δ:20°
- P:F -> F[-F]F[+F]-F
手帕的繪製:
# 定義初始變量
step = 20 # 步長
n = 3 #迭代次數
# 初始條件
origin = 'F+F+F+F'
angel = 90
# 迭代規則
rule = { 'F' : "F[F]+F-F[++F]-F+F" }
# 初始座標
x = 200
y = -200
# 動畫速度
speed = 'fast'
# 初始角度
head = 90
# 渲染速度
tracer = 250
七、總結
LS文法繪製分形圖形是先多次迭代後生成圖形的路徑,然後用畫筆分別描繪出來。這裏只學習了LS單規則文法,還有多規則文法。多規則文法簡單的講就是增加規則表中的字母種類,有的只作替換(比如X),有的既作替換,也做繪製(比如F)。有興趣的讀者可以自己閱讀一下相關書籍。
這裏的LS文法路徑是預先生成好並保存在list中的長串字符,當規則複雜,迭代次數很多時並不是很高效。下一步看能否用一個生成器代替。
歡迎大家指出錯誤或者提出改進意見。