function Animator()
{
   var _this = this;
// pointer to the 'animate' function to be used in 'window.setInterval'
// tnx to Bernard Sumption's Animator class for this technique <http://berniecode.com/writing/animator.html>
   this.doAnimation = function() { _this.animate() };
   this.sprites     = [];
   this.timer       = null;
   this.running     = false;
   this.clipRe      = /^clip/i;
   this.colorRe     = /color$/i;
   this.transRe     = /^rotate|^scale|^skew|^translate/i;
   this.scrollRe    = /^scroll/i;
   this.dupletsRe   = /\#([0-9a-f]{2})+([0-9a-f]{2})+([0-9a-f]{2})/i;
// same as Webkit since 'boxshadow' is in Chrome's style object
   this.shadowRe    = /rgba\((\d+)[,][ ](\d+)[,][ ](\d+)[,][ ]((\d+[.]\d+)|0)\)[ ]([-]?\d+[a-z]+)[ ]([-]?\d+[a-z]+)[ ]([-]?\d+[a-z]+)/i;
// see <http://www.thespanner.co.uk/2009/01/29/detecting-browsers-javascript-hacks/> for regex detection strings
   if (/a/[-1] == 'a')                 // Firefox - MoxBoxShadow returns '0pt 5px 15px 5px rgba(0, 0, 0, 0.49)'
      this.shadowRe = /([-]?\d+[a-z]+)[ ]([-]?\d+[a-z]+)[ ]([-]?\d+[a-z]+)[ ]([-]?\d+[a-z]+)[ ](rgba\((\d+)[,][ ](\d+)[,][ ](\d+)[,][ ](\d+[.]\d+)\)|transparent)/i;
   else if (/a/.__proto__ == '//')     // Safari - WebkitBoxShadow returns 'rgba(0, 0, 0, 0.49) 0pt 5px 15px'
      this.shadowRe = /rgba\((\d+)[,][ ](\d+)[,][ ](\d+)[,][ ]((\d+[.]\d+)|0)\)[ ]([-]?\d+[a-z]+)[ ]([-]?\d+[a-z]+)[ ]([-]?\d+[a-z]+)/i;
}

Animator.prototype =
{
   init: function(duration, callBack, fps)
   {
      this.duration = duration;
      this.callBack = callBack || null;
      this.fps      = fps || 30;
   },

   add: function(node, start, end, fn, attr, dir, debug)
   {
   // apparently we want to debug...
      if (typeof dir == 'boolean')
         debug = dir;

      var trueStart = dir == 'rev' ? end : start;
      var trueEnd   = dir == 'rev' ? start : end;

      var distance;
      if (this.colorRe.test(attr))
      {
         var sDuplets = this.dupletsRe.exec(trueStart);
         var eDuplets = this.dupletsRe.exec(trueEnd);
         trueStart    = [this.h2d(sDuplets[1]), this.h2d(sDuplets[2]), this.h2d(sDuplets[3])];
         distance     = [this.h2d(eDuplets[1]) - this.h2d(sDuplets[1]), this.h2d(eDuplets[2]) - this.h2d(sDuplets[2]), this.h2d(eDuplets[3]) - this.h2d(sDuplets[3])];
      }
      else if (this.clipRe.test(attr))
         distance = [trueEnd[0] - trueStart[0], trueEnd[1] - trueStart[1], trueEnd[2] - trueStart[2], trueEnd[3] - trueStart[3]];
      else
         distance = trueEnd - trueStart;

      this.sprites.push({ 'self': node, 'start': trueStart, 'method': fn, 'distance': distance, 'attr': attr, 'dir': dir, 'isDebugSet': debug, 'cache': {} });
   },

   start: function()
   {
      if (this.running)
         return;

      this.interval    = Math.floor(1000 / this.fps);
      this.totalFrames = Math.floor(this.duration / this.interval);
      this.startTime   = new Date().getTime();
   // notice how 'this' is still valid within this context when using 'window.setInterval' rather than 'setInterval'
   // tnx to Bernard Sumption's Animator class for this insight <http://berniecode.com/writing/animator.html>
      this.timer       = window.setInterval(this.doAnimation, this.interval);
      this.running     = true;
   },

   animate: function()
   {
      var time = new Date().getTime();
      if (time < this.startTime + this.duration)
      {
         this.frame = Math.floor((time - this.startTime) / this.interval);
         for (i = 0; i < this.sprites.length; i++)
         {
            this.sprite = this.sprites[i];
            this.calculate();
         }
      }
      else
         this.stop(true);
   },

   stop: function(runCallBack)
   {
      if (!this.running)
         return;

      window.clearInterval(this.timer);
      this.timer   = null;
      this.running = false;
      this.sprites = [];

      if (runCallBack && this.callBack && typeof this.callBack == 'function')
         this.callBack();
   },

   calculate: function()
   {
      if (this.colorRe.test(this.sprite.attr))
      {
         var newR = parseInt(Animator.methods[this.sprite.method](this.frame, this.sprite.start[0], this.sprite.distance[0], this.totalFrames));
         var newG = parseInt(Animator.methods[this.sprite.method](this.frame, this.sprite.start[1], this.sprite.distance[1], this.totalFrames));
         var newB = parseInt(Animator.methods[this.sprite.method](this.frame, this.sprite.start[2], this.sprite.distance[2], this.totalFrames));

      // no need to paint the same frame twice
         if (this.sprite.cache.newR && newR == this.sprite.cache.newR
         &&  this.sprite.cache.newG && newG == this.sprite.cache.newG
         &&  this.sprite.cache.newB && newB == this.sprite.cache.newB)
         {
            if (this.sprite.isDebugSet)
               this.debug('  frame#' + this.frame + ' calculated value in cache: skipped');
            return;
         }

         this.sprite.cache.newR = newR;
         this.sprite.cache.newG = newG;
         this.sprite.cache.newB = newB;

         this.sprite.self.style[this.sprite.attr] = '#' + this.d2h(newR) + this.d2h(newG) + this.d2h(newB);
      }
      else if (this.clipRe.test(this.sprite.attr))
      {
         var clipTop    = parseInt(Animator.methods[this.sprite.method](this.frame, this.sprite.start[0], this.sprite.distance[0], this.totalFrames));
         var clipRight  = parseInt(Animator.methods[this.sprite.method](this.frame, this.sprite.start[1], this.sprite.distance[1], this.totalFrames));
         var clipBottom = parseInt(Animator.methods[this.sprite.method](this.frame, this.sprite.start[2], this.sprite.distance[2], this.totalFrames));
         var clipLeft   = parseInt(Animator.methods[this.sprite.method](this.frame, this.sprite.start[3], this.sprite.distance[3], this.totalFrames));

         if ((this.sprite.cache.clipTop    && clipTop    == this.sprite.cache.clipTop)
         &&  (this.sprite.cache.clipRight  && clipRight  == this.sprite.cache.clipRight)
         &&  (this.sprite.cache.clipBottom && clipBottom == this.sprite.cache.clipBottom)
         &&  (this.sprite.cache.clipLeft   && clipLeft   == this.sprite.cache.clipLeft))
         {
            if (this.sprite.isDebugSet)
               this.debug('  frame#' + this.frame + ' calculated value in cache: skipped');
            return;
         }
         this.sprite.cache.clipTop    = clipTop;
         this.sprite.cache.clipRight  = clipRight;
         this.sprite.cache.clipBottom = clipBottom;
         this.sprite.cache.clipLeft   = clipLeft;

         this.sprite.self.style.clip = 'rect(' + clipTop + 'px ' + clipRight + 'px ' + clipBottom + 'px ' + clipLeft + 'px)';
      }
      else
      {
         var newValue = parseInt(Animator.methods[this.sprite.method](this.frame, this.sprite.start, this.sprite.distance, this.totalFrames));

         if (this.sprite.cache.newValue && this.sprite.cache.newValue == newValue)
         {
            if (this.sprite.isDebugSet)
               this.debug('  frame#' + this.frame + ' calculated value in cache: skipped');
            return;
         }
         this.sprite.cache.newValue = newValue;

         if (this.transRe.test(this.sprite.attr))
         {
         // under construction
            if (this.sprite.attr.match('rotate') || this.sprite.attr.match('skew'))
            {
               switch(this.sprite.attr.split(/\//)[0])
               {
                  case 'rotate':    this.sprite.self.style.MozTransform = this.sprite.self.style.WebkitTransform = 'rotate(' + (newValue * this.sprite.attr.split(/\//)[1]) + 'deg)'; break;
                  case 'skew':      this.sprite.self.style.MozTransform = this.sprite.self.style.WebkitTransform = 'skew('   + (newValue * this.sprite.attr.split(/\//)[1]) + 'deg)'; break;
                  case 'skewX':     this.sprite.self.style.MozTransform = this.sprite.self.style.WebkitTransform = 'skewX('  + (newValue * this.sprite.attr.split(/\//)[1]) + 'deg)'; break;
                  case 'skewY':     this.sprite.self.style.MozTransform = this.sprite.self.style.WebkitTransform = 'skewY('  + (newValue * this.sprite.attr.split(/\//)[1]) + 'deg)';
               }
            }
            else
            {
               switch(this.sprite.attr)
               {
                  case 'scale':      this.sprite.self.style.MozTransform = this.sprite.self.style.WebkitTransform = 'scale('      + newValue + ')';   break;
                  case 'scaleX':     this.sprite.self.style.MozTransform = this.sprite.self.style.WebkitTransform = 'scaleX('     + newValue + ')';   break;
                  case 'scaleY':     this.sprite.self.style.MozTransform = this.sprite.self.style.WebkitTransform = 'scaleY('     + newValue + ')';   break;
                  case 'translate':  this.sprite.self.style.MozTransform = this.sprite.self.style.WebkitTransform = 'translate('  + newValue + 'px)'; break;
                  case 'translateX': this.sprite.self.style.MozTransform = this.sprite.self.style.WebkitTransform = 'translateX(' + newValue + 'px)'; break;
                  case 'translateY': this.sprite.self.style.MozTransform = this.sprite.self.style.WebkitTransform = 'translateY(' + newValue + 'px)';
               }
            }
         }
         else if (typeof this.sprite.attr == 'object')  this.sprite.self.src = this.sprite.attr[0] + newValue + '.' + this.sprite.attr[1];
         else if (this.scrollRe.test(this.sprite.attr)) this.sprite.self[this.sprite.attr] = newValue;
         else if (this.sprite.attr == 'opacity')        this.setOpacity(this.sprite.self, newValue);
         else if (this.sprite.attr == 'boxshadow')      this.setBoxshadow(this.sprite.self, newValue);
         else                                           this.sprite.self.style[this.sprite.attr] = newValue + 'px';
      }
      if (this.sprite.isDebugSet)
         this.debug();
   },

   d2h: function(n)
   {
      if (n > 255) n = 255;
      var hexString = n.toString(16);
      if (n < 16) hexString = '0' + hexString;
      return hexString;
   },

   h2d: function(s)
   {
      return parseInt(s, 16);
   },

   setOpacity: function(sprite, newValue)
   {
           if ('opacity' in sprite.style)      sprite.style.opacity      = newValue / 100;
      else if ('MozOpacity' in sprite.style)   sprite.style.MozOpacity   = newValue / 100;
      else if ('KhtmlOpacity' in sprite.style) sprite.style.KhtmlOpacity = newValue / 100;
      else if ('filter' in sprite.style)       sprite.style.filter       = 'alpha(opacity=' + newValue + ')';
   },

   setBoxshadow: function(sprite, newValue)
   {
   // xPloder doesn't support this - see <http://www.javascriptkit.com/javatutors/conditionalcompile.shtml> for cc_* syntax
      if (/*@cc_on!@*/false)
         return;

      var sX, sY, sBlur, sSpread, sR, sG, sB, sA;
      if ('boxShadow' in sprite.style)
      {
      // same as Webkit since 'boxshadow' is in Chrome's style object
         var temp = this.shadowRe.exec(sprite.style.WebkitBoxShadow);
            sR      = temp[1];
            sG      = temp[2];
            sB      = temp[3];
            sA      = newValue / 100;
            sX      = temp[6];
            sY      = temp[7];
            sBlur   = temp[8];
         sprite.style.WebkitBoxShadow = 'rgba(' + sR + ', ' + sG + ', ' + sB + ', ' + sA + ') ' + sX + ' ' + sY + ' ' + sBlur;
      }
      else if ('MozBoxShadow' in sprite.style)
      {
         var temp = this.shadowRe.exec(sprite.style.MozBoxShadow);
            sX      = temp[1];
            sY      = temp[2];
            sBlur   = temp[3];
            sSpread = temp[4];
            sR      = temp[5] == 'transparent' ? 0 : temp[6];
            sG      = temp[5] == 'transparent' ? 0 : temp[7];
            sB      = temp[5] == 'transparent' ? 0 : temp[8];
            sA      = newValue / 100;
         sprite.style.MozBoxShadow = 'rgba(' + sR + ', ' + sG + ', ' + sB + ', ' + sA + ') ' + sX + ' ' + sY + ' ' + sBlur + ' ' + sSpread;
      }
      else if ('WebkitBoxShadow' in sprite.style)
      {
         var temp = this.shadowRe.exec(sprite.style.WebkitBoxShadow);
            sR      = temp[1];
            sG      = temp[2];
            sB      = temp[3];
            sA      = newValue / 100;
            sX      = temp[6];
            sY      = temp[7];
            sBlur   = temp[8];
         sprite.style.WebkitBoxShadow = 'rgba(' + sR + ', ' + sG + ', ' + sB + ', ' + sA + ') ' + sX + ' ' + sY + ' ' + sBlur;
      }
   },

   help: function()
   {
      if (document.getElementById('animatorHelp'))
         return;

      var br = /*@cc_on!@*/false ? '\r' : '\n';    // xPloder needs this - see <http://www.javascriptkit.com/javatutors/conditionalcompile.shtml> for cc_* syntax

      var s = document.createElement('span');
         s.style.cursor          = 'pointer';
         s.style.border          = '2px solid #ababab';
         s.style.cssFloat        = 'right';
         s.style.padding         = '.125em .75em';
         s.style.MozBorderRadius = '100%';
         s.onclick               = this.hideHelp;
         s.appendChild(document.createTextNode('dismiss'));
      var p = document.createElement('pre');
         p.id                    = 'animatorHelp';
         p.style.backgroundColor = '#ffffff';
         p.style.border          = '1px solid #000000';
         p.style.color           = '#000000';
         p.style.fontSize        = '.85em';
         p.style.padding         = '1em 1.5em';
         p.style.position        = 'absolute';
         p.style.zIndex          = 1000;
         p.style.MozBoxShadow    = 'rgba(0, 0, 0, .4) 0 2px 5px 2px';  // color X  Y blur spread
         p.style.WebkitBoxShadow = 'rgba(0, 0, 0, .4) 0 2px 5px';      // webkit does not support 'spread'
         p.appendChild(document.createTextNode(
            'usage:' + br +
            '   var foo = new Animator();' + br +
            '      foo.init(iDuration[, oCallBack[, iFps]]);' + br +
            '      foo.add(oDOMnode, iStart|aStart, iEnd|aEnd, sMethod, sAttribute|aImgSrc[, sDirection[, bDebug]]);' + br +
            '                                                    ...' + br +
            '      foo.add(oDOMnode, iStart|aStart, iEnd|aEnd, sMethod, sAttribute|aImgSrc[, sDirection[, bDebug]]);' + br +
            '      foo.start();' + br + br +

            '      foo.stop(bRunCallBack);' + br + br +

            '      foo.setOpacity(oElement, iOpacity);' + br + br +

            '      foo.help();' + br + br +

            'where' + br +
            '   duration:    time in mSecs' + br +
            '   callback:    function pointer|anonymous function [use "this.sprite.self" to address the animated object]' + br +
            '   fps:         frames/second (defaults to 30)' + br + br +

            '   start, end:  integer|4 co-ordinate clip array e.g. [0, 0, 0, 0]|6 digit hex RGB color value e.g. #ffbb00' + br +
            '   method:      linear|in|out|inout[back|bounce|circ|cubic|elastic|expo|quad|quart|quint|sine]' + br +
            '   attribute:   left|right|top|bottom|width|height|scroll[Left|Top]|clip|backgroundColor|borderColor|color|' + br +
            '                rotate|scale[X|Y]|skew[X|Y]|translate[X|Y] (experimental!)' + br +
            '                      transformations must be followed by slash separated multiplier value e.g. "rotate/20"' + br +
            '   image src:   array containing path/imageName, image extension e.g. ["./pix/foo", "png"]' + br +
            '   direction:   fwd|rev (defaults to "fwd")' + br + br +

            '   debug:       if true outputs calculated values for attr to error console, if any' + br + br +

            '   runCallBack: if true runs the callBack function, if any' + br + br +

            '   element:     HTML element reference' + br +
            '   opacity:     opacity value' + br + br +

            '                                 Easing formulae (c)2003 Robert Penner [http://www.robertpenner.com/easing]' + br + br
         ));
         p.appendChild(s);
         this.setOpacity(p, 0);
      document.body.appendChild(p);
         p.style.left = (((document.documentElement.offsetWidth || document.body.offsetWidth) - p.offsetWidth) / 2) + 'px';
         p.style.top  = ((document.documentElement.scrollTop || document.body.scrollTop) + 10) + 'px';

   // if you have it, flaunt it ;)
      this.init(500);
      this.add(p, 0, 100, 'linear', 'opacity');
      this.start();
   },

   hideHelp: function()
   {
      if (!document.getElementById('animatorHelp'))
         return;

      var fader = new Animator();
         fader.init(1500, function() { document.body.removeChild(document.getElementById('animatorHelp')) });
         fader.add(this.parentNode,
                   [0,                                this.parentNode.offsetWidth,     this.parentNode.offsetHeight,     0],
                   [this.parentNode.offsetHeight / 2, this.parentNode.offsetWidth / 2, this.parentNode.offsetHeight / 2, this.parentNode.offsetWidth / 2],
                   'outquint',
                   'clip');
         fader.start();
   },

   debug: function(msg)
   {
      if (!msg)
      {
         var msg = '@ frame#' + this.frame + ' ' + this.sprite.self.id;
      // under construction
         if (this.transRe.test(this.sprite.attr))
            msg += '.' + this.sprite.attr.split(/\//)[0] + ' = ' + (this.sprite.self.style.MozTransform || this.sprite.self.style.WebkitTransform);
         else if (this.sprite.self.src)
            msg += '.src' + ' = ' + this.sprite.self.src;
         else
            msg += ' ' + this.sprite.attr + ' = ' + (this.sprite.self[this.sprite.attr] || this.sprite.self.style[this.sprite.attr] || this.sprite.self.style['filter']);
      }
      if (window.opera) opera.postError(msg); else if (typeof console != 'undefined' && console) console.log(msg);
   }
};

Animator.methods =
{
/* =========================================================================================== *
 *  Easing Equations v2.0                                                                      *
 *  September 1, 2003                                                                          *
 *  (c)2003 Robert Penner, all rights reserved.                                                *
 *  http://www.robertpenner.com/easing
 *                                                                                             *
 *  This work is subject to the terms in http://www.robertpenner.com/easing_terms_of_use.html
 * =========================================================================================== *
 */
// t = time, s = start, d = distance, l = length, a = amplitude, p = period, o = overshoot
linear:function(t,s,d,l){return d*t/l+s;},
inback:function(t,s,d,l,o){if(typeof(o)=='undefined')o=1.70158;return d*(t/=l)*t*((o+1)*t-o)+s;},
outback:function(t,s,d,l,o){if(typeof(o)=='undefined')o=1.70158;return d*((t=t/l-1)*t*((o+1)*t+o)+1)+s;},
inoutback:function(t,s,d,l,o){if(typeof(o)=='undefined')o=1.70158;if((t/=l/2)<1)return d/2*(t*t*(((o*=(1.525))+1)*t-o))+s;return d/2*((t-=2)*t*(((o*=(1.525))+1)*t+o)+2)+s;},
inbounce:function(t,s,d,l){if((t/=l)<(1/2.75))return d*(7.5625*t*t)+s;else if(t<(2/2.75))return d*(7.5625*(t-=(1.5/2.75))*t+.75)+s;else if(t<(2.5/2.75))return d*(7.5625*(t-=(2.25/2.75))*t+.9375)+s;else return d*(7.5625*(t-=(2.625/2.75))*t+.984375)+s;},
outbounce:function(t,s,d,l){return d-Animator.methods.inbounce(l-t,0,d,l)+s;},
inoutbounce:function(t,s,d,l){if(t<l/2)return Animator.methods.outbounce(t*2,0,d,l)*.5+s;else return Animator.methods.inbounce(t*2-l,0,d,l)*.5+d*.5+s;},
incirc:function(t,s,d,l){return -d*(Math.sqrt(1-(t/=l)*t)-1)+s;},
outcirc:function(t,s,d,l){return d*Math.sqrt(1-(t=t/l-1)*t)+s;},
inoutcirc:function(t,s,d,l){if((t/=l/2)<1)return -d/2*(Math.sqrt(1-t*t)-1)+s;return d/2*(Math.sqrt(1-(t-=2)*t)+1)+s;},
incubic:function(t,s,d,l){return d*(t/=l)*t*t+s;},
outcubic:function(t,s,d,l){return d*((t=t/l-1)*t*t+1)+s;},
inoutcubic:function(t,s,d,l){if((t/=l/2)<1)return d/2*t*t*t+s;return d/2*((t-=2)*t*t+2)+s;},
inelastic:function(t,s,d,l,a,p){if(t==0)return s;if((t/=l)==1)return s+d;if(!p)p=l*.3;if(!a||a<Math.abs(d)){a=d;var x=p/4;}else var x=p/(2*Math.PI)*Math.asin(d/a);return -(a*Math.pow(2,10*(t-=1))*Math.sin((t*l-x)*(2*Math.PI)/p))+s;},
outelastic:function(t,s,d,l,a,p){if(t==0)return s;if((t/=l)==1)return s+d;if(!p)p=l*.3;if(!a||a<Math.abs(d)){a=d;var x=p/4;}else var x=p/(2*Math.PI)*Math.asin(d/a);return (a*Math.pow(2,-10*t)*Math.sin((t*l-x)*(2*Math.PI)/p)+d+s);},
inoutelastic:function(t,s,d,l,a,p){if(t==0)return s;if((t/=l/2)==2)return s+d;if(!p)p=l*(.3*1.5);if(!a||a<Math.abs(d)){a=d;var x=p/4;}else var x=p/(2*Math.PI)*Math.asin(d/a);if(t<1)return -.5*(a*Math.pow(2,10*(t-=1))*Math.sin((t*l-x)*(2*Math.PI)/p))+s;return a*Math.pow(2,-10*(t-=1))*Math.sin((t*l-x)*(2*Math.PI)/p)*.5+d+s;},
inexpo:function(t,s,d,l){return (t==0)?s:d*Math.pow(2,10*(t/l-1))+s;},
outexpo:function(t,s,d,l){return (t==l)?s+d:d*(-Math.pow(2,-10*t/l)+1)+s;},
inoutexpo:function(t,s,d,l){if(t==0)return s;if(t==l)return s+d;if((t/=l/2)<1)return d/2*Math.pow(2,10*(t-1))+s;return d/2*(-Math.pow(2,-10*--t)+2)+s;},
inquad:function(t,s,d,l){return d*(t/=l)*t+s;},
outquad:function(t,s,d,l){return -d*(t/=l)*(t-2)+s;},
inoutquad:function(t,s,d,l){if((t/=l/2)<1)return d/2*t*t+s;return -d/2*((--t)*(t-2)-1)+s;},
inquart:function(t,s,d,l){return d*(t/=l)*t*t*t+s;},
outquart:function(t,s,d,l){return -d*((t=t/l-1)*t*t*t-1)+s;},
inoutquart:function(t,s,d,l){if((t/=l/2)<1)return d/2*t*t*t*t+s;return -d/2*((t-=2)*t*t*t-2)+s;},
inquint:function(t,s,d,l){return d*(t/=l)*t*t*t*t+s;},
outquint:function(t,s,d,l){return d*((t=t/l-1)*t*t*t*t+1)+s;},
inoutquint:function(t,s,d,l){if((t/=l/2)<1)return d/2*t*t*t*t*t+s;return d/2*((t-=2)*t*t*t*t+2)+s;},
insine:function(t,s,d,l){return -d*Math.cos(t/l*(Math.PI/2))+d+s;},
outsine:function(t,s,d,l){return d*Math.sin(t/l*(Math.PI/2))+s;},
inoutsine:function(t,s,d,l){return -d/2*(Math.cos(Math.PI*t/l)-1)+s;}
};
