最近瞎逛的时候发现了一个超炫的粒子进度效果,有多炫呢?请擦亮眼镜!html
粗略一看真的被惊艳到了,并且很实用啊有木有!这是 Jack Rugile 写的一个小效果,源码固然是有的。聪慧如你,确定以为这个东西so easy 要看啥源码,给我3分钟我就写出来了吧。因此你的思路多是:git
1)进度条的实现没什么好说的,简单的一个 fillRect(0,0,long,20),long和20是分别进度条的长宽。而后每帧动画调用前将画布清除clearRect(0,0,canvas.width,canvas.height)。作出来应该是这样的(点击启动/暂停动画):github
2)进度条色彩的变化。这个也简单,颜色渐变嘛,fillStyle = createLinearGradient() 就好了吧。不对哦,是颜色随时间变化,每一帧内的进度条颜色同样的哦。理所固然就能搞出一句:fillStyle = rgba(f(t),f(t),f(t),1),f(t)是随时间变化的函数。然而,这些只知道rgba的哥们,发现怎么调也调不出这样的渐变效果,rgb变化哪个都会形成颜色明暗变化,卡壳了吧,这里估计要卡掉5%的人。要保持亮度不发生变化,这里要用到hsla这种颜色格式,就是妹子们自拍修图时经常使用的色调/饱和度/亮度/透明度。对照进度条的效果,明显咱们只要改色调就OK了。web
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
canvas
这里咱们每帧只添加一个粒子:数组
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多个粒子进去,这样数量上就上来了。app
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?好吧,你也猜到了我要说什么,对的,复用和封装。
先看人家的源码,貌似这哥们连中止动画都没写呢,就一个无限循环。。。
1 var lightLoader = function(c, cw, ch){ 2
3 var that = this; 4 this.c = c; 5 this.ctx = c.getContext('2d'); 6 this.cw = cw; 7 this.ch = ch; 8 this.raf = null; 9
10 this.loaded = 0; 11 this.loaderSpeed = .6; 12 this.loaderWidth = cw * 0.8; 13 this.loaderHeight = 20; 14 this.loader = { 15 x: (this.cw/2) - (this.loaderWidth/2), 16 y: (this.ch/2) - (this.loaderHeight/2) 17 }; 18 this.particles = []; 19 this.particleLift = 220; 20 this.hueStart = 0
21 this.hueEnd = 120; 22 this.hue = 0; 23 this.gravity = .15; 24 this.particleRate = 4; 25
26 /*========================================================*/
27 /* Initialize 28 /*========================================================*/
29 this.init = function(){ 30 this.loaded = 0; 31 this.particles = []; 32 this.loop(); 33 }; 34
35 /*========================================================*/
36 /* Utility Functions 37 /*========================================================*/
38 this.rand = function(rMi, rMa){return ~~((Math.random()*(rMa-rMi+1))+rMi);}; 39 this.hitTest = function(x1, y1, w1, h1, x2, y2, w2, h2){return !(x1 + w1 < x2 || x2 + w2 < x1 || y1 + h1 < y2 || y2 + h2 < y1);}; 40
41 /*========================================================*/
42 /* Update Loader 43 /*========================================================*/
44 this.updateLoader = function(){ 45 if(this.loaded < 100){ 46 this.loaded += this.loaderSpeed; 47 } else { 48 this.loaded = 0; 49 } 50 }; 51
52 /*========================================================*/
53 /* Render Loader 54 /*========================================================*/
55 this.renderLoader = function(){ 56 this.ctx.fillStyle = '#000'; 57 this.ctx.fillRect(this.loader.x, this.loader.y, this.loaderWidth, this.loaderHeight); 58
59 this.hue = this.hueStart + (this.loaded/100)*(this.hueEnd - this.hueStart);
60
61 var newWidth = (this.loaded/100)*this.loaderWidth;
62 this.ctx.fillStyle = 'hsla('+this.hue+', 100%, 40%, 1)'; 63 this.ctx.fillRect(this.loader.x, this.loader.y, newWidth, this.loaderHeight); 64
65 this.ctx.fillStyle = '#222'; 66 this.ctx.fillRect(this.loader.x, this.loader.y, newWidth, this.loaderHeight/2);
67 }; 68
69 /*========================================================*/
70 /* Particles 71 /*========================================================*/
72 this.Particle = function(){ 73 this.x = that.loader.x + ((that.loaded/100)*that.loaderWidth) - that.rand(0, 1);
74 this.y = that.ch/2 + that.rand(0,that.loaderHeight)-that.loaderHeight/2; 75 this.vx = (that.rand(0,4)-2)/100;
76 this.vy = (that.rand(0,that.particleLift)-that.particleLift*2)/100;
77 this.width = that.rand(2,6)/2;
78 this.height = that.rand(2,6)/2;
79 this.hue = that.hue; 80 }; 81
82 this.Particle.prototype.update = function(i){ 83 this.vx += (that.rand(0,6)-3)/100;
84 this.vy += that.gravity; 85 this.x += this.vx; 86 this.y += this.vy; 87
88 if(this.y > that.ch){ 89 that.particles.splice(i, 1); 90 } 91 }; 92
93 this.Particle.prototype.render = function(){ 94 that.ctx.fillStyle = 'hsla('+this.hue+', 100%, '+that.rand(50,70)+'%, '+that.rand(20,100)/100+')';
95 that.ctx.fillRect(this.x, this.y, this.width, this.height); 96 }; 97
98 this.createParticles = function(){ 99 var i = this.particleRate; 100 while(i--){ 101 this.particles.push(new this.Particle()); 102 }; 103 }; 104
105 this.updateParticles = function(){ 106 var i = this.particles.length; 107 while(i--){ 108 var p = this.particles[i]; 109 p.update(i); 110 }; 111 }; 112
113 this.renderParticles = function(){ 114 var i = this.particles.length; 115 while(i--){ 116 var p = this.particles[i]; 117 p.render(); 118 }; 119 }; 120
121
122 /*========================================================*/
123 /* Clear Canvas 124 /*========================================================*/
125 this.clearCanvas = function(){ 126 this.ctx.globalCompositeOperation = 'source-over'; 127 this.ctx.clearRect(0,0,this.cw,this.ch); 128 this.ctx.globalCompositeOperation = 'lighter'; 129 }; 130
131 /*========================================================*/
132 /* Animation Loop 133 /*========================================================*/
134 this.loop = function(){ 135 var loopIt = function(){ 136 that.raf = requestAnimationFrame(loopIt); 137 that.clearCanvas(); 138
139 that.createParticles(); 140
141 that.updateLoader(); 142 that.updateParticles(); 143
144 that.renderLoader(); 145 that.renderParticles(); 146
147 }; 148 loopIt(); 149 }; 150
151
152 this.stop = function(){ 153 this.ctx.globalCompositeOperation = 'source-over'; 154 this.ctx.clearRect(0,0,this.cw,this.ch); 155 window.cancelAnimationFrame(this.raf); 156 } 157
158 }; 159
160
161 /*========================================================*/
162 /* Setup requestAnimationFrame when it is unavailable. 163 /*========================================================*/
164 var setupRAF = function(){ 165 var lastTime = 0; 166 var vendors = ['ms', 'moz', 'webkit', 'o']; 167 for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x){ 168 window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; 169 window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; 170 }; 171
172 if(!window.requestAnimationFrame){ 173 window.requestAnimationFrame = function(callback, element){ 174 var currTime = new Date().getTime(); 175 var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 176 var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); 177 lastTime = currTime + timeToCall; 178 return id; 179 }; 180 }; 181
182 if (!window.cancelAnimationFrame){ 183 window.cancelAnimationFrame = function(id){ 184 clearTimeout(id); 185 }; 186 }; 187 };
我在源码基础上加了个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.**,也就是说都暴露给外界了。这些属性都是跟效果相关的,不少属性须要看着效果调试出来的。暴露出来的好处就是调试的时候能够在运行时动态改变相应的值,观察效果的变化,很是方便。大家感觉一下:
但并非全部属性都应该被暴露出来的,哪些须要暴露,哪些须要隐藏这个要看具体场景了。另外私有成员的命名潜规则(←.←)是前面加_,私有属性和私有方法都应该这样命名,这样同类们一看到就懂啦。
封装的另一个方面是要与DOM对象松耦合,一个组件假如跟其余元素的联系很紧密的话,移植性就很是差了。这一点暂时我还没太多体会,不敢妄议。
就说到这里啦,看起来不是颇有料呢。。。因此,仍是补张图片丰满一下吧~码字不易,顺手点赞哈!
(图片出处:著名摄影师 小张同窗,转载请注明)
原创文章,转载请注明出处!本文连接:http://www.cnblogs.com/qieguo/p/5438380.html