Koch曲線(Koch curve),碎形界另一個名氣響噹噹的圖案,是由瑞典數學家Helge von Koch在1904年所提出的。Koch曲線的製作規則非常簡單,就是把一段線段三等分,然後以中間那等分為底部畫出正三角形,接著把底部挖掉。不斷針對新形成的線段重複同樣的動作,就可以產生Koch曲線了。畫圖來看會更清楚一些:

製作Koch曲線時,經過1次迭代之後,可以得到上圖步驟3中的圖形;假設一開始時所用的線段長度是1,則這個圖形中所有線段的長度總和是4/3。繼續迭代下去,當迭代n次之後,圖形中所有線段的長度總和會是(4/3)n。所以,如果我們讓它迭代無窮多次,那最後圖形中所有線段的長度總和,會變成無窮大;這也就是說,Koch曲線的長度是無窮大。
Koch曲線的長度是無窮大,然後呢?這有什麼特別的嗎?不說不知道,說了嚇一跳。雖然Koch曲線的長度是無窮大,但是,夾在它和一開始時所用的那條線段之間的區域,面積居然是有限的;這個只要看一下前面那張圖就可以理解了,因為Koch曲線就只在那小小的一塊範圍中逐漸成形,沒有跑到外面去。Koch曲線這個怪異的性質,讓它和其他許多碎形圖案一樣,被認為是「數學怪物」,也讓它在「怪物曲線」(monster curve)家族中,佔有一席之地。製作Koch曲線的方法和製作Cantor集的方法差不多,寫個遞迴函數就可以了。不過,在這裡我們要改變一下寫法,改用物件導向的方式來寫;也就是把每一截構成Koch曲線的線段都當作個別的物件來處理。這樣做的好處是可以創造更多的可能性;例如,可以拿個別的線段來進行物理現象的模擬,也可以讓每個線段都有自己的顏色、寬度、位置等。
要把每一截構成Koch曲線的線段都當作物件來處理,可以設計一個類別來達到這個目的。既然這個類別是要用來描述線段,所以它應該含有線段的起點和終點這兩個屬性;除此之外,應該也要有用來把線段畫出來的方法。所以,這個用來描述Koch曲線線段的類別,可以這樣子設計:
class KochLine:
def __init__(self, start, end):
self.start = start.copy()
self.end = end.copy()
def show(self, surface):
pygame.draw.line(surface, (0, 0, 0), self.start, self.end)
這裡頭的start
和end
,都是pygame.Vector2
物件。
在KochLine
類別中,還缺少一個可以用來將Koch曲線的製作規則作用在線段上的方法。Koch曲線的製作規則作用在線段上之後,線段的數量會從1條增至4條;如下圖所示:

從圖中可以很清楚地看出來,只要知道a、b、c、d、e這5個點的位置,就足以描述那4條線段了。點a、e分別就是點start、end,所以a、b的位置是已知的。假設a、b、c、d、e的位置向量分別為a、b、c、d、e,令
v = (e - a)/3
則可以得到
b = a + v
c = b + vr
d = b + v
其中vr是v逆時針旋轉60度之後所得到的向量。根據上述推導的結果,用來將Koch曲線的製作規則作用在線段上的方法可以這樣設計:
def Koch_points(self):
a = self.start.copy()
e = self.end.copy()
vec = (e - a)/3
b = a + vec
c = b + vec.rotate(-60)
d = b + vec
return [a, b, c, d, e]
這裡要注意一下:在數學上,逆時針旋轉時,角度應該是正值,但因為pygame所使用的座標系統的y軸方向,和直角座標系統的y軸方向是相反的,所以逆時針旋轉時,角度會是負值。
設計好KochLine
類別之後,就可以開始製作Koch曲線了。假設線段的起點和終點分別為start
及end
這兩個pygame.Vector2
物件,要製作Koch曲線,程式可以這樣寫:
segment = KochLine(start, end)
[a, b, c, d, e] = segment.Koch_points()
這樣從a
、b
、c
、d
、e
這5個點,就可以得到構成Koch曲線的4條線段了。
得到構成Koch曲線的4條線段了;然後呢?要製作更細緻的Koch曲線,這4條線段在下一次迭代時,還得分別再用製作規則把它們轉變成更短的線段;這個過程重複越多次,製作出來的Koch曲線會越細緻。所以,除了第1次迭代之外,每次迭代要用製作規則處理的線段都不只一條,而且處理過後所得到的新的線段,還會再次以同樣的方式處理。既然如此,那把這個部分寫成函數,會是比較好的做法;這個函數可以這樣寫:
def generate(segments):
new_segments = []
for segment in segments:
[a, b, c, d, e] = segment.Koch_points()
new_segments.append(KochLine(a, b))
new_segments.append(KochLine(b, c))
new_segments.append(KochLine(c, d))
new_segments.append(KochLine(d, e))
return new_segments
參數segments
是個list,裡頭所放的,是KochLine
物件,也就是要處理的線段;而傳回來的,也是個裝著KochLine
物件的list,這些KochLine
物件,就是新得到的線段。
在下面這個例子中,我們就利用KochLine
類別及generate()
函數來製作Koch曲線。
Example 8.5: The Koch Curve

# python version 3.10.9
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 8.5: The Koch Curve")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 320
screen = pygame.display.set_mode(screen_size)
FPS = 30
frame_rate = pygame.time.Clock()
screen.fill(WHITE)
# 迭代次數
n = 5
# 線段起點與終點
start = pygame.Vector2(50, HEIGHT-50)
end = pygame.Vector2(WIDTH-50, HEIGHT-50)
segments = [KochLine(start, end)]
for i in range(n):
segments = generate(segments)
for segment in segments:
segment.show(screen)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
pygame.display.update()
frame_rate.tick(FPS)
Exercise 8.2
要注意線段起點到終點的方向。迭代之後所得到的Koch曲線,會位於最初那條線段起點到終點所構成向量的左手邊。

由左至右為迭代1~5次所得到的Koch曲線。主程式如下:
# python version 3.10.9
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 8.2")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 320
screen = pygame.display.set_mode(screen_size)
FPS = 30
frame_rate = pygame.time.Clock()
screen.fill(WHITE)
# 迭代次數
n = 5
for i in range(n):
# 三角形的三個頂點
vertex1 = pygame.Vector2(10+130*i, HEIGHT/2)
vertex2 = pygame.Vector2(110+130*i, HEIGHT/2)
vertex3 = vertex1 + (vertex2 - vertex1).rotate(-60)
# 要製作Koch曲線的三條線段。凸起部分會位於線段起點到終點所構成向量的左手邊
segments = [KochLine(vertex1, vertex3)]
segments.append(KochLine(vertex3, vertex2))
segments.append(KochLine(vertex2, vertex1))
for _ in range(i):
segments = generate(segments)
for segment in segments:
segment.show(screen)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
pygame.display.update()
frame_rate.tick(FPS)
Exercise 8.3
由左至右畫出Koch曲線。畫完之後,Koch曲線散架,所有線段向下掉落至地面。




主程式
# python version 3.10.9
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 8.3")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 520
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
CEIL, FLOOR = WIDTH/3, HEIGHT-50
start = pygame.Vector2(50, CEIL)
end = pygame.Vector2(WIDTH-50, CEIL)
segments = [KochLine(start, end)]
for i in range(3):
segments = generate(segments)
# 由左至右畫出的線段數量
n = 1
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
for i in range(n):
segments[i].show(screen)
if n < len(segments):
n += 1
else:
# Koch曲線散架掉落
for segment in segments:
speed = 5*random.random()
segment.start.y = segment.start.y+speed if segment.start.y<FLOOR else FLOOR
segment.end.y = segment.end.y+speed if segment.end.y<FLOOR else FLOOR
pygame.display.update()
frame_rate.tick(FPS)
Exercise 8.4
class CantorLine:
def __init__(self, start, end):
self.start = start
self.end = end
def Cantor_points(self):
a = self.start.copy()
d = self.end.copy()
vec = (d - a)/3
b = a + vec
c = b + vec
return [a, b, c, d]
def show(self, surface):
pygame.draw.line(surface, (0, 0, 0), self.start, self.end)
def generate(segments):
new_segments = []
for segment in segments:
[a, b, c, d] = segment.Cantor_points()
new_segments.append(CantorLine(a, b))
new_segments.append(CantorLine(c, d))
return new_segments
# python version 3.10.9
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 8.4")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 320
screen = pygame.display.set_mode(screen_size)
FPS = 30
frame_rate = pygame.time.Clock()
screen.fill(WHITE)
# 迭代次數
generations = 5
# 兩次迭代所得圖案之間的距離
shift = 20
start = pygame.Vector2(50, 70)
end = pygame.Vector2(WIDTH-50, 70)
segment = CantorLine(start, end)
segment.show(screen)
segments = [segment]
for generation in range(generations):
for segment in segments:
segment.start.y += shift
segment.end.y += shift
segments = generate(segments)
for segment in segments:
segment.show(screen)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
pygame.display.update()
frame_rate.tick(FPS)
Exercise 8.5

如上圖。底部起點與終點分別為start及end之正三角形,迭代一次後會產生3個底部分為ab、cd、de之黑色正三角形。利用這個特性,將generate()
函數改用遞迴的方式來寫。
class SierpinskiTriangle :
def __init__(self, start, end):
# 正三角形底邊起點與終點
self.base_start = start.copy()
self.base_end = end.copy()
# 正三角形底邊頂點
self.apex = self.base_start + (self.base_end - self.base_start).rotate(-60)
# 正三角形邊長
self.side_length = (end-start).length()
def Sierpinski_points(self):
a = (self.apex + self.base_start)/2
b = (self.apex + self.base_end)/2
c = self.base_start
d = (self.base_start + self.base_end)/2
e = self.base_end
return [a, b, c, d, e]
def show(self, surface):
points = [self.apex, self.base_start, self.base_end]
pygame.draw.polygon(surface, (0, 0, 0), points)
def generate(bases):
if bases[0].side_length < 5:
return bases
new_bases = []
for base in bases:
[a, b, c, d, e] = base.Sierpinski_points()
new_bases.append(SierpinskiTriangle(a, b))
new_bases.append(SierpinskiTriangle(c, d))
new_bases.append(SierpinskiTriangle(d, e))
return generate(new_bases)
# python version 3.10.9
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 8.5")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 320
screen = pygame.display.set_mode(screen_size)
FPS = 30
frame_rate = pygame.time.Clock()
screen.fill(WHITE)
# 正三角形底邊起點與終點
start = pygame.Vector2(200, HEIGHT-50)
end = pygame.Vector2(WIDTH-200, HEIGHT-50)
bases = [SierpinskiTriangle(start, end)]
bases = generate(bases)
for base in bases:
base.show(screen)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
pygame.display.update()
frame_rate.tick(FPS)