最近一直在弄手機端的游戲,接觸到各種動畫。加之對之前的自己那個動畫類不滿意,就有心想寫個新的。
然后翻看各種博客,查資料。也學到一些新的東西。
動畫原理
所謂的動畫,就是通過一些列的運動形成的動的畫面。在網頁中,我們可以通過不斷的改變元素的css值,來達到動的效果。
用到的公式
總距離S = 總時間T * 速度V 即: V = S/T
當前距離s = S/T * 已耗時t 即: s = S * (t/T)
即:當前距離 = 總距離 * (已耗時/總時間)
即:動畫元素開始值 + (動畫元素結束值 - 動畫元素開始值) * (當前時間-開始時間) / (動畫需要時間) + 值的格式
有了上面這些公式,我們就能利用javascript的setInterval或者setTimeout來做一個簡單的動畫了。
然而想要做一個動畫庫,就不得不考慮另外一些因素了。 比如同一個元素的動畫,必須要有順序的執行。不同元素的動畫可以同步運行。
如此一來,就必須得用另外一個對象來管理這些動畫了。我開始的想法是講每個元素都放在一個數組里,用幾個setInterval來循環取出數組中的動畫函數依次執行。
animate1 = [{elem,fn},{elem,fn}];
animate2 = [{elem,fn},{elem,fn}];
這樣就能達到,相同的元素動畫,是有順序的執行,而不同的則可以同時運行了。然后這樣卻存在一個問題,那就是如果超過10個元素的動畫。程序就要開十個setInterval。
為了避免這樣的情況發生,就在上面的基礎上做了一些改進。使得,不論多少個動畫。都使用一個setInterval來完成。修改后結構如下。
[
[elem,[fn,fn,fn,fn]],
[elem,[fn,fn,fn,fn]],
[elem,[fn,fn,fn,fn]]
]
這樣一來,就可以用一個setInterval來完成所有動畫了。 所需要做就是,循環取出elem,並執行elem后面一個元素的頭一個fn,fn執行完畢后刪除fn。調用下一個fn,如果fn全部為空則從大的數組中刪除elem,如果elem為空時,則清楚setInterval。這樣一來,邏輯上便可以走得通了。
然而動畫最關鍵的因素還有一個,那就是緩動。 如果沒有緩動,那么動畫效果看起來就非常的死板。千篇一律。目前做js動畫用到的緩動算法是很多的,大致分為兩類。
一種是flash類,一種是prototype類。
flash的需要四個參數。分別是,
1.時間初始話的時間t
2.動畫的初始值b
3.動畫的結束值c
4.動畫持續的時間d
下面是一個flash 類的勻速運動算法
Linear: function(t,b,c,d){ return c*t/d + b; }
另一種則是prototype,這一類的參數只需要一個,那就是當前時間t與持續時間d的比值 (t/d)
我采用了第二種,因為它的參數方便。也更加適合上面的動畫公式,下面是一個prototype類的勻速運動算法
linear : function(t){ return t;}.
加入緩動后上面的公式變為
動畫元素開始值 + (動畫元素結束值 - 動畫元素開始值) * 緩動函數((當前時間-開始時間) / (動畫需要時間)) + 值的格式。
至此便是整個動畫類設計便結束了。其中參考了一些其它人的博客,在此表示感謝!
最后,還是貼一下詳細代碼吧。
1 /** 2 * create time 2012/08/29 3 * @author lynx cat. 4 * @version 0.77beta. 5 */ 6 7 8 (function(win,doc){ 9 var win = win || window; 10 var doc = doc || win.document, 11 pow = Math.pow, 12 sin = Math.sin, 13 PI = Math.PI, 14 BACK_CONST = 1.70158; 15 16 var Easing = { 17 // 勻速運動 18 linear : function(t){ 19 return t; 20 }, 21 easeIn : function (t) { 22 return t * t; 23 }, 24 easeOut : function (t) { 25 return ( 2 - t) * t; 26 }, 27 easeBoth : function (t) { 28 return (t *= 2) < 1 ? 29 .5 * t * t : 30 .5 * (1 - (--t) * (t - 2)); 31 }, 32 easeInStrong : function (t) { 33 return t * t * t * t; 34 }, 35 easeOutStrong : function (t) { 36 return 1 - (--t) * t * t * t; 37 }, 38 easeBothStrong: function (t) { 39 return (t *= 2) < 1 ? 40 .5 * t * t * t * t : 41 .5 * (2 - (t -= 2) * t * t * t); 42 }, 43 easeOutQuart : function(t){ 44 return -(pow((t-1), 4) -1) 45 }, 46 easeInOutExpo : function(t){ 47 if(t===0) return 0; 48 if(t===1) return 1; 49 if((t/=0.5) < 1) return 0.5 * pow(2,10 * (t-1)); 50 return 0.5 * (-pow(2, -10 * --t) + 2); 51 }, 52 easeOutExpo : function(t){ 53 return (t===1) ? 1 : -pow(2, -10 * t) + 1; 54 }, 55 swingFrom : function(t) { 56 return t*t*((BACK_CONST+1)*t - BACK_CONST); 57 }, 58 swingTo: function(t) { 59 return (t-=1)*t*((BACK_CONST+1)*t + BACK_CONST) + 1; 60 }, 61 sinusoidal : function(t) { 62 return (-Math.cos(t*PI)/2) + 0.5; 63 }, 64 flicker : function(t) { 65 var t = t + (Math.random()-0.5)/5; 66 return this.sinusoidal(t < 0 ? 0 : t > 1 ? 1 : t); 67 }, 68 backIn : function (t) { 69 if (t === 1) t -= .001; 70 return t * t * ((BACK_CONST + 1) * t - BACK_CONST); 71 }, 72 backOut : function (t) { 73 return (t -= 1) * t * ((BACK_CONST + 1) * t + BACK_CONST) + 1; 74 }, 75 bounce : function (t) { 76 var s = 7.5625, r; 77 78 if (t < (1 / 2.75)) { 79 r = s * t * t; 80 } 81 else if (t < (2 / 2.75)) { 82 r = s * (t -= (1.5 / 2.75)) * t + .75; 83 } 84 else if (t < (2.5 / 2.75)) { 85 r = s * (t -= (2.25 / 2.75)) * t + .9375; 86 } 87 else { 88 r = s * (t -= (2.625 / 2.75)) * t + .984375; 89 } 90 91 return r; 92 } 93 }; 94 95 /** 96 * 基石 用於返回一個包含對話方法的對象 97 * @param elem 98 * @return {Object} 99 */ 100 101 function catfx(elem){ 102 elem = typeof elem === 'string' ? doc.getElementById(elem) : elem; 103 return new fx(elem); 104 } 105 106 /** 107 * 內部基石 用於返回一個包含對話方法的對象 108 * @param elem 109 * @return {Object} 110 */ 111 function fx(elem){ 112 this.elem = elem; 113 return this; 114 } 115 116 /** 117 * 基礎類 包含一些基礎方法,和不變量 118 */ 119 var fxBase = { 120 speed : { 121 slow : 600, 122 fast : 200, 123 defaults : 400 124 }, 125 fxAttrs : [], 126 fxMap:[], 127 128 /** 129 * 返回對象元素的css值 130 * @param elem 131 * @param p 132 * @return css value 133 */ 134 getStyle : function(){ 135 var fn = function (){}; 136 if('getComputedStyle' in win){ 137 fn = function(elem, p){ 138 var p = p.replace(/\-(\w)/g,function(i,str){ 139 return str.toUpperCase(); 140 }); 141 var val = getComputedStyle(elem, null)[p]; 142 if(~(' '+p+' ').indexOf(' left right top bottom ') && val === 'auto'){ 143 val = '0px'; 144 } 145 return val; 146 } 147 }else { 148 fn = function(elem, p){ 149 var p = p.replace(/\-(\w)/g,function(i,str){ 150 return str.toUpperCase(); 151 }); 152 var val = elem.currentStyle[p]; 153 154 if(~(' '+p+' ').indexOf(' width height') && val === 'auto'){ 155 var rect = elem.getBoundingClientRect(); 156 val = ( p === 'width' ? rect.right - rect.left : rect.bottom - rect.top ) + 'px'; 157 } 158 159 if(p === 'opacity'){ 160 var filter = elem.currentStyle.filter; 161 if( /opacity/.test(filter) ){ 162 val = filter.match( /\d+/ )[0] / 100; 163 val = (val === 1 || val === 0) ? val.toFixed(0) : val.toFixed(1); 164 }else if( val === undefined ){ 165 val = 1; 166 } 167 } 168 169 if(~(' '+p+' ').indexOf(' left right top bottom ') && val === 'auto'){ 170 val = '0px'; 171 } 172 173 return val; 174 } 175 } 176 return fn; 177 }(), 178 179 /** 180 * 返回對象元素的css值 181 * @param 顏色值(暫不支持red,pink,blue等英文) 182 * @return rgb(x,x,x) 183 */ 184 getColor : function(val){ 185 var r, g, b; 186 if(/rgb/.test(val)){ 187 var arr = val.match(/\d+/g); 188 r = arr[0]; 189 g = arr[1]; 190 b = arr[2]; 191 }else if(/#/.test(val)){ 192 var len = val.length; 193 if( len === 7 ){ 194 r = parseInt( val.slice(1, 3), 16); 195 g = parseInt( val.slice(3, 5), 16); 196 b = parseInt( val.slice(5), 16); 197 } 198 else if( len === 4 ){ 199 r = parseInt(val.charAt(1) + val.charAt(1), 16); 200 g = parseInt(val.charAt(2) + val.charAt(2), 16); 201 b = parseInt(val.charAt(3) + val.charAt(3), 16); 202 } 203 }else{ 204 return val; 205 } 206 return { 207 r : parseFloat(r), 208 g : parseFloat(g), 209 b : parseFloat(b) 210 } 211 }, 212 /** 213 * 返回解析后的css 214 * @param prop 215 * @return {val:val,unit:unit} 216 */ 217 parseStyle : function(prop){ 218 var val = parseFloat(prop), 219 unit = prop.replace(/^[\-\d\.]+/, ''); 220 if(isNaN(val)){ 221 val = this.getColor(unit); 222 unit = ''; 223 } 224 return {val : val, unit : unit}; 225 }, 226 /** 227 * 設置元素的透明度 228 * @param elem 229 * @param val 230 */ 231 setOpacity : function(elem, val){ 232 if( 'getComputedStyle' in win ){ 233 elem.style.opacity = val === 1 ? '' : val; 234 }else{ 235 elem.style.zoom = 1; 236 elem.style.filter = val === 1 ? '' : 'alpha(opacity=' + val * 100 + ')'; 237 } 238 }, 239 /** 240 * 設置元素的css值 241 * @param elem 242 * @param prop 243 * @param val 244 */ 245 setStyle : function(elem, prop, val){ 246 if(prop != 'opacity'){ 247 prop = prop.replace(/\-(\w)/g,function(i,p){ 248 return p.toUpperCase(); 249 }); 250 elem.style[prop] = val; 251 }else{ 252 this.setOpacity(elem, val); 253 } 254 }, 255 /** 256 * 返回解析后的prop 257 * @param prop 258 * @return {prop} 259 */ 260 parseProp : function(prop){ 261 var props = {}; 262 for(var i in prop){ 263 props[i] = this.parseStyle(prop[i].toString()); 264 } 265 return props; 266 }, 267 /** 268 * 修正用戶的參數 269 * @param elem 270 * @param duration 271 * @param easing 272 * @param callback 273 * @return {options} 274 */ 275 setOption : function(elem,duration, easing, callback){ 276 var options = {}; 277 var _this = this; 278 options.duration = function(duration){ 279 if(typeof duration == 'number'){ 280 return duration; 281 }else if(typeof duration == 'string' && _this.speed[duration]){ 282 return _this.speed[duration]; 283 }else{ 284 return _this.speed.defaults; 285 } 286 }(duration); 287 288 options.easing = function(easing){ 289 if(typeof easing == 'function'){ 290 return easing; 291 }else if(typeof easing == 'string' && Easing[easing]){ 292 return Easing[easing]; 293 }else{ 294 return Easing.linear; 295 } 296 }(easing); 297 298 options.callback = function(callback){ 299 var _this = this; 300 return function (){ 301 if(typeof callback == 'function'){ 302 callback.call(elem); 303 } 304 } 305 }(callback) 306 307 return options; 308 }, 309 /** 310 * 維護setInterval的函數,動畫的啟動 311 */ 312 tick : function(){ 313 var _this = this; 314 if(!_this.timer){ 315 _this.timer = setInterval(function(){ 316 for(var i = 0, len = _this.fxMap.length; i < len; i++){ 317 var elem = _this.fxMap[i][0]; 318 var core = _this.data(elem)[0]; 319 core(elem); 320 } 321 },16); 322 } 323 }, 324 /** 325 * 停止所有動畫 326 */ 327 stop : function(){ 328 if(this.timer){ 329 clearInterval(this.timer); 330 this.timer = undefined; 331 } 332 }, 333 /** 334 * 存儲或者拿出隊列 335 * @param elem 336 */ 337 data : function(elem){ 338 for(var i = 0, len = this.fxMap.length; i < len; i++){ 339 var data = this.fxMap[i]; 340 if(elem === data[0]){ 341 return data[1]; 342 } 343 } 344 this.fxMap.push([elem,[]]); 345 return this.fxMap[this.fxMap.length - 1][1]; 346 347 }, 348 /** 349 * 刪除隊列 350 * @param elem 351 */ 352 removeData : function(elem){ 353 for(var i = 0, len = this.fxMap.length; i < len; i++){ 354 var data = this.fxMap[i]; 355 if(elem === data[0]){ 356 this.fxMap.splice(i, 1); 357 if(this.isDataEmpty()){ 358 this.stop(); 359 } 360 } 361 } 362 }, 363 isDataEmpty : function(){ 364 return this.fxMap.length == 0; 365 } 366 }, $ = fxBase; 367 368 /** 369 * 核心對象,用於生成動畫對象。 370 * @param elem 371 * @param props 372 * @param options 373 * @return {Object} 374 */ 375 function fxCore(elem, props, options){ 376 this.elem = elem; 377 this.props = props; 378 this.options = options; 379 this.start(); 380 } 381 382 fxCore.prototype = { 383 constructor : fxCore, 384 /** 385 * 將動畫函數加入到隊列中,並啟動動畫。 386 */ 387 start : function(){ 388 var cores = $.data(this.elem); 389 cores.push(this.step()); 390 $.tick(); 391 }, 392 /** 393 * 核心方法,控制每一幀元素的狀態。 394 * @return function 395 */ 396 step : function(){ 397 var _this = this; 398 var fn = function(elem){ 399 var t = Date.now() - this.startTime; 400 if(Date.now() < this.startTime + this.options.duration){ 401 if(t <= 1){ t = 1;} 402 for(var i in this.target){ 403 if(typeof this.source[i]['val'] === 'number'){ 404 var val = parseFloat((this.source[i]['val'] + (this.target[i]['val'] - this.source[i]['val']) * this.options.easing(t / this.options.duration)).toFixed(7)); 405 }else{ 406 var r = parseInt(this.source[i]['val']['r'] + (this.target[i]['val']['r'] - this.source[i]['val']['r']) * this.options.easing(t / this.options.duration)); 407 var g = parseInt(this.source[i]['val']['g'] + (this.target[i]['val']['g'] - this.source[i]['val']['g']) * this.options.easing(t / this.options.duration)); 408 var b = parseInt(this.source[i]['val']['b'] + (this.target[i]['val']['b'] - this.source[i]['val']['b']) * this.options.easing(t / this.options.duration)); 409 var val = 'rgb(' + r + ',' + g + ',' + b + ')'; 410 } 411 $.setStyle(this.elem,i,val + this.source[i]['unit']); 412 } 413 }else{ 414 for(var i in this.target){ 415 if(typeof this.target[i]['val'] === 'number'){ 416 var val = this.target[i]['val']; 417 }else{ 418 var val = 'rgb(' + this.target[i]['val']['r'] + ',' + this.target[i]['val']['g'] + ',' + this.target[i]['val']['b'] + ')'; 419 } 420 $.setStyle(elem,i,val + this.source[i]['unit']); 421 } 422 var cores = $.data(elem); 423 cores.shift(); 424 this.options.callback(); 425 if(cores.length == 0){ 426 $.setStyle(elem,'overflow',this.overflow); 427 $.removeData(elem); 428 } 429 } 430 } 431 return function(elem){ 432 if(!_this.startTime){ 433 var source = {}; 434 _this.target = _this.props; 435 for(var i in _this.props){ 436 var val = $.getStyle(_this.elem, i); 437 source[i] = $.parseStyle(val); 438 } 439 _this.source = source; 440 _this.startTime = Date.now(); 441 _this.overflow = $.getStyle(elem,'overflow'); 442 $.setStyle(elem,'overflow','hidden'); 443 } 444 fn.call(_this,elem); 445 } 446 } 447 } 448 449 /** 450 * 外部接口類。 451 */ 452 fx.prototype = { 453 constructor : fx, 454 /** 455 * 動畫方法 456 * @param prop 457 * @param duration 458 * @param easing 459 * @param callback 460 * @return {Object} 461 */ 462 animate : function(prop, duration, easing, callback){ 463 if(arguments.length == 3 && typeof easing === 'function'){ //多數時候用戶第三個參數是回調 464 callback = easing; 465 easing = undefined; 466 } 467 var props = $.parseProp(prop); 468 var options = $.setOption(this.elem,duration,easing,callback); 469 var core = new fxCore(this.elem,props,options); 470 return this; 471 }, 472 /** 473 * 停止動畫方法 474 * 使用方法 catjs('your element id').stop(); 475 */ 476 stop : function(){ 477 $.removeData(this.elem); 478 } 479 } 480 481 win.catfx = catfx; 482 })(this,document);
使用起來也比較簡單.直接catfx('ID').animate({'margin-left':200,'background-color':'#ff0000'},600,'easeOut',function(){});
跟jquery的使用方法差不多,如果不傳第二個參數,則默認為400毫秒。不傳第三個參數則默認勻速。第三個參數為函數,並且總共只有三個參數時。第三個參數為回調。
例:catfx('ID').animate({'margin-left':200,'background-color':'#ff0000'},600,function(){alert('灑家是回調函數~')});