Source: hui_animation.js

hui = window.hui || {};

/////////////////////////// Animation ///////////////////////////

/**
 * Animate something
 * <pre><strong>options:</strong> {
 *  node : «Element»,
 *  css : { fontSize : '11px', color : '#f00', opacity : 0.5 },
 *  duration : 1000, // 1sec
 *  ease : function(num) {},
 *  $complete : function() {}
 *}
 * TODO Document options.property, options.value
 * @memberof hui
 *
 * @param {Element | Object} options Options or an element
 * @param {String} style The css property
 * @param {String} value The css value
 * @param {Number} duration The duration in milisecons
 * @param {Object} delegate The options if first param is an element
 */
hui.animate = function(options, property, value, duration, delegate) {
  if (typeof(options) == 'string' || hui.dom.isElement(options)) {
    hui.animation.get(options).animate(null, value, property, duration, delegate);
  } else {
    var item = hui.animation.get(options.node);
    if (options.property) {
      item.animate(null, options.value, options.property, options.duration, options);
    } else if (!options.css) {
      item.animate(null, '', '', options.duration, options);
    } else {
      var o = options;
      for (var prop in options.css) {
        item.animate(null, options.css[prop], prop, options.duration, o);
        o = hui.override({}, options);
        o.$complete = undefined;
      }
    }
  }
};


/** @namespace */
hui.animation = {
  objects: {},
  running: false,
  latestId: 0,

  /**
   * Get an animation item for a node
   * @return hui.animation.Item
   */
  get: function(element) {
    element = hui.get(element);
    if (!element.huiAnimationId) {
      element.huiAnimationId = this.latestId++;
    }
    if (!this.objects[element.huiAnimationId]) {
      this.objects[element.huiAnimationId] = new hui.animation.Item(element);
    }
    return this.objects[element.huiAnimationId];
  },

  /**
   * Start animating any pending tasks
   */
  start: function() {
    if (!this.running) {
      hui.animation._render();
    }
  }
};


hui.animation._lengthUpater = function(element,v,work) {
  element.style[work.property] = (work.from + (work.to - work.from) * v) + (work.unit ? work.unit : '');
};

hui.animation._transformUpater = function(element, v, work) {
  var t = work.transform;
  var str = '';
  if (t.rotate) {
    str += ' rotate(' + (t.rotate.from + (t.rotate.to - t.rotate.from) * v) + t.rotate.unit + ')';
  }
  if (t.scale) {
    str += ' scale(' + (t.scale.from + (t.scale.to - t.scale.from) * v) + ')';
  }
  element.style[hui.animation.TRANSFORM] = str;
};

hui.animation._colorUpater = function(element, v, work) {
  var red = Math.round(work.from.red + (work.to.red - work.from.red) * v);
  var green = Math.round(work.from.green + (work.to.green - work.from.green) * v);
  var blue = Math.round(work.from.blue + (work.to.blue - work.from.blue) * v);
  if (work.to.alpha < 255 || work.from.alpha < 255) {
    var alpha = Math.max(0,Math.min(1,work.from.alpha + (work.to.alpha - work.from.alpha) * v));
    element.style[work.property] = 'rgba(' + red + ',' + green + ',' + blue + ',' + alpha + ')';
  } else {
    element.style[work.property] = 'rgb(' + red + ',' + green + ',' + blue + ')';
  }
};

hui.animation._propertyUpater = function(element, v, work) {
  element[work.property] = Math.round(work.from+(work.to-work.from)*v);
};

hui.animation._ieOpacityUpdater = function(element, v, work) {
  var opacity = (work.from + (work.to - work.from) * v);
  if (opacity == 1) {
    element.style.removeAttribute('filter');
  } else {
    element.style.filter = 'alpha(opacity=' + (opacity * 100) + ')';
  }
};

hui.animation._render = function() {
  hui.animation.running = true;
  var next = false,
  stamp = Date.now();
  for (var id in hui.animation.objects) {
    var obj = hui.animation.objects[id];
    if (obj.work) {
      var element = obj.element;
      for (var i=0; i < obj.work.length; i++) {
        var work = obj.work[i];
        if (work.finished) {
          continue;
        }
        var place = (stamp-work.start)/(work.end-work.start);
        if (place < 0) {
          next = true;
          continue;
        }
        else if (isNaN(place) || place>1) {
          place = 1;
        }
        else if (place<1) {
          next=true;
        }
        var v = place,
          value = null;
        if (work.delegate && work.delegate.ease) {
          v = work.delegate.ease(v);
        }
        if (work.delegate && work.delegate.$render) {
          work.delegate.$render(element,v);
        } else if (work.delegate && work.delegate.callback) {
          work.delegate.callback(element,v);
        } else if (work.updater) {
          work.updater(element,v,work);
        }
        if (place==1) {
          work.finished = true;
          if (work.delegate && work.delegate.$complete) {
            window.setTimeout(work.delegate.$complete);
          } else if (work.delegate && work.delegate.onComplete) {
            window.setTimeout(work.delegate.onComplete);
          } else if (work.delegate && work.delegate.hideOnComplete) {
            element.style.display='none';
          }
        }
      }
    }
  }
  if (next) {
    hui.onDraw(hui.animation._render);
  } else {
    hui.animation.running = false;
  }
};

hui.animation._parseStyle = function(value) {
  var parsed = {type:null,value:null,unit:null};
  var match;
  if (!hui.isDefined(value)) {
    return parsed;
  }
  if (!isNaN(value)) {
    parsed.value=parseFloat(value);
    return parsed;
  }
  match = value.match(/([\-]?[0-9\.]+)(px|pt|%)/);
  if (match) {
    parsed.type = 'length';
    parsed.value = parseFloat(match[1]);
    parsed.unit = match[2];
    return parsed;
  }
  match = match=value.match(/rgb\(([0-9]+),[ ]?([0-9]+),[ ]?([0-9]+)\)/);
  if (match) {
    parsed.type = 'color';
    parsed.value = {
      red:parseInt(match[1]),
      green:parseInt(match[2]),
      blue:parseInt(match[3]),
      alpha:1
    };
    return parsed;
  }
  match=value.match(/rgba\(([0-9]+),[ ]?([0-9]+),[ ]?([0-9]+),[ ]?([\.0-9]+)\)/);
  if (match) {
    parsed.type = 'color';
    parsed.value = {
      red:parseInt(match[1]),
      green:parseInt(match[2]),
      blue:parseInt(match[3]),
      alpha:parseFloat(match[4]),
    };
    return parsed;
  }
  var color = new hui.Color(value);
  if (color.ok) {
    parsed.type = 'color';
    parsed.value = {
      red:color.r,
      green:color.g,
      blue:color.b,
      alpha:1
    };
  }
  return parsed;
};

///////////////////////////// Item ///////////////////////////////

/**
 * An animation item describing what to animate on an element
 * @constructor
 */
hui.animation.Item = function(element) {
  this.element = element;
  this.work = [];
};

hui.animation.Item.prototype.animate = function(from,to,property,duration,delegate) {
  var work = this.getWork(hui.string.camelize(property));
  work.delegate = delegate;
  work.finished = false;
  var css = !(property=='scrollLeft' || property=='scrollTop' || property==='');
  if (from!==null) {
    work.from = from;
  } else if (property=='transform') {
    work.transform = hui.animation.Item.parseTransform(to,this.element);
  } else if (!hui.browser.opacity && property=='opacity') {
    work.from = this._getIEOpacity(this.element);
  } else if (css) {
    var style = hui.style.get(this.element,property);
    var parsedStyle = hui.animation._parseStyle(style);
    work.from = parsedStyle.value;
  } else {
    work.from = this.element[property];
  }
  if (css) {
    var parsed = hui.animation._parseStyle(to);
    work.to = parsed.value;
    work.unit = parsed.unit;
    if (!hui.browser.opacity && property=='opacity') {
      work.updater = hui.animation._ieOpacityUpdater;
    } else if (property=='transform') {
      work.updater = hui.browser.msie ? function() {} : hui.animation._transformUpater;
    } else if (parsed.type=='color') {
      work.updater = hui.animation._colorUpater;
    } else {
      work.updater = hui.animation._lengthUpater;
    }
  } else {
    work.to = to;
    work.unit = null;
    work.updater = hui.animation._propertyUpater;
  }
  work.start = Date.now();
  if (delegate && delegate.delay) {
    work.start+=delegate.delay;
  }
  work.end = work.start+duration;
  hui.animation.start();
};

hui.animation.TRANSFORM = (function() {
  var agent = navigator.userAgent;
  var gecko = agent.indexOf('Gecko') !== -1 && agent.indexOf('WebKit') === -1;
  return gecko ? 'MozTransform' : 'WebkitTransform';
})();

hui.animation.Item.parseTransform = function(value,element) {
  var result = {};
  var from,fromMatch;
  var rotateReg = /rotate\(([0-9\.]+)([a-z]+)\)/i;
  var rotate = value.match(rotateReg);
  if (rotate) {
    from = 0;
    if (element.style[hui.animation.TRANSFORM]) {
      fromMatch = element.style[hui.animation.TRANSFORM].match(rotateReg);
      if (fromMatch) {
        from = parseFloat(fromMatch[1]);
      }
    }
    result.rotate = {from:from,to:parseFloat(rotate[1]),unit:rotate[2]};
  }
  var scaleReg = /scale\(([0-9\.]+)\)/i;
  var scale = value.match(scaleReg);
  if (scale) {
    from = 1;
    if (element.style[hui.animation.TRANSFORM]) {
      fromMatch = element.style[hui.animation.TRANSFORM].match(scaleReg);
      if (fromMatch) {
        from = parseFloat(fromMatch[1]);
      }
    }
    result.scale = {from:from,to:parseFloat(scale[1])};
  }
  return result;
};

hui.animation.Item.prototype._getIEOpacity = function(element) {
  var filter = hui.style.get(element,'filter').toLowerCase();
  var match = filter.match(/opacity=([0-9]+)/);
  if (match) {
    return parseFloat(match[1])/100;
  } else {
    return 1;
  }
};

hui.animation.Item.prototype.getWork = function(property) {
  for (var i = this.work.length - 1; i >= 0; i--) {
    if (this.work[i].property===property) {
      return this.work[i];
    }
  }
  var work = {property:property};
  this.work[this.work.length] = work;
  return work;
};

/////////////////////////////// Loop ///////////////////////////////////

/** @constructor */
hui.animation.Loop = function(recipe) {
  this.recipe = recipe;
  this.position = -1;
  this.running = false;
};

hui.animation.Loop.prototype.next = function() {
  this.position++;
  if (this.position>=this.recipe.length) {
    this.position = 0;
  }
  var item = this.recipe[this.position];
  if (typeof(item)=='function') {
    item();
  } else if (item.element) {
    hui.animate(item.element,item.property,item.value,item.duration,{ease:item.ease});
  }
  var self = this;
  var time = item.duration || 0;
  if (item.wait!==undefined) {
    time = item.wait;
  }
  window.setTimeout(function() {self.next();},time);
};

hui.animation.Loop.prototype.start = function() {
  this.running=true;
  this.next();
};

/** @namespace */
hui.ease = {
  slowFastSlow : function(val) {
    var a = 1.6;
    var b = 1.4;
    return -1*Math.pow(Math.cos((Math.PI/2)*Math.pow(val,a)),Math.pow(Math.PI,b))+1;
  },
  fastSlow : function(val) {
    var a = 0.5;
    var b = 0.7;
    return -1*Math.pow(Math.cos((Math.PI/2)*Math.pow(val,a)),Math.pow(Math.PI,b))+1;
  },
  elastic : function(t) {
    return 1 - hui.ease.elastic2(1-t);
  },

  elastic2 : function (t, a, p) {
    if (t<=0 || t>=1) return t;
    if (!p) p=0.45;
    var s;
    if (!a || a < 1) {
      a=1;
      s=p/4;
    } else {
      s = p/(2*Math.PI) * Math.asin (1/a);
    }
    return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t-s)*(2*Math.PI)/p ));
  },
  bounce : function(t) {
    if (t < (1/2.75)) {
      return 7.5625*t*t;
    } else if (t < (2/2.75)) {
      return (7.5625*(t-=(1.5/2.75))*t + 0.75);
    } else if (t < (2.5/2.75)) {
      return (7.5625*(t-=(2.25/2.75))*t + 0.9375);
    } else {
      return (7.5625*(t-=(2.625/2.75))*t + 0.984375);
    }
  },
  flicker : function(value) {
    if (value==1) return 1;
    return Math.random()*value;
  },

  quadIn: function(/* Decimal? */n){
    return Math.pow(n, 2);
  },

  quadOut: function(/* Decimal? */n){
    return n * (n-2) * -1;
  },

  quadInOut: function(/* Decimal? */n){
    n=n*2;
    if(n<1){ return Math.pow(n, 2) / 2; }
    return -1 * ((--n)*(n-2) - 1) / 2;
  },

  cubicIn: function(/* Decimal? */n){
    return Math.pow(n, 3);
  },

  cubicOut: function(/* Decimal? */n){
    return Math.pow(n-1, 3) + 1;
  },

  cubicInOut: function(/* Decimal? */n){
    n=n*2;
    if(n<1){ return Math.pow(n, 3) / 2; }
    n-=2;
    return (Math.pow(n, 3) + 2) / 2;
  },

  quartIn: function(/* Decimal? */n){
    return Math.pow(n, 4);
  },

  quartOut: function(/* Decimal? */n){
    return -1 * (Math.pow(n-1, 4) - 1);
  },

  quartInOut: function(/* Decimal? */n){
    n=n*2;
    if(n<1){ return Math.pow(n, 4) / 2; }
    n-=2;
    return -1/2 * (Math.pow(n, 4) - 2);
  },

  quintIn: function(/* Decimal? */n){
    return Math.pow(n, 5);
  },

  quintOut: function(/* Decimal? */n){
    return Math.pow(n-1, 5) + 1;
  },

  quintInOut: function(/* Decimal? */n){
    n=n*2;
    if(n<1){ return Math.pow(n, 5) / 2; }
    n-=2;
    return (Math.pow(n, 5) + 2) / 2;
  },

  sineIn: function(/* Decimal? */n){
    return -1 * Math.cos(n * (Math.PI/2)) + 1;
  },

  sineOut: function(/* Decimal? */n){
    return Math.sin(n * (Math.PI/2));
  },

  sineInOut: function(/* Decimal? */n){
    return -1 * (Math.cos(Math.PI*n) - 1) / 2;
  },

  expoIn: function(/* Decimal? */n){
    return (n===0) ? 0 : Math.pow(2, 10 * (n - 1));
  },

  expoOut: function(/* Decimal? */n){
    return (n==1) ? 1 : (-1 * Math.pow(2, -10 * n) + 1);
  },

  expoInOut: function(/* Decimal? */n){
    if (n===0) { return 0; }
    if (n===1) { return 1; }
    n = n*2;
    if (n<1) { return Math.pow(2, 10 * (n-1)) / 2; }
    --n;
    return (-1 * Math.pow(2, -10 * n) + 2) / 2;
  },

  circIn: function(/* Decimal? */n){
    return -1 * (Math.sqrt(1 - Math.pow(n, 2)) - 1);
  },

  circOut: function(/* Decimal? */n){
    n = n-1;
    return Math.sqrt(1 - Math.pow(n, 2));
  },

  circInOut: function(/* Decimal? */n){
    n = n*2;
    if(n<1){ return -1/2 * (Math.sqrt(1 - Math.pow(n, 2)) - 1); }
    n-=2;
    return 1/2 * (Math.sqrt(1 - Math.pow(n, 2)) + 1);
  },

  backIn: function(/* Decimal? */n){
    var s = 1.70158;
    return Math.pow(n, 2) * ((s+1)*n - s);
  },

  backOut: function(/* Decimal? */n){
    // summary: an easing function that pops past the range briefly, and
    //  slowly comes back.
    n = n - 1;
    var s = 1.70158;
    return Math.pow(n, 2) * ((s + 1) * n + s) + 1;
  },

  backInOut: function(/* Decimal? */n){
    var s = 1.70158 * 1.525;
    n = n*2;
    if(n < 1){ return (Math.pow(n, 2)*((s+1)*n - s))/2; }
    n-=2;
    return (Math.pow(n, 2)*((s+1)*n + s) + 2)/2;
  },

  elasticIn: function(/* Decimal? */n){
    if(n===0){ return 0; }
    if(n==1){ return 1; }
    var p = 0.3;
    var s = p/4;
    n = n - 1;
    return -1 * Math.pow(2,10*n) * Math.sin((n-s)*(2*Math.PI)/p);
  },

  elasticOut: function(/* Decimal? */n){
    // summary: An easing function that elasticly snaps around the target value, near the end of the Animation
    if(n===0) return 0;
    if(n==1) return 1;
    var p = 0.3;
    var s = p/4;
    return Math.pow(2,-10*n) * Math.sin((n-s)*(2*Math.PI)/p) + 1;
  },

  elasticInOut: function(/* Decimal? */n){
    // summary: An easing function that elasticly snaps around the value, near the beginning and end of the Animation
    if(n===0) return 0;
    n = n*2;
    if(n==2) return 1;
    var p = 0.3*1.5;
    var s = p/4;
    if(n<1){
      n-=1;
      return -0.5*(Math.pow(2,10*n) * Math.sin((n-s)*(2*Math.PI)/p));
    }
    n-=1;
    return 0.5 * (Math.pow(2, -10 * n) * Math.sin((n - s) * ( 2 * Math.PI) / p)) + 1;
  },

  bounceIn: function(/* Decimal? */n){
    // summary: An easing function that "bounces" near the beginning of an Animation
    return (1 - hui.ease.bounceOut(1-n)); // Decimal
  },

  bounceOut: function(/* Decimal? */n){
    // summary: An easing function that "bounces" near the end of an Animation
    var s=7.5625;
    var p=2.75;
    var l;
    if(n < (1 / p)){
      l = s*Math.pow(n, 2);
    }else if(n < (2 / p)){
      n -= (1.5 / p);
      l = s * Math.pow(n, 2) + 0.75;
    }else if(n < (2.5 / p)){
      n -= (2.25 / p);
      l = s * Math.pow(n, 2) + 0.9375;
    }else{
      n -= (2.625 / p);
      l = s * Math.pow(n, 2) + 0.984375;
    }
    return l;
  },

  bounceInOut: function(/* Decimal? */n){
    if(n<0.5){ return hui.ease.bounceIn(n*2) / 2; }
    return (hui.ease.bounceOut(n*2-1) / 2) + 0.5; // Decimal
  }
};

if (!Date.now) {
  Date.now = function now() {
    return new Date().getTime();
  };
}

hui.on(function() {
  hui.define('hui.animation',hui.animation);
});