手把手教你系列 - Particle粒子特效(下)
本着瞎折腾的学习态度,在闲暇之余就是要搞点奇奇怪怪的事情。文中如有哪不对的地方,还请大家指出。本文项目github地址:https://github.com/SmallStoneSK/particle-effect
回顾
在上一篇文章中,我们简单地介绍了如何利用canvas来实现Particle粒子特效。通过把动画看作是不断地重复clear(), paint()这两步,我们只要把精力更多地关注在如何绘制每一帧画面即可。因此在上篇文章中,我们着重介绍了如何绘制画布上的运动中的粒子、粒子间的连线、以及粒子和鼠标的交互。那么在接下来的这篇文章中,我们将介绍如何优化、完善、改进现有的代码。其中,主要包括以下内容:
- 粒子动效的参数可配置化
- canvas自适应窗口大小
- requestAnimationFrame代替setInterval
- 缓存windowSize
粒子动效的参数可配置化
在之前的代码中,我们将粒子的一些属性都直接用硬编码写死了。如果要将这个ParticleEffect.js做成插件给别人用的话,其他人岂不是还要先看懂我们的代码?所以,我们需要把其中一些可配置的参数暴露出来,做到参数可配置化,方便使用的人自己定制想要的效果。
需要注意的是,我们需要有一个自己的默认保底配置。也就是说,如果用户没有传相应的配置时,粒子特效也能正常运行。而当用户配置了相应的参数时,我们就应该以新的配置为准。话不多说,具体实现看下面代码:
var ParticleEffect = {
// ... 省去其他代码
config: {
count: 100, // 默认创建粒子数量
radius: 5, // 默认粒子半径
vxRange: [-1, 1], // 默认粒子横向移动速度范围
vyRange: [-1, 1], // 默认粒子纵向移动速度范围
scaleRange: [.5, 1], // 默认粒子缩放比例范围
lineLenThreshold: 125, // 默认连线长度阈值
color: 'rgba(255,255,255,.2)' // 默认粒子、线条的颜色
},
init: function() {
// ... 省去其他代码
// 更新config配置
var _this = this;
newConfig && Object.keys(newConfig).forEach(function(key) {
_this.config[key] = newConfig[key];
});
// 生成粒子
var times = this.config.count;
this.particles = [];
while(times--) {
this.particles.push(new Particle({
x: Utils.rangeRandom(this.config.radius, windowSize.width - this.config.radius),
y: Utils.rangeRandom(this.config.radius, windowSize.height - this.config.radius),
vx: Utils.rangeRandom(this.config.vxRange[0], this.config.vxRange[1]),
vy: Utils.rangeRandom(this.config.vyRange[0], this.config.vyRange[1]),
color: this.config.color,
scale: Utils.rangeRandom(this.config.scaleRange[0], this.config.scaleRange[1]),
radius: this.config.radius
}));
}
},
draw: function() {
// ... 省去其他代码
var color = this.config.color;
var lineLenThreshold = this.config.lineLenThreshold;
// 绘制粒子之间的连线
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 < lineLenThreshold) {
this.ctx.strokeStyle = color;
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();
}
}
}
// 绘制粒子和鼠标之间的连线
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 < lineLenThreshold) {
this.ctx.strokeStyle = color;
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();
}
}
},
run: function(config) {
this.init(config);
setInterval(this.draw.bind(this), 1000 / 60);
}
};
经过改造之后,我们就可以像下面这样往ParticleEffect.run方法中传入想要的配置参数了。
ParticleEffect.run({
radius: 8,
color: 'rgba(20,200,150,.2)'
});
canvas自适应窗口大小
说实话,这其实应该算是一个Bug,不信你可以改变窗口大小,然后再看看效果如何。究其原因,还是因为当窗口大小发生变化的时候,我们的canvas宽高还没有发生改变。所以解决办法也很简单,监听window的resize事件,当浏览器窗口大小发生变化的时候,相应地我们也同时更新canvas的宽高就可以了。具体代码如下:
var ParticleEffect = {
// ... 省去其他代码
init: function() {
// ... 省去其他代码
// 监听窗口大小改变事件
window.addEventListener('resize', this.handleWindowResize.bind(this), false);
},
handleWindowResize: function() {
var windowSize = Utils.getWindowSize();
this.canvas.width = windowSize.width;
this.canvas.height = windowSize.height;
}
};
requestAnimationFrame代替setInterval
在之前的代码中,我们在run方法里通过setInterval来不断地重新绘制canvas上的内容。但是实际上浏览器给我们提供了一个更友好的API来绘制,那就是requestAnimationFrame,它可以跟随浏览器自身的重绘时机来绘制我们的内容。这样性能更好,也可以避免setInterval时间间隔不准的问题(具体介绍可以看看鑫神的这篇文章)。
不过,也并不是所有的浏览器都支持这个API,所以我们还得加上相应的兼容处理。具体代码如下:
var ParticleEffect = {
// ... 省去其他代码
init: function() {
// ... 省去其他代码
// 兼容requestAnimationFrame
this.supportRequestAnimationFrame();
},
draw: function() {
// ... 省去其他代码
// 循环调用draw方法
window.requestAnimationFrame(this.draw.bind(this));
},
supportRequestAnimationFrame: function() {
if(!window.requestAnimationFrame) {
window.requestAnimationFrame = (
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
setInterval(callback, 1000 / 60)
}
);
}
},
run: function() {
this.init(config);
window.requestAnimationFrame(this.draw.bind(this));
}
};
缓存windowSize
这里为什么要缓存windowSize呢,或者说怎么缓存法?其实根据上面的代码我们可以看到,Utils对象提供了一个getWindowSize方法,所以凡是要获取窗口大小的地方都会实时地去获取一次真实的大小,而且这也是跟DOM相关的。但是仔细想想:一方面,窗口大小并不总是在一直变化的,用到windowSize的时候没必要每次都实时获取windowSize;另一方面,我们已经监听了window的resize事件,所以每次窗口大小变化的时候,更新一下缓存下来的windowSize就可以了。口说无凭,做个实验就知道了,代码如下:
/**
* 测试方法
*/
function test(executeFunc, times) {
var start, end, num = times;
start = new Date();
while(times--) {
executeFunc();
}
end = new Date();
console.log(executeFunc.name + ' executes ' + num + 'times and takes ' + (end.getTime() - start.getTime()) / 1000 + 's.');
}
/**
* 实时获取窗口大小
*/
function getWindowSizeRealTime() {
return {
width: window.innerWidth || document.documentElement.clientWidth,
height: window.innerHeight || document.documentElement.clientHeight
};
}
/**
* 窗口大小从缓存中获取
*/
var cache = {width: 1024, height: 780};
function getWindowSizeFromCache() {
return cache;
}
// 执行测试
[1000, 10000, 100000, 1000000].forEach(function(times) {
test(getWindowSizeRealTime, times);
test(getWindowSizeFromCache, times);
});
// 输出结果
getWindowSizeRealTime executes 1000 times and takes 0.001s.
getWindowSizeFromCache executes 1000 times and takes 0s.
getWindowSizeRealTime executes 10000 times and takes 0.007s.
getWindowSizeFromCache executes 10000 times and takes 0s.
getWindowSizeRealTime executes 100000 times and takes 0.051s.
getWindowSizeFromCache executes 100000 times and takes 0.001s.
getWindowSizeRealTime executes 1000000 times and takes 0.405s.
getWindowSizeFromCache executes 1000000 times and takes 0.005s.
从上面的测试结果中可以看到,当执行次数少的时候,两种方法的速度并没有多大区别。但是当执行次数的数量级上十万、甚至百万的时候,差距就非常明显了。所以说前面的猜测并没有错,性能上的确是有差距,但是这有优化的必要吗?因为只要代码写得好,避免这种短时间内重复获取的情况就行,而且如果缓存窗口大小,势必也会增加维护cache的开销。
不过,我是觉得虽然频繁改变窗口的场景很少,但是每个人写代码的水平不同,可能有人就是把getWindowSize放在了一个循环中,那就gg了。。。所以,最终决定还是把windowSize的缓存给加上去(说是缓存,其实就是一个变量。。。),毕竟代码实现也很简单。具体代码如下:
var ParticleEffect = {
// ... 省去其他代码
init: function(newConfig) {
// ... 省去其他代码
// 初始化的时候,第一次获取窗口大小之前要先更新一下(其实更准确的应该在window.onload中更新)
Utils.updateWindowSize();
var windowSize = Utils.getWindowSize();
// 设置canvas宽高
this.canvas.width = windowSize.width;
this.canvas.height = windowSize.height;
// ... 省去其他代码
},
handleWindowResize: function() {
// 窗口大小发生变化的时候,需要更新缓存中的windowSize
Utils.updateWindowSize();
var windowSize = Utils.getWindowSize();
this.canvas.width = windowSize.width;
this.canvas.height = windowSize.height;
}
}
var Utils = {
_windowSize: {
width: 0,
height: 0
},
getWindowSize: function() {
return this._windowSize;
},
updateWindowSize: function() {
this._windowSize.width = this.getWindowWidth();
this._windowSize.height = this.getWindowHeight();
}
// ... 省去其他代码
}
写在最后
Particle粒子特效到这就已经基本上搞定了,其他的话还可以再进一步把这个ParticleEffect.js文件好好封装一下,做成插件给他人使用。源码都已经上传到我的github上,喜欢的可以star一个哦~