用Python放一場浪漫的煙花秀!
程序君個人微信
和我聊聊編程和創業的事
加好友
作者丨集智專欄
https://jizhi.im/blog/post/py_make_fireworks
天天敲代碼的朋友,有沒有想過代碼也可以變得很酷炫又浪漫?今天就教大家用Python模擬出綻放的煙花,工作之餘也可以隨時讓程序為自己放一場煙花秀。
這個有趣的小項目並不複雜,只需一點可視化技巧,100餘行Python代碼和程序庫Tkinter,最後我們就能達到下面這個效果:
學完本教程後,你也能做出這樣的煙花秀。
整體概念梳理
我們的整個理念比較簡單。
如上圖示,我們這裡通過讓畫面上一個粒子分裂為X數量的粒子來模擬爆炸效果。粒子會發生「膨脹」,意思是它們會以恆速移動且相互之間的角度相等。這樣就能讓我們以一個向外膨脹的圓圈形式模擬出煙花綻放的畫面。經過一定時間後,粒子會進入「自由落體」階段,也就是由於重力因素它們開始墜落到地面,仿若綻放後熄滅的煙花。
基本知識:用Python和Tkinter設計煙花
這裡不再一股腦把數學知識全丟出來,我們邊寫代碼邊說理論。首先,確保你安裝和導入了Tkinter,它是Python的標準 GUI 庫,廣泛應用於各種各樣的項目和程序開發,在Python中使用 Tkinter 可以快速的創建 GUI 應用程序。
import as from import from import from import from import
除了Tkinter之外,為了能讓界面有漂亮的背景,我們也導入PIL用於圖像處理,以及導入其它一些包,比如time,random和math。它們能讓我們更容易的控制煙花粒子的運動軌跡。
Tkinter應用的基本設置如下:
root
為了能初始化Tkinter,我們必須創建一個Tk()根部件(root widget),它是一個窗口,帶有標題欄和由窗口管理器提供的其它裝飾物。該根部件必須在我們創建其它小部件之前就創建完畢,而且只能有一個根部件。
w "Hello Tkinter!"
這一行代碼包含了Label部件。該Label調用中的第一個參數就是父窗口的名字,即我們這裡用的「根」。關鍵字參數「text」指明顯示的文字內容。你也可以調用其它小部件:Button,Canvas等等。
w .pack root .mainloop
接下來的這兩行代碼很重要。這裡的打包方法是告訴Tkinter調整窗口大小以適應所用的小部件。窗口直到我們進入Tkinter事件循環,被root.mainloop()調用時才會出現。在我們關閉窗口前,腳本會一直在停留在事件循環。
將煙花綻放轉譯成代碼
現在我們設計一個對象,表示煙花事件中的每個粒子。每個粒子都會有一些重要的屬性,支配了它的外觀和移動狀況:大小,顏色,位置,速度等等。
""" 粒子在空中隨機生成隨機,變成一個圈、下墜、消失 屬性: """ class part
particles 類
- id: 粒子的id
- x, y: 粒子的坐標
- vx, vy: 在坐標的變化速度
- total: 總數
- age: 粒子存在的時長
- color: 顏色
- cv: 畫布
- lifespan: 最高存在時長
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 = idxself
.x = xself
.y = yself
.initial_speed = explosion_speedself
.vx = vxself
.vy = vyself
.total = totalself
.age = 0self.color = colorself
.cv = cvself
.cid =self
.cv.create_oval(x - size, y - size, x + size,
y + size, fill=
self
.color)self
.lifespan = lifespan如果我們回過頭想想最開始的想法,就會意識到必須確保每個煙花綻放的所有粒子必須經過3個不同的階段,即「膨脹」「墜落」和「消失」。 所以我們向粒子類中再添加一些運動函數,如下所示:
def update ( self
# 粒子膨脹if self.alive() and self.expand():
move_x = cos(radians(
self
.id*360
/self
.total))*self
.initial_speedmove_y = sin(radians(
self
.id*360
/self
.total))*self
.initial_speedself
.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 isnot
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坐標、大小、顏色、初始速度。
6 10 # 為所有模擬煙花綻放的全部粒子創建一列列表 for in range numb_explodenumb_explode = randint(
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
iin
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將其完全移除前的邊緣狀態)。
.0 1.8 while 1.8 0.01 for in for part in parttotal_time =
# 在
sleep(
tnew = time()
t, dt = tnew, tnew - t
cv.update()
total_time += dt
現在,我們只需將最後兩個gist合併為一個能被Tkinter調用的函數,就叫它simulate()吧。該函數會展示所有的數據項,並根據我們設置的時間更新每個數據項的屬性。在我們的主代碼中,我們會用一個alarm處理模塊after()調用此函數,after()會等待一定的時間,然後再調用函數。
我們這裡設置讓Tkinter等待100個單位(1秒鐘)再調取simulate。
if _ "__main__" 600 600 # 繪製一個黑色背景 0 0 600 600 "black"
root = tk.Tk()
cv = tk.Canvas(root, height=
cv.create_rectangle(
cv.pack()
root.protocol(
"WM_DELETE_WINDOW"
,close
)# 在1秒後才開始調用stimulate()
root.after(
100
, simulate, cv)root.mainloop()
好了,這樣我們就用Python代碼放了一場煙花秀:
本文只一個簡單版本,等進一步熟悉Tkinter後,還可以添加更多顏色更漂亮的背景照片,讓代碼為你綻放更美的煙花!
以下是全部代碼:
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
# 模擬重力
GRAVITY =
0
.05
# 顏色選項(隨機或者按順序)
colors = [
"red"
,"blue"
,"yellow"
,"white"
,"green"
,"orange"
,"purple"
,"seagreen"
,"indigo"
,"cornflowerblue"
]"""
particles 類
粒子在空中隨機生成隨機,變成一個圈、下墜、消失
屬性:
- id: 粒子的id
- x, y: 粒子的坐標
- vx, vy: 在坐標的變化速度
- total: 總數
- age: 粒子存在的時長
- color: 顏色
- cv: 畫布
- lifespan: 最高存在時長
"""
class
Particle
:
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 = idxself
.x = xself
.y = yself
.initial_speed = explosion_speedself
.vx = vxself
.vy = vyself
.total = totalself
.age = 0self.color = colorself
.cv = cvself
.cid =self
.cv.create_oval(x - size, y - size, x + size,
y + size, fill=
self
.color)self
.lifespan = lifespan
def
update
(
self
, dt):self
.age += dt
# 粒子範圍擴大
if
self
.alive()and
self
.expand():move_x = cos(radians(
self
.id *360
/self
.total)) *self
.initial_speedmove_y = sin(radians(
self
.id *360
/self
.total)) *self
.initial_speedself
.cv.move(self
.cid, move_x, move_y)self
.vx = move_x / (float(dt) *1000
)
# 以自由落體墜落
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 isnot
None:
cv.delete(
self
.cid)self
.cid = None
# 擴大的時間
def
expand
(
self
):return
self
.age <=1.2
# 粒子是否在最高存在時長內
def
alive
(
self
):return
self
.age <=self
.lifespan"""
循環調用保持不停
"""
def
simulate
(cv)
:t = time()
explode_points = []
wait_time = randint(
10
,100
)numb_explode = randint(
6
,10
)# 創建一個所有粒子同時擴大的二維列表
for
pointin
range(numb_explode):objects = []
x_cordi = randint(
50
,550
)y_cordi = randint(
50
,150
)speed = uniform(
0
.5
,1.5
)size = uniform(
0
.5
,3
)color = choice(colors)
explosion_speed = uniform(
0
.2
,1
)total_particles = randint(
10
,50
)for
iin
range(1
, total_particles):r = Particle(cv, idx=i, total=total_particles, explosion_speed=explosion_speed, x=x_cordi, y=y_cordi,
vx=speed, vy=speed, color=color, size=size, lifespan=uniform(
0
.6
,1.75
))objects.append(r)
explode_points.append(objects)
total_time = .
0
# 1.8s內一直擴大
while
total_time <1.8
:sleep(
0
.01
)tnew = time()
t, dt = tnew, tnew - t
for
pointin
explode_points:
for
itemin
point:
item.update(dt)
cv.update()
total_time += dt
# 循環調用
root.after(wait_time, simulate, cv)
def
close
(*ignore)
:"""退出程序、關閉窗口"""
global root
root.quit()
if
__name__
=="__main__"
:root = tk.Tk()
cv = tk.Canvas(root, height=
400
, width=600
)# 選一個好看的背景會讓效果更驚艷!
image = Image.open(
"./image.jpg"
)photo = ImageTk.PhotoImage(image)
cv.create_image(
0
,0
, image=photo, anchor="nw"
)cv.pack()
root.protocol(
"WM_DELETE_WINDOW"
, close)root.after(
100
, simulate, cv)root.mainloop()
推薦↓↓↓
長
按
關
注
??
【
16個技術公眾號
】都在這裡!
涵蓋:程序員大咖、源碼共讀、程序員共讀、數據結構與演算法、黑客技術和網路安全、大數據科技、編程前端、Java、Python、Web編程開發、Android、iOS開發、Linux、資料庫研發、幽默程序員等。
※編程中最困難的地方是取名
※python3 拼接字元串的7種方法
TAG:Python開發 |