javascript动画浅析

动画原理

  

  所谓的动画,就是通过一些列的运动形成的动的画面。在网页中,我们可以通过不断的改变元素的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;}.

  加入缓动后上面的公式变为

  动画元素开始值 + (动画元素结束值 - 动画元素开始值) * 缓动函数((当前时间-开始时间) / (动画需要时间)) + 值的格式。

  至此便是整个动画类设计便结束了。其中参考了一些其它人的博客,在此表示感谢!

  最后,还是贴一下详细代码吧。

  

复制代码 代码如下:

  /**

  * create time 2012/08/29

  * @author lynx cat.

  * @version 0.77beta.

  */

  (function(win,doc){

  var win = win || window;

  var doc = doc || win.document,

  pow = Math.pow,

  sin = Math.sin,

  PI = Math.PI,

  BACK_CONST = 1.70158;

  var Easing = {

  // 匀速运动

  linear : function(t){

  return t;

  },

  easeIn : function (t) {

  return t * t;

  },

  easeOut : function (t) {

  return ( 2 - t) * t;

  },

  easeBoth : function (t) {

  return (t *= 2) < 1 ?

  .5 * t * t :

  .5 * (1 - (--t) * (t - 2));

  },

  easeInStrong : function (t) {

  return t * t * t * t;

  },

  easeOutStrong : function (t) {

  return 1 - (--t) * t * t * t;

  },

  easeBothStrong: function (t) {

  return (t *= 2) < 1 ?

  .5 * t * t * t * t :

  .5 * (2 - (t -= 2) * t * t * t);

  },

  easeOutQuart : function(t){

  return -(pow((t-1), 4) -1)

  },

  easeInOutExpo : function(t){

  if(t===0) return 0;

  if(t===1) return 1;

  if((t/=0.5) < 1) return 0.5 * pow(2,10 * (t-1));

  return 0.5 * (-pow(2, -10 * --t) + 2);

  },

  easeOutExpo : function(t){

  return (t===1) ? 1 : -pow(2, -10 * t) + 1;

  },

  swingFrom : function(t) {

  return t*t*((BACK_CONST+1)*t - BACK_CONST);

  },

  swingTo: function(t) {

  return (t-=1)*t*((BACK_CONST+1)*t + BACK_CONST) + 1;

  },

  sinusoidal : function(t) {

  return (-Math.cos(t*PI)/2) + 0.5;

  },

  flicker : function(t) {

  var t = t + (Math.random()-0.5)/5;

  return this.sinusoidal(t < 0 ? 0 : t > 1 ? 1 : t);

  },

  backIn : function (t) {

  if (t === 1) t -= .001;

  return t * t * ((BACK_CONST + 1) * t - BACK_CONST);

  },

  backOut : function (t) {

  return (t -= 1) * t * ((BACK_CONST + 1) * t + BACK_CONST) + 1;

  },

  bounce : function (t) {

  var s = 7.5625, r;

  if (t < (1 / 2.75)) {

  r = s * t * t;

  }

  else if (t < (2 / 2.75)) {

  r = s * (t -= (1.5 / 2.75)) * t + .75;

  }

  else if (t < (2.5 / 2.75)) {

  r = s * (t -= (2.25 / 2.75)) * t + .9375;

  }

  else {

  r = s * (t -= (2.625 / 2.75)) * t + .984375;

  }

  return r;

  }

  };

  /**

  * 基石 用于返回一个包含对话方法的对象

  * @param elem

  * @return {Object}

  */

  function catfx(elem){

  elem = typeof elem === 'string' ? doc.getElementById(elem) : elem;

  return new fx(elem);

  }

  /**

  * 内部基石 用于返回一个包含对话方法的对象

  * @param elem

  * @return {Object}

  */

  function fx(elem){

  this.elem = elem;

  return this;

  }

  /**

  * 基础类 包含一些基础方法,和不变量

  */

  var fxBase = {

  speed : {

  slow : 600,

  fast : 200,

  defaults : 400

  },

  fxAttrs : [],

  fxMap:[],

  /**

  * 返回对象元素的css值

  * @param elem

  * @param p

  * @return css value

  */

  getStyle : function(){

  var fn = function (){};

  if('getComputedStyle' in win){

  fn = function(elem, p){

  var p = p.replace(/\-(\w)/g,function(i,str){

  return str.toUpperCase();

  });

  var val = getComputedStyle(elem, null)[p];

  if(~(' '+p+' ').indexOf(' left right top bottom ') && val === 'auto'){

  val = '0px';

  }

  return val;

  }

  }else {

  fn = function(elem, p){

  var p = p.replace(/\-(\w)/g,function(i,str){

  return str.toUpperCase();

  });

  var val = elem.currentStyle[p];

  if(~(' '+p+' ').indexOf(' width height') && val === 'auto'){

  var rect = elem.getBoundingClientRect();

  val = ( p === 'width' ? rect.right - rect.left : rect.bottom - rect.top ) + 'px';

  }

  if(p === 'opacity'){

  var filter = elem.currentStyle.filter;

  if( /opacity/.test(filter) ){

  val = filter.match( /\d+/ )[0] / 100;

  val = (val === 1 || val === 0) ? val.toFixed(0) : val.toFixed(1);

  }else if( val === undefined ){

  val = 1;

  }

  }

  if(~(' '+p+' ').indexOf(' left right top bottom ') && val === 'auto'){

  val = '0px';

  }

  return val;

  }

  }

  return fn;

  }(),

  /**

  * 返回对象元素的css值

  * @param 颜色值(暂不支持red,pink,blue等英文)

  * @return rgb(x,x,x)

  */

  getColor : function(val){

  var r, g, b;

  if(/rgb/.test(val)){

  var arr = val.match(/\d+/g);

  r = arr[0];

  g = arr[1];

  b = arr[2];

  }else if(/#/.test(val)){

  var len = val.length;

  if( len === 7 ){

  r = parseInt( val.slice(1, 3), 16);

  g = parseInt( val.slice(3, 5), 16);

  b = parseInt( val.slice(5), 16);

  }

  else if( len === 4 ){

  r = parseInt(val.charAt(1) + val.charAt(1), 16);

  g = parseInt(val.charAt(2) + val.charAt(2), 16);

  b = parseInt(val.charAt(3) + val.charAt(3), 16);

  }

  }else{

  return val;

  }

  return {

  r : parseFloat(r),

  g : parseFloat(g),

  b : parseFloat(b)

  }

  },

  /**

  * 返回解析后的css

  * @param prop

  * @return {val:val,unit:unit}

  */

  parseStyle : function(prop){

  var val = parseFloat(prop),

  unit = prop.replace(/^[\-\d\.]+/, '');

  if(isNaN(val)){

  val = this.getColor(unit);

  unit = '';

  }

  return {val : val, unit : unit};

  },

  /**

  * 设置元素的透明度

  * @param elem

  * @param val

  */

  setOpacity : function(elem, val){

  if( 'getComputedStyle' in win ){

  elem.style.opacity = val === 1 ? '' : val;

  }else{

  elem.style.zoom = 1;

  elem.style.filter = val === 1 ? '' : 'alpha(opacity=' + val * 100 + ')';

  }

  },

  /**

  * 设置元素的css值

  * @param elem

  * @param prop

  * @param val

  */

  setStyle : function(elem, prop, val){

  if(prop != 'opacity'){

  prop = prop.replace(/\-(\w)/g,function(i,p){

  return p.toUpperCase();

  });

  elem.style[prop] = val;

  }else{

  this.setOpacity(elem, val);

  }

  },

  /**

  * 返回解析后的prop

  * @param prop

  * @return {prop}

  */

  parseProp : function(prop){

  var props = {};

  for(var i in prop){

  props[i] = this.parseStyle(prop[i].toString());

  }

  return props;

  },

  /**

  * 修正用户的参数

  * @param elem

  * @param duration

  * @param easing

  * @param callback

  * @return {options}

  */

  setOption : function(elem,duration, easing, callback){

  var options = {};

  var _this = this;

  options.duration = function(duration){

  if(typeof duration == 'number'){

  return duration;

  }else if(typeof duration == 'string' && _this.speed[duration]){

  return _this.speed[duration];

  }else{

  return _this.speed.defaults;

  }

  }(duration);

  options.easing = function(easing){

  if(typeof easing == 'function'){

  return easing;

  }else if(typeof easing == 'string' && Easing[easing]){

  return Easing[easing];

  }else{

  return Easing.linear;

  }

  }(easing);

  options.callback = function(callback){

  var _this = this;

  return function (){

  if(typeof callback == 'function'){

  callback.call(elem);

  }

  }

  }(callback)

  return options;

  },

  /**

  * 维护setInterval的函数,动画的启动

  */

  tick : function(){

  var _this = this;

  if(!_this.timer){

  _this.timer = setInterval(function(){

  for(var i = 0, len = _this.fxMap.length; i < len; i++){

  var elem = _this.fxMap[i][0];

  var core = _this.data(elem)[0];

  core(elem);

  }

  },16);

  }

  },

  /**

  * 停止所有动画

  */

  stop : function(){

  if(this.timer){

  clearInterval(this.timer);

  this.timer = undefined;

  }

  },

  /**

  * 存储或者拿出队列

  * @param elem

  */

  data : function(elem){

  for(var i = 0, len = this.fxMap.length; i < len; i++){

  var data = this.fxMap[i];

  if(elem === data[0]){

  return data[1];

  }

  }

  this.fxMap.push([elem,[]]);

  return this.fxMap[this.fxMap.length - 1][1];

  },

  /**

  * 删除队列

  * @param elem

  */

  removeData : function(elem){

  for(var i = 0, len = this.fxMap.length; i < len; i++){

  var data = this.fxMap[i];

  if(elem === data[0]){

  this.fxMap.splice(i, 1);

  if(this.isDataEmpty()){

  this.stop();

  }

  }

  }

  },

  isDataEmpty : function(){

  return this.fxMap.length == 0;

  }

  }, $ = fxBase;

  /**

  * 核心对象,用于生成动画对象。

  * @param elem

  * @param props

  * @param options

  * @return {Object}

  */

  function fxCore(elem, props, options){

  this.elem = elem;

  this.props = props;

  this.options = options;

  this.start();

  }

  fxCore.prototype = {

  constructor : fxCore,

  /**

  * 将动画函数加入到队列中,并启动动画。

  */

  start : function(){

  var cores = $.data(this.elem);

  cores.push(this.step());

  $.tick();

  },

  /**

  * 核心方法,控制每一帧元素的状态。

  * @return function

  */

  step : function(){

  var _this = this;

  var fn = function(elem){

  var t = Date.now() - this.startTime;

  if(Date.now() < this.startTime + this.options.duration){

  if(t <= 1){ t = 1;}

  for(var i in this.target){

  if(typeof this.source[i]['val'] === 'number'){

  var val = parseFloat((this.source[i]['val'] + (this.target[i]['val'] - this.source[i]['val']) * this.options.easing(t / this.options.duration)).toFixed(7));

  }else{

  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));

  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));

  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));

  var val = 'rgb(' + r + ',' + g + ',' + b + ')';

  }

  $.setStyle(this.elem,i,val + this.source[i]['unit']);

  }

  }else{

  for(var i in this.target){

  if(typeof this.target[i]['val'] === 'number'){

  var val = this.target[i]['val'];

  }else{

  var val = 'rgb(' + this.target[i]['val']['r'] + ',' + this.target[i]['val']['g'] + ',' + this.target[i]['val']['b'] + ')';

  }

  $.setStyle(elem,i,val + this.source[i]['unit']);

  }

  var cores = $.data(elem);

  cores.shift();

  this.options.callback();

  if(cores.length == 0){

  $.setStyle(elem,'overflow',this.overflow);

  $.removeData(elem);

  }

  }

  }

  return function(elem){

  if(!_this.startTime){

  var source = {};

  _this.target = _this.props;

  for(var i in _this.props){

  var val = $.getStyle(_this.elem, i);

  source[i] = $.parseStyle(val);

  }

  _this.source = source;

  _this.startTime = Date.now();

  _this.overflow = $.getStyle(elem,'overflow');

  $.setStyle(elem,'overflow','hidden');

  }

  fn.call(_this,elem);

  }

  }

  }

  /**

  * 外部接口类。

  */

  fx.prototype = {

  constructor : fx,

  /**

  * 动画方法

  * @param prop

  * @param duration

  * @param easing

  * @param callback

  * @return {Object}

  */

  animate : function(prop, duration, easing, callback){

  if(arguments.length == 3 && typeof easing === 'function'){ //多数时候用户第三个参数是回调

  callback = easing;

  easing = undefined;

  }

  var props = $.parseProp(prop);

  var options = $.setOption(this.elem,duration,easing,callback);

  var core = new fxCore(this.elem,props,options);

  return this;

  },

  /**

  * 停止动画方法

  * 使用方法 catjs('your element id').stop();

  */

  stop : function(){

  $.removeData(this.elem);

  }

  }

  win.catfx = catfx;

  })(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('洒家是回调函数~')});