慶祝法國隊奪冠:用Python放一場煙花秀

景略集智發表於2018-07-17

本文首發自集智專欄

程式碼也浪漫:用Python放一場煙花秀

參考資料:towardsdatascience.com


天天敲程式碼的朋友,有沒有想過程式碼也可以變得很酷炫又浪漫?今天就教大家用Python模擬出綻放的煙花慶祝昨晚法國隊奪冠,工作之餘也可以隨時讓程式為自己放一場煙花秀。

這個有趣的小專案並不複雜,只需一點視覺化技巧,100餘行Python程式碼和程式庫Tkinter,最後我們就能達到下面這個效果:

慶祝法國隊奪冠:用Python放一場煙花秀

學完本教程後,你也能做出這樣的煙花秀。

整體梳理概念

我們的整個理念比較簡單。

慶祝法國隊奪冠:用Python放一場煙花秀

如上圖示,我們這裡通過讓畫面上一個粒子分裂為X數量的粒子來模擬爆炸效果。粒子會發生“膨脹”,意思是它們會以恆速移動且相互之間的角度相等。這樣就能讓我們以一個向外膨脹的圓圈形式模擬出煙花綻放的畫面。經過一定時間後,粒子會進入“自由落體”階段,也就是由於重力因素它們開始墜落到地面,仿若綻放後熄滅的煙花。

用Python和Tkinter設計煙花:基本知識

這裡不再一股腦把數學知識全丟出來,我們邊寫程式碼邊說理論。首先,確保你安裝和匯入了Tkinter,它是Python的標準 GUI 庫,廣泛應用於各種各樣的專案和程式開發,在Python中使用 Tkinter 可以快速的建立 GUI 應用程式。

import tkinter as tk
from PIL import Image, ImageTk
from time import time, sleep
from random import choice, uniform, randint
from math import sin, cos, radians
複製程式碼

除了Tkinter之外,為了能讓介面有漂亮的背景,我們也匯入PIL用於影像處理,以及匯入其它一些包,比如time,random和math。它們能讓我們更容易的控制煙花粒子的運動軌跡。

Tkinter應用的基本設定如下:

root = tk.Tk()
複製程式碼

為了能初始化Tkinter,我們必須建立一個Tk()根部件(root widget),它是一個視窗,帶有標題欄和由視窗管理器提供的其它裝飾物。該根部件必須在我們建立其它小部件之前就建立完畢,而且只能有一個根部件。

w = tk.Label(root, text="Hello Tkinter!")
複製程式碼

這一行程式碼包含了Label部件。該Label呼叫中的第一個引數就是父視窗的名字,即我們這裡用的“根”。關鍵字引數“text”指明顯示的文字內容。你也可以呼叫其它小部件:Button,Canvas等等。

w.pack()
root.mainloop()
複製程式碼

接下來的這兩行程式碼很重要。這裡的打包方法是告訴Tkinter調整視窗大小以適應所用的小部件。視窗直到我們進入Tkinter事件迴圈,被root.mainloop()呼叫時才會出現。在我們關閉視窗前,指令碼會一直在停留在事件迴圈。

將煙花綻放轉譯成程式碼

現在我們設計一個物件,表示煙花事件中的每個粒子。每個粒子都會有一些重要的屬性,支配了它的外觀和移動狀況:大小,顏色,位置,速度等等。

'''
Generic class for particles

particles are emitted almost randomly on the sky, forming a round of circle (a star) before falling and getting removed
from canvas

Attributes:
    - id: identifier of a particular particle in a star
    - x, y: x,y-coordinate of a star (point of explosion)
    - vx, vy: speed of particle in x, y coordinate
    - total: total number of particle in a star
    - age: how long has the particle last on canvas
    - color: self-explantory
    - cv: canvas
    - lifespan: how long a particle will last on canvas
    - intial_speed: speed of particle at explosion

'''
class part:
    def __init__(self, cv, idx, total, explosion_speed, x=0., y=0., vx = 0., vy = 0., size=2., color = 'red', lifespan = 2, **kwargs):
        self.id = idx
        self.x = x
        self.y = y
        self.initial_speed = explosion_speed
        self.vx = vx
        self.vy = vy
        self.total = total
        self.age = 0
        self.color = color
        self.cv = cv
        self.cid = self.cv.create_oval(
            x - size, y - size, x + size,
            y + size, fill=self.color)
        self.lifespan = lifespan
複製程式碼

如果我們回過頭想想最開始的想法,就會意識到必須確保每個煙花綻放的所有粒子必須經過3個不同的階段,即“膨脹”“墜落”和“消失”。 所以我們向粒子類中再新增一些運動函式,如下所示:

def update(self, dt):
    # 粒子膨脹
    if self.alive() and self.expand():
        move_x = cos(radians(self.id*360/self.total))*self.initial_speed
        move_y = sin(radians(self.id*360/self.total))*self.initial_speed
        self.vx = move_x/(float(dt)*1000)
        self.vy = move_y/(float(dt)*1000)
        self.cv.move(self.cid, move_x, move_y)

    # 以自由落體墜落
    elif self.alive():
        move_x = cos(radians(self.id*360/self.total))
        # we technically don't need to update x, y because move will do the job
        self.cv.move(self.cid, self.vx + move_x, self.vy+GRAVITY*dt)
        self.vy += GRAVITY*dt

    # 如果粒子的生命週期已過,就將其移除
    elif self.cid is not None:
        cv.delete(self.cid)
        self.cid = None
複製程式碼

當然,這也意味著我們必須定義每個粒子綻放多久、墜落多久。這部分需要我們多嘗試一些引數,才能達到最佳視覺效果。

# 定義膨脹效果的時間幀
def expand (self):
    return self.age <= 1.2

# 檢查粒子是否仍在生命週期內
def alive(self):
    return self.age <= self.lifespan
複製程式碼

使用Tkinter模擬

現在我們將粒子的移動概念化,不過很明顯,一個煙花不能只有一個粒子,一場煙花秀也不能只有一個煙花。我們下一步就是讓Python和Tkinter以我們可控的方式向天上連續“發射”粒子。

到了這裡,我們需要從操作一個粒子升級為在螢幕上展現多個煙花及每個煙花中的多個粒子。

我們的解決思路如下:建立一列列表,每個子列表是一個煙花,其包含一列粒子。每個列表中的例子有相同的x,y座標、大小、顏色、初始速度。

numb_explode = randint(6,10)
# 為所有模擬煙花綻放的全部粒子建立一列列表
for point in range(numb_explode):
    objects = []
    x_cordi = randint(50,550)
    y_cordi = randint(50, 150)       
    size = uniform (0.5,3)
    color = choice(colors)
    explosion_speed = uniform(0.2, 1)
    total_particles = randint(10,50)
    for i in range(1,total_particles):
        r = part(cv, idx = i, total = total_particles, explosion_speed = explosion_speed, x = x_cordi, y = y_cordi, 
        color=color, size = size, lifespan = uniform(0.6,1.75))
        objects.append(r)
explode_points.append(objects)
複製程式碼

我們下一步就是確保定期更新粒子的屬性。這裡我們設定讓粒子每0.01秒更新它們的狀態,在1.8秒之後停止更新(這意味著每個粒子的存在時間為1.6秒,其中1.2秒為“綻放”狀態,0.4秒為“墜落”狀態,0.2秒處於Tkinter將其完全移除前的邊緣狀態)。

total_time = .0
# 在1.8秒時間幀內保持更新
while total_time < 1.8:
    sleep(0.01)
    tnew = time()
    t, dt = tnew, tnew - t
    for point in explode_points:
        for part in point:
            part.update(dt)
    cv.update()
    total_time += dt
複製程式碼

現在,我們只需將最後兩個gist合併為一個能被Tkinter呼叫的函式,就叫它simulate()吧。該函式會展示所有的資料項,並根據我們設定的時間更新每個資料項的屬性。在我們的主程式碼中,我們會用一個alarm處理模組after()呼叫此函式,after()會等待一定的時間,然後再呼叫函式。 我們這裡設定讓Tkinter等待100個單位(1秒鐘)再調取simulate。

if __name__ == '__main__':
    root = tk.Tk()
    cv = tk.Canvas(root, height=600, width=600)
    # 繪製一個黑色背景
    cv.create_rectangle(0, 0, 600, 600, fill="black")
    cv.pack()

    root.protocol("WM_DELETE_WINDOW", close)
    # 在1秒後才開始呼叫stimulate()
    root.after(100, simulate, cv)
    root.mainloop()
複製程式碼

好了,這樣我們就用Python程式碼放了一場煙花秀:

慶祝法國隊奪冠:用Python放一場煙花秀

本文只是基本版本,等你進一步熟悉Tkinter後,還可以新增更多顏色更漂亮的背景照片,讓程式碼為你綻放更美的煙花!

本文全部程式碼地址


0806期《人工智慧-從零開始到精通》限時折扣中!

戳這裡看詳情

談笑風生 線上程式設計 瞭解一下?

(前25位同學還可領取¥200優惠券哦)

相關文章