本著瞎折騰的學(xué)習(xí)態(tài)度,在閑暇之余就是要搞點(diǎn)奇奇怪怪的事情粟按。文中如有哪不對(duì)的地方,還請(qǐng)大家指出。本文項(xiàng)目github地址:https://github.com/SmallStoneSK/particle-effect
前言
今天要跟大家一起分享的是最近用canvas寫的一個(gè)Particle粒子特效弦牡,這效果是很早就出現(xiàn)過了友驮,所以想必大家應(yīng)該都見到過(沒見過的看下面的gif圖,或者戳演示地址驾锰,或者直接看知乎首頁卸留。
以前第一次見到的時(shí)候,心想:哇靠椭豫,那么炫酷耻瑟,這是怎么實(shí)現(xiàn)的?于是開始想各種可能的實(shí)現(xiàn)方案赏酥,但是卻從來沒有真正地去實(shí)現(xiàn)一次喳整。最近閑下來之后又想到了這個(gè)特效,手癢就折騰了下裸扶,在這兒跟大家分享一下算柳。
動(dòng)手前的思考
在動(dòng)手前,不妨先思考一下這效果是怎么實(shí)現(xiàn)的呢姓言?有兩種方式:dom 或 canvas瞬项。
第一種dom方法,我們可以把頁面上的粒子看做是一個(gè)個(gè)通過絕對(duì)定位布局的div何荚,然后用定時(shí)器不斷改變它們?cè)陧撁嬷械膖op和left值囱淋,這樣就可以模擬出粒子運(yùn)動(dòng)的效果。但是我們都知道對(duì)dom的操作是很耗性能的餐塘,如果粒子的數(shù)量少的話可能還好妥衣,但要是把粒子的數(shù)量設(shè)的超級(jí)大,那對(duì)瀏覽器來說戒傻,性能方面肯定是個(gè)顧慮税手。所以出于性能考慮,這種方法果斷棄之需纳。
第二種方法是用canvas來實(shí)現(xiàn)芦倒。想必大家都知道canvas是干嘛用的,其實(shí)說白了就是給你一塊畫布不翩,然后你想怎么畫就怎么畫兵扬。那么對(duì)應(yīng)到本文中要實(shí)現(xiàn)的這個(gè)Particle粒子特效,是不是只要把這個(gè)動(dòng)畫的每一幀都畫出來就可以了口蝠?而要畫出每一幀器钟,是不是只要把這個(gè)場(chǎng)景中的所有元素對(duì)應(yīng)到畫布中的具體位置畫出來就可以了?為了便于大家的理解妙蔗,不妨瞅一眼下面的偽代碼:
while(true) {
// 清除上一幀
clearLastFrame();
// 繪制(Particle粒子特效的繪制包括drawParticle和drawLine)
draw();
// 延遲16.7s(對(duì)于人的肉眼而言傲霸,1秒60幀的動(dòng)畫就可以)
sleep(1000/60);
}
實(shí)現(xiàn)設(shè)計(jì)
既然已經(jīng)明確使用canvas來實(shí)現(xiàn)我們的Particle粒子特效,那在具體實(shí)現(xiàn)之前,我們不妨先來捋一捋整個(gè)過程昙啄。
1. init初始化
第一步:在html的body中先插入一個(gè)帶id的canvas標(biāo)簽穆役,以便在js中找到它。
<canvas id="canvas">
<p>your browser doesn't support canvas.</p>
</canvas>
* {
margin: 0;
padding: 0;
}
html, body {
width: 100%;
height: 100%;
background-color: #292d35;
}
第二步:新建一個(gè)ParticleEffect.js文件跟衅,添加一個(gè)init方法孵睬,需要設(shè)置canvas的寬高,獲取canvas的上下文伶跷。
// 粒子特效
var ParticleEffect = {
ctx: null,
canvas: null,
init: function() {
var windowSize = Utils.getWindowSize(); // 獲取窗口大小
this.canvas = document.getElementById('canvas');
this.ctx = this.canvas.getContext('2d');
if(this.ctx) {
// 設(shè)置canvas的寬高
this.canvas.width = windowSize.width;
this.canvas.height = windowSize.height;
// 在這里掰读,你就可以開始用ctx隨心所欲的畫畫了
}
}
};
// 工具
var Utils = {
getWindowSize: function() {
return {
width: this.getWindowWidth(),
height: this.getWindowHeight()
};
},
getWindowWidth: function() {
return window.innerWidth || document.documentElement.clientWidth;
},
getWindowHeight: function() {
return window.innerHeight || document.documentElement.clientHeight;
}
};
2.構(gòu)造Particle類
我們會(huì)利用工廠模式創(chuàng)造一個(gè)Particle類,給每個(gè)粒子都添加以下屬性:
// 粒子類
function Particle(info) {
// 粒子屬性
this.x = info.x; // 粒子在畫布中的橫坐標(biāo)
this.y = info.y; // 粒子在畫布中的縱坐標(biāo)
this.vx = info.vx; // 粒子的橫向運(yùn)動(dòng)速度
this.vy = info.vy; // 粒子的縱向運(yùn)動(dòng)速度
this.color = info.color; // 粒子的顏色
this.scale = info.scale; // 粒子的縮放比例
this.radius = info.radius; // 粒子的半徑大小
// 繪制方法
if(typeof Particle.prototype.draw === 'undefined') {
Particle.prototype.draw = function(ctx) {
// canvas畫圓方法
ctx.beginPath();
ctx.fillStyle = this.color;
ctx.strokeStyle = this.color;
ctx.arc(this.x, this.y, this.radius * this.scale, 0, 2 * Math.PI, false);
ctx.closePath();
ctx.fill();
}
}
}
3.生成粒子
現(xiàn)在我們就可以在init方法中用剛創(chuàng)建好的Particle類來生成我們的粒子了叭莫。這里還有一點(diǎn)需要注意的就是蹈集,為了模擬粒子運(yùn)動(dòng)的真實(shí)性,我們就得隨機(jī)的賦予粒子的各個(gè)屬性雇初,包括橫坐標(biāo)拢肆、縱坐標(biāo)、橫向速度靖诗、縱向速度郭怪、半徑、縮放比例刊橘。具體的就看下面代碼吧:
var ParticleEffect = {
// ... 省去其他代碼
init: function() {
// ... 省去其他代碼
if(this.ctx) {
// ... 省去其他代碼
// 生成100個(gè)粒子(這里需要注意的是鄙才,由于粒子是有半徑的,所以初始的x, y值范圍需要相應(yīng)的調(diào)整)
var times = 100;
this.particles = [];
while(times--) {
this.particles.push(new Particle({
x: Utils.rangeRandom(10, windowSize.width - 10),
y: Utils.rangeRandom(10, windowSize.height - 10),
vx: Utils.rangeRandom(-1.2, 1.2),
vy: Utils.rangeRandom(-1.2, 1.2),
color: 'rgba(255,255,255,.2)',
scale: Utils.rangeRandom(0.8, 1.2),
radius: 10
}));
}
}
}
};
var Utils = {
... // 省去其他代碼
rangeRandom: function(min, max) {
const diff = max - min;
return min + Math.random() * diff;
}
};
4.讓粒子動(dòng)起來
終于到了激動(dòng)人心的時(shí)候了促绵,在這一步中攒庵,我們就讓粒子滿屏的動(dòng)起來。
第一步:添加move方法败晴,更新粒子的x, y坐標(biāo)浓冒。需要注意的是,在每次更新完粒子的坐標(biāo)以后尖坤,需要檢測(cè)是否有碰到墻壁稳懒。如果有的話,需要改變粒子的運(yùn)動(dòng)方向糖驴。
var ParticleEffect = {
// ... 省去其他代碼
move: function() {
var windowSize = Utils.getWindowSize();
this.particles.forEach(function(item) {
// 更新粒子坐標(biāo)
item.x += item.vx;
item.y += item.vy;
// 如果粒子碰到了左墻壁或右墻壁僚祷,則改變粒子的橫向運(yùn)動(dòng)方向
if((item.x - item.radius < 0) || (item.x + item.radius > windowSize.width)) {
item.vx *= -1;
}
// 如果粒子碰到了上墻壁或下墻壁,則改變粒子的縱向運(yùn)動(dòng)方向
if((item.y - item.radius < 0) || (item.y + item.radius > windowSize.height)) {
item.vy *= -1;
}
});
}
};
第二步:添加draw方法贮缕,控制canvas每次繪制的內(nèi)容(我們先不繪制粒子之間的連線,先讓粒子動(dòng)起來看到效果)俺榆。
var ParticleEffect = {
// ... 省去其他代碼
draw: function() {
var _this = this;
// 每次重新繪制之前感昼,需要先清空畫布,把上一次的內(nèi)容清空
this.ctx.clearRect(0, 0, windowSize.width, windowSize.height);
// 繪制粒子
this.particles.forEach(function(item) {
item.draw(_this.ctx);
});
// TODO: 繪制粒子之間的連線
// 粒子移動(dòng)罐脊,更新相應(yīng)的x, y坐標(biāo)
this.move();
}
};
第三步:添加run方法定嗓,使用定時(shí)器蜕琴,不斷重新繪制canvas上的內(nèi)容
var ParticleEffect = {
// ... 省去其他代碼
run: function() {
this.init();
setInterval(this.draw.bind(this), 1000 / 60);
}
};
第四步:到這兒就是調(diào)用了,調(diào)用下Particle.run方法就可以讓滿屏的粒子動(dòng)起來了宵溅。
<body>
<canvas id="canvas">
<p>your browser doesn't support canvas.</p>
</canvas>
<script src="./Particle.js"></script>
<script>
window.onload = function() {
ParticleEffect.run();
};
</script>
</body>
5.回過頭來繪制粒子之間的線條
通過觀察不難發(fā)現(xiàn)凌简,在粒子運(yùn)動(dòng)的過程中,兩個(gè)粒子之間的距離比較遠(yuǎn)的時(shí)候是不相連的恃逻,只有當(dāng)兩個(gè)粒子運(yùn)動(dòng)到一定距離范圍之內(nèi)才會(huì)相連雏搂,而遠(yuǎn)了之后細(xì)線又會(huì)斷開。所以實(shí)現(xiàn)起來也不難寇损,只要遍歷所有粒子之間的距離凸郑,只要小于一個(gè)閾值,我們就用canvas畫一條線矛市,把這兩個(gè)粒子連接起來芙沥。具體代碼如下:
var ParticleEffect = {
// ... 省去其他代碼
draw: function() {
// ... 省去其他代碼
// 繪制粒子之間的連線
for(var i = 0; i < this.particles.length; i++) {
for(var j = i + 1; j < this.particles.length; j++) {
var distance = Math.sqrt(Math.pow(this.particles[i].x - this.particles[j].x, 2) + Math.pow(this.particles[i].y - this.particles[j].y, 2));
if(distance < 100) {
// 這里我們讓距離遠(yuǎn)的線透明度淡一點(diǎn),距離近的線透明度深一點(diǎn)
this.ctx.strokeStyle = 'rgba(255,255,255,' + (distance / 100 * .2) + ')';
this.ctx.beginPath();
this.ctx.moveTo(this.particles[i].x, this.particles[i].y);
this.ctx.lineTo(this.particles[j].x, this.particles[j].y);
this.ctx.closePath();
this.ctx.stroke();
}
}
}
}
};
6.添加粒子與鼠標(biāo)之間的連線
其實(shí)完成上面的這個(gè)步驟浊吏,粒子特效已經(jīng)基本上能跑了而昨。但是我們還可以再添加粒子跟鼠標(biāo)之間的交互效果,比如當(dāng)鼠標(biāo)與粒子之間的距離小于一定閾值時(shí)找田,連接粒子與鼠標(biāo)歌憨。話不多說,看下面的代碼:
var ParticleEffect = {
// ... 省去其他代碼
mouseCoordinates: {x: 0, y: 0},
init: function() {
// ... 省去其他代碼
// 監(jiān)聽鼠標(biāo)的mouseMove事件午阵,記錄下鼠標(biāo)的x,y坐標(biāo)
window.addEventListener('mousemove', this.handleMouseMove.bind(this), false);
},
draw: function() {
// ... 省去其他代碼
// 繪制粒子和鼠標(biāo)之間的連線
for(i = 0; i < this.particles.length; i++) {
distance = Math.sqrt(Math.pow(this.particles[i].x - this.mouseCoordinates.x, 2) + Math.pow(this.particles[i].y - this.mouseCoordinates.y, 2));
if(distance < 100) {
this.ctx.strokeStyle = 'rgba(255,255,255,' + (1 - distance / 100) * .3 + ')';
this.ctx.beginPath();
this.ctx.moveTo(this.particles[i].x, this.particles[i].y);
this.ctx.lineTo(this.mouseCoordinates.x, this.mouseCoordinates.y);
this.ctx.closePath();
this.ctx.stroke();
}
}
},
handleMouseMove: function(event) {
var x, y;
event = event || window.event;
// 處理兼容
if(event.pageX || event.pageY) {
x = event.pageX;
y = event.pageY;
} else {
x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
}
this.mouseCoordinates = {x: x, y: y};
}
};
未完躺孝,待續(xù)...
其實(shí)做到這兒,已經(jīng)能夠看到效果了底桂。但是植袍,還有還有很多可以進(jìn)一步優(yōu)化的地方。比如:粒子動(dòng)效的參數(shù)可配置化籽懦,canvas自適應(yīng)窗口大小于个,requestAnimationFrame代替setInterval,緩存windowSize等等... 這些內(nèi)容將在下一章中再做介紹暮顺。