The Nature of Code閱讀心得與Python實作:8.3 The Koch Curve

更新於 發佈於 閱讀時間約 24 分鐘

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

raw-image

製作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)

這裡頭的startend,都是pygame.Vector2物件。

KochLine類別中,還缺少一個可以用來將Koch曲線的製作規則作用在線段上的方法。Koch曲線的製作規則作用在線段上之後,線段的數量會從1條增至4條;如下圖所示:

raw-image

從圖中可以很清楚地看出來,只要知道a、b、c、d、e這5個點的位置,就足以描述那4條線段了。點a、e分別就是點start、end,所以a、b的位置是已知的。假設a、b、c、d、e的位置向量分別為abcde,令

v = (e - a)/3

則可以得到

b = a + v
c = b + vr
d = b + v

其中vrv逆時針旋轉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曲線了。假設線段的起點和終點分別為startend這兩個pygame.Vector2物件,要製作Koch曲線,程式可以這樣寫:

segment = KochLine(start, end)
[a, b, c, d, e] = segment.Koch_points()

這樣從abcde這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

raw-image
# 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曲線,會位於最初那條線段起點到終點所構成向量的左手邊。

raw-image

由左至右為迭代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曲線散架,所有線段向下掉落至地面。

raw-image
raw-image
raw-image
raw-image

主程式

# 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

raw-image

如上圖。底部起點與終點分別為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)


留言
avatar-img
留言分享你的想法!
avatar-img
ysf的沙龍
18會員
147內容數
寫點東西自娛娛人
ysf的沙龍的其他內容
2025/06/16
除了自我相似性之外,碎形的另一個基本組成部分是遞迴(recursion)。遞迴指的是,利用稱為製作規則(production rule)的同一套規則,不斷地進行迭代,而且每次迭代時,都會把上一次迭代的結果,作為這次迭代的起點。
Thumbnail
2025/06/16
除了自我相似性之外,碎形的另一個基本組成部分是遞迴(recursion)。遞迴指的是,利用稱為製作規則(production rule)的同一套規則,不斷地進行迭代,而且每次迭代時,都會把上一次迭代的結果,作為這次迭代的起點。
Thumbnail
2025/06/09
「fractal」這個字,有人翻譯成「碎形」,也有人翻譯成「分形」,是Benoit Mandelbrot在1975年,根據拉丁文中含有「零碎」、「破裂」意思的「fractus」這個字所造出來的。
Thumbnail
2025/06/09
「fractal」這個字,有人翻譯成「碎形」,也有人翻譯成「分形」,是Benoit Mandelbrot在1975年,根據拉丁文中含有「零碎」、「破裂」意思的「fractus」這個字所造出來的。
Thumbnail
2025/06/02
放眼望去,自然界中大部分的事物,長得都不是歐幾里得幾何圖形的樣子,也都不是歐幾里得幾何所能夠描述得很好的。如果我們想要創造一個像真實世界一樣的模擬世界,除了歐幾里得幾何之外,有沒有更適合的工具可以用呢?有的!這個更適合用來描述自然界事物模樣的幾何工具,就是這章要介紹的「碎形」(fractal)。
2025/06/02
放眼望去,自然界中大部分的事物,長得都不是歐幾里得幾何圖形的樣子,也都不是歐幾里得幾何所能夠描述得很好的。如果我們想要創造一個像真實世界一樣的模擬世界,除了歐幾里得幾何之外,有沒有更適合的工具可以用呢?有的!這個更適合用來描述自然界事物模樣的幾何工具,就是這章要介紹的「碎形」(fractal)。
看更多
你可能也想看
Thumbnail
每年4月、5月都是最多稅要繳的月份,當然大部份的人都是有機會繳到「綜合所得稅」,只是相當相當多人還不知道,原來繳給政府的稅!可以透過一些有活動的銀行信用卡或電子支付來繳,從繳費中賺一點點小確幸!就是賺個1%~2%大家也是很開心的,因為你們把沒回饋變成有回饋,就是用卡的最高境界 所得稅線上申報
Thumbnail
每年4月、5月都是最多稅要繳的月份,當然大部份的人都是有機會繳到「綜合所得稅」,只是相當相當多人還不知道,原來繳給政府的稅!可以透過一些有活動的銀行信用卡或電子支付來繳,從繳費中賺一點點小確幸!就是賺個1%~2%大家也是很開心的,因為你們把沒回饋變成有回饋,就是用卡的最高境界 所得稅線上申報
Thumbnail
全球科技產業的焦點,AKA 全村的希望 NVIDIA,於五月底正式發布了他們在今年 2025 第一季的財報 (輝達內部財務年度為 2026 Q1,實際日曆期間為今年二到四月),交出了打敗了市場預期的成績單。然而,在銷售持續高速成長的同時,川普政府加大對於中國的晶片管制......
Thumbnail
全球科技產業的焦點,AKA 全村的希望 NVIDIA,於五月底正式發布了他們在今年 2025 第一季的財報 (輝達內部財務年度為 2026 Q1,實際日曆期間為今年二到四月),交出了打敗了市場預期的成績單。然而,在銷售持續高速成長的同時,川普政府加大對於中國的晶片管制......
Thumbnail
重點摘要: 6 月繼續維持基準利率不變,強調維持高利率主因為關稅 點陣圖表現略為鷹派,收斂 2026、2027 年降息預期 SEP 連續 2 季下修 GDP、上修通膨預測值 --- 1.繼續維持利率不變,強調需要維持高利率是因為關稅: 聯準會 (Fed) 召開 6 月利率會議
Thumbnail
重點摘要: 6 月繼續維持基準利率不變,強調維持高利率主因為關稅 點陣圖表現略為鷹派,收斂 2026、2027 年降息預期 SEP 連續 2 季下修 GDP、上修通膨預測值 --- 1.繼續維持利率不變,強調需要維持高利率是因為關稅: 聯準會 (Fed) 召開 6 月利率會議
Thumbnail
高中數學主題練習—和差角公式
Thumbnail
高中數學主題練習—和差角公式
Thumbnail
高中數學主題練習—和差角公式
Thumbnail
高中數學主題練習—和差角公式
Thumbnail
這篇是給初學技術分析者的建議,覺得去蕪存菁,最簡潔有效的東西。 1.認識什麼是K線,開高低收,成交量。 2.知道均線與均量的數學意義。 3.學習簡單的走勢型態,比如W底M頭,切線,跳空缺口。 以上3點就足夠了,不管基於什麼說法想法理由,都不要花時間去學任何其他指標。
Thumbnail
這篇是給初學技術分析者的建議,覺得去蕪存菁,最簡潔有效的東西。 1.認識什麼是K線,開高低收,成交量。 2.知道均線與均量的數學意義。 3.學習簡單的走勢型態,比如W底M頭,切線,跳空缺口。 以上3點就足夠了,不管基於什麼說法想法理由,都不要花時間去學任何其他指標。
Thumbnail
高中數學主題練習—點與圓之關係
Thumbnail
高中數學主題練習—點與圓之關係
Thumbnail
高中數學主題練習—對數方程式
Thumbnail
高中數學主題練習—對數方程式
Thumbnail
高中數學主題練習—根式化簡
Thumbnail
高中數學主題練習—根式化簡
Thumbnail
高中數學主題練習—求空間中直線參數式
Thumbnail
高中數學主題練習—求空間中直線參數式
Thumbnail
上篇進一步認識基本的圖形架構與三大 Graph 算法,那首先從 shortest path 開始,我們會陸續去理解這些算法,以及可能的應用,如果還沒有看過上一篇的,可以點以下連結~那我們就開始吧! 【圖論Graph】Part1:初探圖形與圖形演算法之應用
Thumbnail
上篇進一步認識基本的圖形架構與三大 Graph 算法,那首先從 shortest path 開始,我們會陸續去理解這些算法,以及可能的應用,如果還沒有看過上一篇的,可以點以下連結~那我們就開始吧! 【圖論Graph】Part1:初探圖形與圖形演算法之應用
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News