想要實作一個效果,首先我們要簡化模型,可以分成色彩的變化、位置的變化、大小的變化等,還有就是将某個因子獨立出來看,通過各種抽繭剝絲的手法去找到效果後面的數學模型,然後程式設計去實作它。藝術總是源于生活,是以在做時候應該好好考慮是否應該加入慣性、彈性、重力這些效果,這些實體特性反映到效果中的話,會更加自然逼真。
最近瞎逛的時候發現了一個超炫的粒子進度效果,有多炫呢?請擦亮眼鏡!
粗略一看真的被驚豔到了,而且很實用啊有木有!這是 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,初始化的時候清除了進度條位置和粒子位置,改動就這兩點。大家可以在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