最近瞎逛的時候發現了一個超炫的粒子進度效果,有多炫呢?請擦亮眼鏡! // <![CDATA[ /* */ /* Light Loader /* */ var lightLoader = function(c, cw, ch){ var _this = this; this.c = c; this.c ...
最近瞎逛的時候發現了一個超炫的粒子進度效果,有多炫呢?請擦亮眼鏡!
粗略一看真的被驚艷到了,而且很實用啊有木有!這是 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?好吧,你也猜到了我要說什麼,對的,復用和封裝。
先看人家的源碼,貌似這哥們連停止動畫都沒寫呢,就一個無限迴圈。。。
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,初始化的時候清除了進度條位置和粒子位置,改動就這兩點。後續引用這個組件非常簡單:
var c = document.createElement('canvas');
c.width = 400;
c.height = 100;
c.id = 'canvas-test1';
parent.appendChild(c); //在需要的位置加入canvas元素
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