超炫的HTML5粒子效果进度条 VS 如何规范而优雅地code

最近瞎逛的时候发现了一个超炫的粒子进度效果,有多炫呢?请擦亮眼镜!

 

粗略一看真的被惊艳到了,而且很实用啊有木有!这是 Jack Rugile 写的一个小效果,源码当然是有的。聪慧如你,肯定觉得这个东西so easy 要看啥源码,给我3分钟我就写出来了吧。所以你的思路可能是:

分步实现

1)进度条的实现没什么好说的,简单的一个 fillRect(0,0,long,20),long和20是分别进度条的长宽。然后每帧动画调用前将画布清除clearRect(0,0,canvas.width,canvas.height)。做出来应该是这样的(点击启动/暂停动画):

 

2)进度条色彩的变化。这个也简单,颜色渐变嘛,fillStyle = createLinearGradient() 就行了吧。不对哦,是颜色随时间变化,每一帧内的进度条颜色一样的哦。理所当然就能搞出一句:fillStyle = rgba(f(t),f(t),f(t),1),f(t)是随时间变化的函数。然而,这些只知道rgba的哥们,发现怎么调也调不出这样的渐变效果,rgb变化哪一个都会造成颜色明暗变化,卡壳了吧,这里估计要卡掉5%的人。要保持亮度不发生变化,这里要用到hsla这种颜色格式,就是妹子们自拍修图时常用的色调/饱和度/亮度/透明度。对照进度条的效果,明显我们只要改色调就OK了。

ctx.fillStyle = 'hsla('+(hue++)+', 100%, 40%, 1)'; 

结果可能是这样的(点击启动/暂停动画):

 

3)接下来进入正题,要做粒子效果了。粒子很多,观察力不好或者没掌握方法的同学这里就要歇菜啦(此处应有博主爽朗的笑声,哈哈哈~)。对于元素数量巨大的效果,我们应该尽可能缩小观察范围,只观察一个或者一组元素,找出单体的规律。多看几次,就能发现单个粒子是先向上运动一阵子然后掉下去,单个粒子的x轴应该是不变的。对于粒子集合来说,每个粒子的x坐标递增,就能产生我们需要的效果了。这里推荐同学们去看一下MDN的例程,超好玩的ball(好玩、ball?嘿嘿~):https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Advanced_animations

这里我们每帧只添加一个粒子:

var raf = null,
c = document.createElement('canvas'),
parent = document.getElementById('canvas-wrapper-test3');
c.width = 400;
c.height = 100;
c.id = 'canvas-test3';
parent.appendChild(c);
var ctx = c.getContext('2d'),
hue = 0, //色调
vy = -2, //y轴速度
par = [], //粒子数组
x = 0, //进度条当前位置
draw = function () {
var color;
   ctx.clearRect(0,0,c.width,c.height);
x += 3; //进度条速度为每帧3个像素
   hue = (x > 310) ? 0 : hue;
//颜色渐变为每帧1色调
  color = 'hsla('+(hue++)+', 100%, 40%, 1)' ;
par.push({ //用数组模拟队列
px: x + 40,
   py: 50,
   pvy: vy,
   pcolor: 'hsla('+(hue+30)+', 100%, 70%, 1)',
  });
  x = (x > 310) ? 0 : x; //进度条到右侧后返回
ctx.fillStyle = color;
  ctx.fillRect(45, 40, x, 20);
var n = par.length;
  while(n--){
//切记要随机差异化粒子y轴速度,否则就变成一根抛物线了
   par[n].pvy += (Math.random()+0.1)/5;
  par[n].py += par[n].pvy;
   if (par[n].py > c.height ) {
   par.splice(n, 1); //掉到画布之外了,清除该粒子
   continue;
   }
  ctx.fillStyle = par[n].pcolor;
   ctx.fillRect(par[n].px, par[n].py, 5, 5);
  }
raf = window.requestAnimationFrame(draw);
};
raf = window.requestAnimationFrame(draw);

虽然简单,但效果还是出来了(点击启动/暂停动画):

 

至此,这个动画效果基本完成了,后续要做的就是优化了:

1)增加粒子数量,现在我们每帧要push多个粒子进去,这样数量上就上来了。

2)应该直接调用fillRect绘制小矩形代替圆形,有些筒子可能会真的用arc画一个粒子,囧。。。这里稍微提点常识,计算机绘图中所有曲线都是由直线构成的,要画一个圆就相当于调用了相当多次的画线功能,性能消耗非常大。在粒子这么小的情况下,是圆是方只有瞎子才能分得清了,所以我们应该直接调用fillRect绘制小矩形代替圆形。这个也是canvas绘图里面常用的优化方法哦~

3)增加随机化效果。现在xy起始坐标都跟进度条紧密联系在一起。我们每次生成几个粒子的话,粒子初始坐标应该在一定范围浮动,另外粒子的大小、颜色也应该要在小范围内随机化。颜色相对进度条颜色有一定滞后的话,效果会更加自然。

4)上面说到x方向不动,但是如果x方向增加一点抖动效果的话会更自然生动。

5)画布颜色混合选项设置线性叠加:globalCompositeOperation = 'lighter',这样在粒子重叠的时候颜色会有叠加的效果。这个是在源码上看到的,大牛就是细节会做得比别人好的家伙!关于这个属性的具体解释可以看看这位"大白鲨"的实验,中文的!http://www.cnblogs.com/jenry/archive/2012/02/11/2347012.html

总结一下

想要实现一个效果,首先我们要简化模型,可以分成色彩的变化、位置的变化、大小的变化等,还有就是将某个因子独立出来看,通过各种抽茧剥丝的手法去找到效果后面的数学模型,然后编程去实现它。艺术总是源于生活,所以在做时候应该好好考虑是否应该加入惯性、弹性、重力这些效果,这些物理特性反映到效果中的话,会更加自然逼真。

都总结了,那完事了?

NO!NO!NO!

接下来才是我想要说的重点!上面的代码效果优化之后,老大看到效果觉得还不错哦,加到新项目去吧。。。然后就是啪啦啪啦ctrlC ctrlV?好吧,你也猜到了我要说什么,对的,复用和封装。

先看人家的源码,貌似这哥们连停止动画都没写呢,就一个无限循环。。。

 var lightLoader = function(c, cw, ch){

     var that = this;
this.c = c;
this.ctx = c.getContext('2d');
this.cw = cw;
this.ch = ch;
this.raf = null; this.loaded = 0;
this.loaderSpeed = .6;
this.loaderWidth = cw * 0.8;
this.loaderHeight = 20;
this.loader = {
x: (this.cw/2) - (this.loaderWidth/2),
y: (this.ch/2) - (this.loaderHeight/2)
};
this.particles = [];
this.particleLift = 220;
this.hueStart = 0
this.hueEnd = 120;
this.hue = 0;
this.gravity = .15;
this.particleRate = 4; /*========================================================*/
/* Initialize
/*========================================================*/
this.init = function(){
this.loaded = 0;
this.particles = [];
this.loop();
}; /*========================================================*/
/* Utility Functions
/*========================================================*/
this.rand = function(rMi, rMa){return ~~((Math.random()*(rMa-rMi+1))+rMi);};
this.hitTest = function(x1, y1, w1, h1, x2, y2, w2, h2){return !(x1 + w1 < x2 || x2 + w2 < x1 || y1 + h1 < y2 || y2 + h2 < y1);}; /*========================================================*/
/* Update Loader
/*========================================================*/
this.updateLoader = function(){
if(this.loaded < 100){
this.loaded += this.loaderSpeed;
} else {
this.loaded = 0;
}
}; /*========================================================*/
/* Render Loader
/*========================================================*/
this.renderLoader = function(){
this.ctx.fillStyle = '#000';
this.ctx.fillRect(this.loader.x, this.loader.y, this.loaderWidth, this.loaderHeight); this.hue = this.hueStart + (this.loaded/100)*(this.hueEnd - this.hueStart); var newWidth = (this.loaded/100)*this.loaderWidth;
this.ctx.fillStyle = 'hsla('+this.hue+', 100%, 40%, 1)';
this.ctx.fillRect(this.loader.x, this.loader.y, newWidth, this.loaderHeight); this.ctx.fillStyle = '#222';
this.ctx.fillRect(this.loader.x, this.loader.y, newWidth, this.loaderHeight/2);
}; /*========================================================*/
/* Particles
/*========================================================*/
this.Particle = function(){
this.x = that.loader.x + ((that.loaded/100)*that.loaderWidth) - that.rand(0, 1);
this.y = that.ch/2 + that.rand(0,that.loaderHeight)-that.loaderHeight/2;
this.vx = (that.rand(0,4)-2)/100;
this.vy = (that.rand(0,that.particleLift)-that.particleLift*2)/100;
this.width = that.rand(2,6)/2;
this.height = that.rand(2,6)/2;
this.hue = that.hue;
}; this.Particle.prototype.update = function(i){
this.vx += (that.rand(0,6)-3)/100;
this.vy += that.gravity;
this.x += this.vx;
this.y += this.vy; if(this.y > that.ch){
that.particles.splice(i, 1);
}
}; this.Particle.prototype.render = function(){
that.ctx.fillStyle = 'hsla('+this.hue+', 100%, '+that.rand(50,70)+'%, '+that.rand(20,100)/100+')';
that.ctx.fillRect(this.x, this.y, this.width, this.height);
}; this.createParticles = function(){
var i = this.particleRate;
while(i--){
this.particles.push(new this.Particle());
};
}; this.updateParticles = function(){
var i = this.particles.length;
while(i--){
var p = this.particles[i];
p.update(i);
};
}; this.renderParticles = function(){
var i = this.particles.length;
while(i--){
var p = this.particles[i];
p.render();
};
}; /*========================================================*/
/* Clear Canvas
/*========================================================*/
this.clearCanvas = function(){
this.ctx.globalCompositeOperation = 'source-over';
this.ctx.clearRect(0,0,this.cw,this.ch);
this.ctx.globalCompositeOperation = 'lighter';
}; /*========================================================*/
/* Animation Loop
/*========================================================*/
this.loop = function(){
var loopIt = function(){
that.raf = requestAnimationFrame(loopIt);
that.clearCanvas(); that.createParticles(); that.updateLoader();
that.updateParticles(); that.renderLoader();
that.renderParticles(); };
loopIt();
}; this.stop = function(){
this.ctx.globalCompositeOperation = 'source-over';
this.ctx.clearRect(0,0,this.cw,this.ch);
window.cancelAnimationFrame(this.raf);
} }; /*========================================================*/
/* Setup requestAnimationFrame when it is unavailable.
/*========================================================*/
var setupRAF = function(){
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x){
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
}; if(!window.requestAnimationFrame){
window.requestAnimationFrame = function(callback, element){
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall);
lastTime = currTime + timeToCall;
return id;
};
}; if (!window.cancelAnimationFrame){
window.cancelAnimationFrame = function(id){
clearTimeout(id);
};
};
};

我在源码基础上加了个stop,初始化的时候清除了进度条位置和粒子位置,改动就这两点。大家可以在gitHub上fork我改动过后的版本:https://github.com/QieGuo2016/Light-Loader

引用这个组件也非常简单:

var c = document.createElement('canvas');
c.width = 400;
c.height = 100;
c.id = 'canvas-test1';
parent.appendChild(c); //在需要的位置加入canvas元素
var loader = new lightLoader(c,c.width,c.height);
setupRAF(); //不支持requestAnimationFrame浏览器的替代方案
loader.init();

读源码

这个源码写的也比较规范,结构清晰、组件和DOM分离得很好,是个学习的好题材!下面说说我对源码的理解,菜鸟一枚,有错请务必指出!

(一) 构造函数的形参

像进度条这样的小组件,我们应该尽量将其封装到一个全局变量中,如:var lightLoader = function(e) { }; 。源码中传入的参数是一个canvas和宽高,但是假如我们要设置进度条的属性的时候,就必须到源码里面去改动了,这样的话可复用性就打了个打折扣。还好,与进度条相关的属性都被封装到了全局变量的属性中,要改动的话实例化后直接改lightLoade.属性也可以使用。

如果要增加组件的*度,可以使用一个对象作为形参:var lightLoader = function(opt) { };

设置传入一个对象的话,后续要对这个组件进行扩展或者改动的时候,那对象参数的便利性就体现得淋漓尽致了。

比如我要扩展一个进度条的宽度:this.loaderHeight = opt.loaderHeight ? opt.loaderHeight : 20; 就完事了(实参的类型和值的安全性暂不讨论哈!)。原来的var lightLoader = function(c, cw, ch){} 如果要扩展一个进度条的宽度,想当然地我们可以写出 var lightLoader = function(c, cw, ch, lw) { this.loaderHeight = lw ? lw : 20 },但是麻烦的是,当我们new lightLoader(c, 20)的时候,20并没有传到给宽度啊。因为参数是有顺序的,而对象的属性则安全得多。

(二) 定义对象的方式

源码里面定义lightLoader时使用的是经典的构造函数的方式,将属性和函数都放在构造函数中,而粒子Particle的方法则是放在Particle的原型中定义的。这很关键!

经典构造函数带来的问题可以自行百度,博客园上介绍也非常多,一搜一百页。简单来说就是构造函数内部的所有函数和属性都会被复制到每个实例中,比如说我通过构造函数创建了5个实例,那在内存中就有5份副本存在。但是很明显,方法(不习惯说函数。。。)不应该被复制5份,而应该是5个实例共享一个方法即可。所以,目前推荐的是使用混合模式定义对象:属性放在构造函数中,方法放在原型中。对于数量较大(比如说本例中的粒子),那方法甚至属性都应该放在原型中,以减少内存消耗,提高动画流畅度。

虽然源码那样写了, 但是我还是觉得lightLoader对象的方法也应该放到原型中,这是也是个代码规范的问题。

(三)封装问题

源码中所有属性都被定义为this.**,也就是说都暴露给外界了。这些属性都是跟效果相关的,很多属性需要看着效果调试出来的。暴露出来的好处就是调试的时候可以在运行时动态改变相应的值,观察效果的变化,非常方便。你们感受一下:

超炫的HTML5粒子效果进度条 VS 如何规范而优雅地code

但并不是所有属性都应该被暴露出来的,哪些需要暴露,哪些需要隐藏这个要看具体场景了。另外私有成员的命名潜规则(←.←)是前面加_,私有属性和私有方法都应该这样命名,这样同类们一看到就懂啦。

封装的另外一个方面是要与DOM对象松耦合,一个组件假如跟其他元素的联系很紧密的话,移植性就非常差了。这一点暂时我还没太多体会,不敢妄议。

就说到这里啦,看起来不是很有料呢。。。所以,还是补张图片丰满一下吧~码字不易,顺手点赞哈!

超炫的HTML5粒子效果进度条 VS 如何规范而优雅地code

(图片出处:著名摄影师 小张同学,转载请注明)

原创文章,转载请注明出处!本文链接:http://www.cnblogs.com/qieguo/p/5438380.html 

上一篇:Database first with EntityFramework (Migration)安装和升级


下一篇:Android EventBus 3.0.0 使用总结