canvas實現漂亮的下雨效果
說明
這篇文章說如何用canvas畫出漂亮的下雨效果,先看看最後實現的效果吧。
效果圖
解釋
看圖來分析下,我們需要實現哪些效果。
1、雨滴下落效果,移動滑鼠控制下落方向
2、雨滴下落散成小水珠,小水珠的移動方向和滑鼠移動方向相同
3、雨滴下落到滑鼠坐標一定範圍內,散成小水珠,同樣的,小水珠的移動方向也和滑鼠移動方向相同
好的,我們把整個效果大致拆分成三個效果,實現這三個效果,就完成了。
我們一步一步來實現。
1、雨滴下落效果,移動滑鼠控制下落方向
實現整個效果的思路就是,
初始時
用一個數組保存雨滴對象。
一個雨滴對象裡面有各個屬性用來表示,雨滴的x坐標,y坐標,長度,下落速度,顏色,判斷是否刪除的標誌位
更新動畫時
往數組中添加一定數量的雨滴對象,然後遍曆數組,修改每個雨滴對象的x坐標和y坐標,用canvas根據雨滴對象的坐標,畫出兩個點,連起來就是一個雨滴了。
所以實現效果的重點就在坐標上
初始化一個雨滴的時候
雨滴x坐標:一個隨機數
雨滴y坐標:-100,這樣是為了讓雨滴從可視區域外進來
更新動畫時
雨滴x坐標:原x坐標的值 + speed * speedx
speed 是一個固定的值,表示雨滴下落速度,
speedx 是一個和滑鼠移動方向有關係的變數,
speedx = speedx + (maxspeedx - speedx) / 50
而maxspeedx 是根據滑鼠移動方向得到的一個值
maxspeedx = (e.clientX - canvasEl.clientWidth / 2) / (canvasEl.clientWidth / 2),
e.clientX:滑鼠距離可視區域左邊的值
canvasEl.clientWidth:整個可視區域的寬度
也就是說 speedx 是一個逐漸接近maxspeedx 的值
maxspeedx 的取值範圍是 -1 到 1,他的值越接近 -1,說明方向越向左,值越接近1,說明方向越向右。
為什麼不直接用maxspeedx ?
這是為了讓雨滴變化方向的速度不要那麼快,不要跟隨滑鼠變化方向立即改變,要有點點的延遲,看上去更好些。
如果用maxspeedx ,是這樣的效果
如果用speedx ,是這樣的效果
雨滴y坐標:原y坐標的值 + speed
speed 和上面x坐標中提到的一樣,是一個固定值,表示雨滴下落速度,
好的,最後就是用canvas根據雨滴對象的坐標,畫兩個點了,然後連起來,雨滴就畫出來了
第一個點坐標比較簡單,直接獲取雨滴對象的x坐標和y坐標,就是這個點的坐標
第二個點的坐標:
x坐標 = 雨滴x坐標的值 + 雨滴長度 * speedx
y坐標 = 雨滴y坐標的值 + 雨滴長度
最後把這兩個點連起來,就有一條線了,就是一個雨滴了
當設置x坐標時,又用上了變數 speedx,這是為了讓 雨滴方向 和 雨滴下落方向相同,
當不用 speedx時,是這樣
當用上speedx時,是這樣
2、雨滴下落散成小水珠,小水珠的移動方向和滑鼠移動方向相同
這裡的思路其實,和上面的效果有些相似
初始時
用一個數組保存小水珠對象。
一個小水珠,其實就是畫一個圓弧。
一個小水珠對象裡面有各個屬性用來表示,小水珠的坐標,x軸移動速度,y軸移動速度,圓的半徑,判斷是否刪除的標誌位。
更新動畫時
往數組中添加一定數量的小水珠對象,然後遍曆數組,修改每個小水珠對象的x坐標和y坐標,用canvas根據小水珠對象的坐標屬性 和 半徑屬性,畫一個圓弧。
所以實現效果的重點還在坐標上
初始化一個小水珠的時候
小水珠是雨滴消失的時候出現的,所以小水珠的坐標也是根據雨滴的坐標來的,刪除一個雨滴,就出現一些小水珠,而且小水珠的移動方向也是和雨滴下落方向,滑鼠移動方向一樣,所以還是會需要上面提到的變數 speedx,
小水珠x坐標: 刪除的雨滴x坐標 + 刪除的雨滴長度 * speedx
小水珠y坐標:刪除的雨滴y坐標 + 刪除的雨滴長度
更新動畫時
這裡要用到小水珠對象的兩個屬性 vx(x軸的值 的變化速度) 和vy(y軸的值 的變化速度),
小水珠的x坐標
vx = vx + speedx / 2
小水珠的x坐標 =原x坐標 + vx,
speedx:上面提到的和滑鼠移動方向相關的一個變數,這裡的作用就是用來控制小水珠的移動方向和其他方向相同
speedx / 2,除2是為了使 讓小水珠 在x軸的移動距離短一點,看上去更真實點
小水珠的y坐標
vy = vy + gravity
小水珠的y坐標 = 原y坐標 + vy;,
vy:一個負數
gravity:重力,一個正數,完整代碼里設置的是0.5
因為 原y坐標 是一個正數,這樣小水珠y坐標的值,就會先減小後增大,這是為了實現小水珠會先上升後下降的效果,看圖
最後就是用canvas根據小水珠的坐標屬性和半徑屬性畫圓弧就可以了,弧度是隨機的
3、雨滴下落到滑鼠坐標一定範圍內,散成小水珠,同樣的,小水珠的移動方向也和滑鼠移動方向相同
確定圖中圓的大小容易,假設圓的半徑是35,我們能獲取到滑鼠的坐標,以滑鼠的坐標為圓心,35為半徑,就確定了圓的大小。
重點在於如何判斷,雨滴是不是進入了這個範圍,這就要用勾股定理了,看圖。
因為雨滴是兩個點連起來的一條線,要看雨滴是不是進入了這個範圍內, 就是看雨滴靠下邊的點的坐標,到滑鼠的直線距離是多少,就是圖中AB線段的長度。
勾股定理:直角三角形的兩條直角邊的平方和等於斜邊的平方。
AB = Math.sqrt(BC*BC + AC * AC)
BC = 雨滴x坐標 - 滑鼠x坐標
AC = 雨滴y坐標 - 滑鼠y坐標
Math.sqrt()方法用來計算一個數的平方根
我們知道雨滴到滑鼠的直線距離後,和圓的半徑比較下,大於半徑就不在範圍內,否則就是在了。
如果在範圍內,就刪除雨滴,畫一些小水珠。
總結
要實現這個效果,麻煩的地方在於方向,雨滴方向,雨滴下落方向,小水珠移動方向,而這些都和滑鼠移動方向相關,確定各種方向後,根據距離,用canvas不斷的畫線,畫圓弧就行了。
完整代碼
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<canvas id="canvas" stylex="position: absolute; height: 100%; width:100%;"></canvas>
<script>
window.onload = main;
function main() {
// 獲取canvas元素
var canvasEl = document.getElementById("canvas");
var ctx = canvasEl.getContext("2d");
// canvas畫布的 背景顏色
var backgroundColor = "#000";
// canvas畫布的寬 等於 可視區域的寬
canvasEl.width = canvasEl.clientWidth;
// canvas畫布的高 等於 可視區域的高
canvasEl.height = canvasEl.clientHeight;
// 保存小水珠的數組
// 雨滴下落後散成小水珠,小水珠就是一些圓弧
var dropList = [];
// 重力
// 雨滴下落後散成小水珠,小水珠會先上升後下降,主要是因為 gravity 這個變數的緣故
var gravity = 0.5;
// 保存雨滴的數組
// 每個雨滴 都是 畫的一條線
var linelist = [];
// 保存滑鼠的坐標
// mousePos[0] 代表x軸的值,mousePos[1] 代表y軸的值
var mousePos = [0, 0];
// 跟隨滑鼠, mouseDis 大小區域內的雨滴會消失,形成散落效果
// 以mousePos為圓心,mouseDis為半徑,這個範圍內的雨滴 都會散開,形成許多小水珠
var mouseDis = 35;
// 更新一次動畫,畫lineNum 條雨滴,lineNum 值越大,下雨就越密集
var lineNum = 3;
// 跟隨滑鼠方向 變化下雨方向的 速度
// 滑鼠移動後,下雨的方向 會慢慢改變,主要靠speedx 這個變數
var speedx = 0;
// maxspeedx 為 speedx 可以取的最大值
// 當 speedx = maxspeedx時,下雨方向 會 隨滑鼠移動方向立即改變
var maxspeedx = 0;
// 頁面大小發生變化時,重置canvas畫布大小
window.onresize = function () {
canvasEl.width = canvasEl.clientWidth;
canvasEl.height = canvasEl.clientHeight;
}
//移動滑鼠觸發事件
window.onmousemove = function (e) {
// 設置mousePos 等於 滑鼠坐標
// e.clientX 為距離 瀏覽器窗口可視區域 左邊的距離
// e.clientY 為距離 瀏覽器窗口可視區域 上邊的距離
mousePos[0] = e.clientX;
mousePos[1] = e.clientY;
// 通過滑鼠位置,設置 maxspeedx的值,取值範圍是 -1 到 1
// maxspeedx的值,關係到
// 1、雨滴的方向
// 2、雨滴下落的方向
// 3、雨滴下落方向 跟隨 滑鼠移動方向變化的速度
// 4、小水珠的移動方向
// 值越接近1,表示方向越向右
// 值越接近-1,表示方向越向左
maxspeedx = (e.clientX - canvasEl.clientWidth / 2) / (canvasEl.clientWidth / 2);
}
// 根據參數,返回一個rgb顏色,用於給雨滴設置顏色
function getRgb(r, g, b) {
return "rgb(" + r + "," + g + "," + b + ")";
}
// 畫 一滴雨(一條線)
function createLine(e) {
// 隨機生成 雨滴的長度
var temp = 0.25 * (50 + Math.random() * 100);
// 一個 line 對象,代表一個雨滴
var line = {
// 雨滴下落速度
speed: 5.5 * (Math.random() * 6 + 3),
// 判斷是否刪除,值為true就刪除
die: false,
// 雨滴x坐標
posx: e,
// 雨滴y坐標
posy: -50,
// 雨滴的長度
h: temp,
// 雨滴的顏色
color: getRgb(Math.floor(temp * 255 / 75), Math.floor(temp * 255 / 75), Math.floor(temp * 255 / 75))
};
// 把創建好的line(雨滴)對象,添加到保存雨滴的數組
linelist.push(line);
}
// 畫一個小水珠(雨滴散開後的小水珠就是一個個的圓弧)
function createDrop(x, y) {
// 一個 drop 對象,代表一個圓弧
var drop = {
// 判斷是否刪除,值為true就刪除
die: false,
// 圓弧圓心的x坐標
posx: x,
// 圓弧圓心的y坐標
posy: y,
// vx 表示 x軸的值 變化的速度
vx: (Math.random() - 0.5) * 8,
// vy 表示 y軸的值 變化的速度 取值範圍:-3 到 -9
vy: Math.random() * (-6) - 3,
// 圓弧的半徑
radius: Math.random() * 1.5 + 1
};
return drop;
}
// 畫一定數量的小水珠
function madedrops(x, y) {
// 隨機生成一個數 maxi
// maxi 代表要畫小水珠的數量
var maxi = Math.floor(Math.random() * 5 + 5);
for (var i = 0; i < maxi; i++) {
dropList.push(createDrop(x, y));
}
}
// 開始調用update函數,更新動畫
window.requestAnimationFrame(update);
// 更新動畫
function update() {
// 如果保存小水珠的數組有內容
if (dropList.length > 0) {
// 遍歷保存小水珠的數組
dropList.forEach(function (e) {
//設置e.vx,vx表示x坐標變化的速度
// (speedx)/2 是為了,讓小水珠 在x軸的移動距離短一點,看上去更真實點
// 也使 小水珠的移動方向 和 雨滴方向,雨滴下落方向,滑鼠移動方向相同
e.vx = e.vx + (speedx / 2);
e.posx = e.posx + e.vx;
//設置e.vy,vy表示y坐標變化的速度
// e.vy的範圍是-3 到 -9,而這時e.posy(y坐標)一定是正值,所以 e.posy的值會先減小後增大
// 也就是實現 雨滴散成小水珠,小水珠會先上升後下降的效果
e.vy = e.vy + gravity;
e.posy = e.posy + e.vy;
// 如果 小水珠y坐標 大於 可視區域的高度,設置die屬性為true
// 小水珠如果超出可視區域就刪除掉
if (e.posy > canvasEl.clientHeight) {
e.die = true;
}
});
}
// 刪除 die屬性為ture 的數組成員
// 可視區域外的小水珠刪除掉
for (var i = dropList.length - 1; i >= 0; i--) {
if (dropList[i].die) {
dropList.splice(i, 1);
}
}
// 設置下雨方向變換的速度,取值範圍: -1 到 1
// 當 speedx = maxspeedx時,下雨方向 會 隨滑鼠移動方向立即改變
speedx = speedx + (maxspeedx - speedx) / 50;
// 根據lineNum的值,畫一定數量雨滴
for (var i = 0; i < lineNum; i++) {
// 調用createLine 函數,參數是雨滴x坐標
createLine(Math.random() * 2 * canvasEl.width - (0.5 * canvasEl.width));
}
// 設置結束線,也就是雨滴散開 形成許多小水珠的位置
var endLine = canvasEl.clientHeight - Math.random() * canvasEl.clientHeight / 5;
// 遍歷保存雨滴的數組
linelist.forEach(function (e) {
// 利用勾股定理 確定一個範圍,在這個範圍內雨滴會散開形成小水珠
// e.posx + speedx * e.h 是雨滴x坐標
// e.posy + e.h 是雨滴y坐標
var dis = Math.sqrt(((e.posx + speedx * e.h) - mousePos[0]) * ((e.posx + speedx * e.h) - mousePos[0]) + (e.posy + e.h - mousePos[1]) * (e.posy + e.h - mousePos[1]));
// 如果在mouseDis區域內,就刪除雨滴,畫一些小水珠(圓弧)
// 實現滑鼠碰到雨滴,雨滴散成小水珠的效果
if (dis < mouseDis) {
// 刪除 雨滴
e.die = true;
// 畫一些小水珠(圓弧)
madedrops(e.posx + speedx * e.h, e.posy + e.h);
}
// 如果雨滴超過 結束線,刪除雨滴,畫一些小水珠(圓弧)
if ((e.posy + e.h) > endLine) {
e.die = true;
madedrops(e.posx + speedx * e.h, e.posy + e.h);
}
// 如果 雨滴 y坐標 大於 可視區域的高度,設置die屬性為true
// 如果 雨滴 超出可視區域就刪除掉
if (e.posy >= canvasEl.clientHeight) {
e.die = true;
} else {
// 逐漸增加 雨滴 y坐標的值
e.posy = e.posy + e.speed;
// 變化雨滴 x坐標
// * speedx 用來控制雨滴 下落 方向
// 使 雨滴下落方向 和 滑鼠移動方向相同
e.posx = e.posx + e.speed * speedx;
}
});
// 刪除 die屬性為ture 的數組成員
// 滑鼠區域內的,超過結束線的,可視區域外的雨滴刪除掉
for (var i = linelist.length - 1; i >= 0; i--) {
if (linelist[i].die) {
linelist.splice(i, 1);
}
}
// 渲染
render();
// 遞歸調用 update,實現動畫效果
window.requestAnimationFrame(update);
}
// 渲染
function render() {
// 畫一個和可視區域一樣大的矩形
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvasEl.width, canvasEl.height);
// 畫雨滴效果
ctx.lineWidth = 5;
linelist.forEach(function (line) {
ctx.strokeStyle = line.color;
ctx.beginPath();
ctx.moveTo(line.posx, line.posy);
// * speedx 用來控制雨滴方向
// 使 雨滴方向 和 滑鼠移動方向相同
ctx.lineTo(line.posx + line.h * speedx, line.posy + line.h);
ctx.stroke();
});
// 畫雨滴散開形成小水珠效果
ctx.lineWidth = 1;
ctx.strokeStyle = "#fff";
dropList.forEach(function (e) {
ctx.beginPath();
ctx.arc(e.posx, e.posy, e.radius, Math.random() * Math.PI * 2, 1 * Math.PI);
ctx.stroke();
});
// 解開注釋,可看見滑鼠範圍
/*
ctx.beginPath();
ctx.arc(mousePos[0], mousePos[1], mouseDis, 0, 2 * Math.PI);
ctx.stroke();
*/
}
}
</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
最後說一句
這個特效的作者是 sxq111本尊 大神,我只是站在巨人的肩膀上,喜歡的小夥伴們可以去github上給個小star哦。
※SQL中case when then else end用法
※WebAssembly 的未來:將逐漸解鎖整個「技能樹」
TAG:程序員小新人學習 |