超炫的HTML5粒子效果進度條 VS 如何規範而優雅地code

最近瞎逛的時候發現了一個超炫的粒子進度效果,有多炫呢?請擦亮眼鏡!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 };
View Code

我在源碼基礎上加了個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 

相關文章
相關標籤/搜索