/* anim8js-dom 1.0.17 - anim8 your HTML elements by Philip Diffenderfer */
// UMD (Universal Module Definition)
(function (root, factory)
{
  if (typeof define === 'function' && define.amd) // jshint ignore:line
  {
    // AMD. Register as an anonymous module.
    define(['anim8js'], function(anim8) { // jshint ignore:line
      return factory(anim8, root);
    });
  }
  else if (typeof module === 'object' && module.exports)  // jshint ignore:line
  {
    // Node. Does not work with strict CommonJS, but
    // only CommonJS-like environments that support module.exports,
    // like Node.
    module.exports = factory(require('anim8js'), global);  // jshint ignore:line
  }
  else
  {
    // Browser globals (root is window)
    factory(root.anim8, root);
  }
}(this, function(anim8, window)
{

  var FastMap = anim8.FastMap;
  var Animator = anim8.Animator;
  var Factory = anim8.Factory;
  var Class = anim8.Class;
  var Color = anim8.Color;
  var Easings = anim8.Easings;
  var EasingTypes = anim8.EasingTypes;
  var isFunction = anim8.isFunction;
  var isString = anim8.isString;
  var isNumber = anim8.isNumber;
  var isDefined = anim8.isDefined;
  var isObject = anim8.isObject;
  var isBoolean = anim8.isBoolean;
  var coalesce = anim8.coalesce;
  var clamp = anim8.clamp;
  var toArray = anim8.toArray;

  var $calculator = anim8.calculator;

  var HTMLElement = (this.HTMLElement || window.HTMLElement);
  var document = (this.document || window.document);

  if (!document)
  {
    throw 'document is not defined on this or window, if you are building for node you cannot use the anim8js-dom package. If you are building with webpack make sure to set output.globalObject to "this".';
  }

  if (!HTMLElement)
  {
    throw 'HTMLElement is not defined on this or window, if you are building for node you cannot use the anim8js-dom package. If you are building with webpack make sure to set output.globalObject to "this".';
  }

  function override(target, source)
  {
    for (var prop in source)
    {
      target[ prop ] = source[ prop ];
    }
  }


/**
 * Returns true if the given variable is an HTML element.
 *
 * @method anim8.isElement
 * @param {Any} x
 * @return {Boolean}
 */
function isElement(x)
{
  return typeof HTMLElement === "object" ? x instanceof HTMLElement :
  x && typeof x === "object" && x !== null && x.nodeType === 1 && typeof x.nodeName === "string";
}


function unset( e, anim, attr, property, css, clearedValue )
{
  if ( attr === true )
  {
    e.style[ css ] = clearedValue;
  }
  else
  {
    delete anim.frame[ attr ];

    property.set( e, anim );

    e.style[ css ] = anim.styles[ css ];
  }
}


/**
 * Computes the desired style of the given element and returns it as a string.
 * The style given must be in hyphenated format like so:
 * anim8.dom.style( element, 'font-size' ) = '12px'
 *
 * @param {HTMLElement} e
 * @param {String} style
 * @return {String}
 */
var $style = (function()
{
  var hyphenated = {};

  var hyphenize = function(str)
  {
    if ( str in hyphenated )
    {
      return hyphenated[ str ];
    }

    var key = str;

    str = str.replace(/[a-z][A-Z]/g, function(str, letter)
    {
     return str[0] + '-' + str[1].toLowerCase();
    });

    str = str.replace(/^Webkit/, '-webkit');
    str = str.replace(/^Moz/, '-moz');
    str = str.replace(/^Ms/, '-ms');
    str = str.replace(/^O/, '-o');
    str = str.replace(/^Khtml/, '-khtml');

    hyphenated[ key ] = str;

    return str;
  };

  return function(e, style)
  {
    if (e.currentStyle)
    {
      return e.currentStyle[ style ];
    }
    else if (document.defaultView && document.defaultView.getComputedStyle)
    {
      return document.defaultView.getComputedStyle( e, null ).getPropertyValue( hyphenize( style ) );
    }
    else
    {
      return e.style[ style ];
    }
  };

})();

/**
 * Given an array of styles this will return the first one that is present on elements in the current browser.
 *
 * @param {Array} prefixes
 * @return {String|false}
 */
var $prefix = (function()
{
  var a = document.createElement('a');

  return function(prefixes)
  {
    for (var i = 0; i < prefixes.length; i++)
    {
      if ( isDefined( a.style[ prefixes[ i ] ] ) )
      {
        return prefixes[i];
      }
    }
    return false;
  };

})();

/**
 * Parses the string for a value and a unit.
 *
 * @param {String} value
 * @return {Object|false}
 */
var $parseValue = (function()
{
  var regex = /(-?\d*(\.\d+)|-?\d+)(px|em|%|vw|ex|cm|mm|in|pt|pc|deg|rad)?/;

  return function(x)
  {
    var parsed = regex.exec( x );

    if (parsed)
    {
      return {
        value: parseFloat( parsed[1] ),
        unit: parsed[3]
      };
    }

    return false;
  };
})();


/**
 * Converts one unit to another for a given element.
 *
 * For Example: anim8.dom.convert( element, '100px', '%', 'parentWidth' )
 *    returns how much percent 100px relativeTo parentWidth of the given element
 *
 * @param {HTMLElement} e
 * @param {String} from
 * @param {String} toUnit
 * @param {String|Number} relativeTo
 * @return {Number|false}
 */
var $convert = (function()
{
  /**
   * Calculators how many pixels a given value & unit is.
   *
   * For Example: anim8.toPixels( 100, 'in' )
   *    returns how many pixels are in 1 inch, with up to 2 decimal points of accuracy.
   */
  var toPixels = function(baseValue, baseUnit, defaultRate)
  {
    if ( document.body )
    {
      try
      {
        var div = document.createElement('div');
        document.body.appendChild( div );
        div.style.width = baseValue + baseUnit;
        var pixels = (div.offsetWidth / baseValue);
        document.body.removeChild( div );

        return pixels || defaultRate;
      }
      catch (e)
      {
        // Do nothing
      }
    }

    return defaultRate;
  };

  var getFontSize = function(e, notUnit, relativeTo)
  {
    var fontSize = $style( e, 'fontSize' );
    var parsed = $parseValue( fontSize );

    if ( !parsed || parsed.unit === notUnit )
    {
      return 12;
    }

    if ( parsed.unit === 'px' )
    {
      return parsed.value;
    }

    return getConverterScale( e, conversions[ parsed.unit ].px, relativeTo );
  };

  var variables = {};

  variables.parentWidth = function(e)
  {
    return e.parentNode.scrollWidth;
  };
  variables.parentHeight = function(e)
  {
    return e.parentNode.scrollHeight;
  };
  variables.width = function(e)
  {
    return e.offsetWidth;
  };
  variables.height = function(e)
  {
    return e.offsetHeight;
  };
  variables.fontSize = function(e)
  {
    return getFontSize( e, '%' );
  };
  variables.parentFontSize = function(e)
  {
    return getFontSize( e.parentNode, '%' );
  };
  variables.htmlFontSize = function(e)
  {
    var htmlElement = document.getElementsByTagName("html")[0];

    return getFontSize( htmlElement, '%' );
  };

  var conversions = {};

  conversions['pt']  = { px: toPixels( 100, 'pt', 1 ) };
  conversions['in']  = { px: toPixels( 100, 'in', 72 ) };
  conversions['cm']  = { px: toPixels( 1000, 'cm', 72 / 2.54 ) };
  conversions['mm']  = { px: toPixels( 100000, 'mm', 72 / 25.4 ) };
  conversions['vw']  = { px: toPixels( 1000, 'vw', 1024 * 0.01 ) };
  conversions['deg'] = { rad: Math.PI / 180.0 };

  conversions['em'] =
  {
    px: function(e, relativeTo)
    {
      return getFontSize( e, 'em', relativeTo );
    }
  };

  conversions['rem'] =
  {
    px: function(e, relativeTo)
    {
      var htmlElement = document.getElementsByTagName("html")[0];

      return getFontSize( htmlElement, 'rem', relativeTo );
    }
  };

  conversions['%'] =
  {
    px: function(e, relativeTo)
    {
      if ( isNumber( relativeTo ) )
      {
        return relativeTo;
      }
      if ( relativeTo in variables )
      {
        return variables[ relativeTo ]( e ) * 0.01;
      }

      return 1.0;
    }
  };

  // Populate conversions going other way.
  for (var unit in conversions)
  {
    for (var to in conversions[ unit ])
    {
      if ( !(to in conversions) )
      {
        conversions[ to ] = {};
      }

      if ( !(unit in conversions[ to ]) )
      {
        var given = conversions[ unit ][ to ];

        if ( isNumber( given ) )
        {
          conversions[ to ][ unit ] = 1.0 / given;
        }
        if ( isFunction( given ) )
        {
          conversions[ to ][ unit ] = (function(converter)
          {
            return function(e, relativeTo)
            {
              return 1.0 / converter( e, relativeTo );
            };
          })( given );
        }
      }
    }
  }

  // Given an element, convert, and relativeTo - return the number we need to multiply by.
  var getConverterScale = function(e, converter, relativeTo)
  {
    if ( isNumber( converter ) )
    {
      return converter;
    }
    else if ( isFunction( converter ) )
    {
      return converter( e, relativeTo );
    }

    return 1.0;
  };

  return function(e, from, toUnit, relativeTo)
  {
    if ( isNumber( from ) )
    {
      return from;
    }

    var parsed = $parseValue( from );

    if ( !parsed )
    {
      return false;
    }

    var value = parsed.value;
    var fromUnit = parsed.unit;

    if ( !fromUnit || fromUnit === toUnit )
    {
      return value;
    }

    // First see if we have a direct conversion available...
    if ( fromUnit in conversions && toUnit in conversions[ fromUnit ] )
    {
      var converter = conversions[ fromUnit ][ toUnit ];

      value *= getConverterScale( e, converter, relativeTo );
    }
    // Otherwise convert it to px, then to the desired unit
    else if ( fromUnit in conversions && conversions[ fromUnit ].px && toUnit in conversions.px )
    {
      var converter1 = conversions[ fromUnit ].px;
      var converter2 = conversions.px[ toUnit ];

      var combined = getConverterScale( e, converter1, relativeTo ) *
                     getConverterScale( e, converter2, relativeTo );

      value *= combined;
    }

    return value;
  };

})();


/**
 * Easings equivalent to the CSS animations. These are approximations since the
 * exact functions are not performant enough.
 */

Easings['cssEase']      = Easings.ease;

Easings['cssEaseIn']    = Easings.quad;

Easings['cssEaseOut']   = EasingTypes.out( Easings.quad );

Easings['cssEaseInOut'] = EasingTypes.inout( Easings.quad );

Easings['cssLinear']    = Easings.linear;


var Attributes = {};

/**
 * The default attribute.
 */
Attributes['default']              = {defaultValue: 0};

/**
 * All animatable attributes for AnimatorDoms & HTMLElements.
 */

Attributes.padding                 = {defaultValue: 0, defaultUnit: 'px'};
Attributes.paddingTop              = {defaultValue: 0, defaultUnit: 'px'};
Attributes.paddingRight            = {defaultValue: 0, defaultUnit: 'px'};
Attributes.paddingBottom           = {defaultValue: 0, defaultUnit: 'px'};
Attributes.paddingLeft             = {defaultValue: 0, defaultUnit: 'px'};

Attributes.margin                  = {defaultValue: 0, defaultUnit: 'px'};
Attributes.marginTop               = {defaultValue: 0, defaultUnit: 'px'};
Attributes.marginRight             = {defaultValue: 0, defaultUnit: 'px'};
Attributes.marginBottom            = {defaultValue: 0, defaultUnit: 'px'};
Attributes.marginLeft              = {defaultValue: 0, defaultUnit: 'px'};

Attributes.borderRadius            = {defaultValue: 0, defaultUnit: 'px'};
Attributes.borderTopLeftRadius     = {defaultValue: 0, defaultUnit: 'px'};
Attributes.borderTopRightRadius    = {defaultValue: 0, defaultUnit: 'px'};
Attributes.borderBottomLeftRadius  = {defaultValue: 0, defaultUnit: 'px'};
Attributes.borderBottomRightRadius = {defaultValue: 0, defaultUnit: 'px'};

Attributes.borderWidth             = {defaultValue: 0, defaultUnit: 'px'};
Attributes.borderTopWidth          = {defaultValue: 0, defaultUnit: 'px'};
Attributes.borderRightWidth        = {defaultValue: 0, defaultUnit: 'px'};
Attributes.borderBottomWidth       = {defaultValue: 0, defaultUnit: 'px'};
Attributes.borderLeftWidth         = {defaultValue: 0, defaultUnit: 'px'};

Attributes.outlineWidth            = {defaultValue: 0, defaultUnit: 'px'};
Attributes.outlineOffset           = {defaultValue: 0};
Attributes.textIndent              = {defaultValue: 0, defaultUnit: 'px'};
Attributes.tabSize                 = {defaultValue: 0, defaultUnit: 'px'};
Attributes.borderSpacing           = {defaultValue: 0, defaultUnit: 'px'};
Attributes.fontSize                = {defaultValue: 1, defaultUnit: 'em'};
Attributes.lineHeight              = {defaultValue: 1, defaultUnit: 'em'};
Attributes.letterSpacing           = {defaultValue: 0, defaultUnit: 'px'};
Attributes.wordSpacing             = {defaultValue: 0, defaultUnit: 'px'};

Attributes.origin                  = {defaultValue: {x:50, y:50}, defaultUnit: '%', property: 'transformOrigin', calculator: '2d'};
Attributes.originX                 = {defaultValue: 50, defaultUnit: '%', property: 'transformOrigin'};
Attributes.originY                 = {defaultValue: 50, defaultUnit: '%', property: 'transformOrigin'};

Attributes.opacity                 = {defaultValue: 1};
Attributes.zIndex                  = {defaultValue: 1};

Attributes.width                   = {defaultValue: 0, defaultUnit: 'px'};
Attributes.minWidth                = {defaultValue: 0, defaultUnit: 'px'};
Attributes.maxWidth                = {defaultValue: 0, defaultUnit: 'px'};

Attributes.height                  = {defaultValue: 0, defaultUnit: 'px'};
Attributes.minHeight               = {defaultValue: 0, defaultUnit: 'px'};
Attributes.maxHeight               = {defaultValue: 0, defaultUnit: 'px'};

Attributes.angle                   = {defaultValue: 0, property: 'orbit', defaultUnit: 'deg'};
Attributes.distance                = {defaultValue: 0, property: 'orbit', defaultUnit: 'px'};
Attributes.orbitOffset             = {defaultValue: {x:50, y:50}, defaultUnit: '%', property: 'orbit', calculator: '2d'};

Attributes.top                     = {defaultValue: 0, defaultUnit: 'px'};
Attributes.right                   = {defaultValue: 0, defaultUnit: 'px'};
Attributes.bottom                  = {defaultValue: 0, defaultUnit: 'px'};
Attributes.left                    = {defaultValue: 0, defaultUnit: 'px'};

Attributes.center                  = {defaultValue: {x:0, y:0}, defaultUnit: 'px', property: 'center', calculator: '2d'};
Attributes.centerX                 = {defaultValue: 0, defaultUnit: 'px', property: 'center'};
Attributes.centerY                 = {defaultValue: 0, defaultUnit: 'px', property: 'center'};

Attributes.blur                    = {defaultValue: 0, property: 'filter', defaultUnit: 'px'};
Attributes.sepia                   = {defaultValue: 0, property: 'filter', defaultUnit: '%'};
Attributes.brightness              = {defaultValue: 100, property: 'filter', defaultUnit: '%'};
Attributes.grayscale               = {defaultValue: 0, property: 'filter', defaultUnit: '%'};
Attributes.contrast                = {defaultValue: 100, property: 'filter', defaultUnit: '%'};
Attributes.invert                  = {defaultValue: 0, property: 'filter', defaultUnit: '%'};
Attributes.saturation              = {defaultValue: 0, property: 'filter', defaultUnit: '%'};
Attributes.hueRotate               = {defaultValue: 0, property: 'filter', defaultUnit: 'deg'};

Attributes.rotate                  = {defaultValue: 0, property: 'transform', defaultUnit: 'deg'};
Attributes.rotate3d                = {defaultValue: {x:0, y:0, z:1, angle:0}, property: 'transform', calculator: 'quaternion', defaultUnit: 'deg'};

Attributes.translate               = {defaultValue: {x:0, y:0}, property: 'transform', calculator: '2d', defaultUnit: 'px'};
Attributes.translateX              = {defaultValue: 0, property: 'transform', defaultUnit: 'px'};
Attributes.translateY              = {defaultValue: 0, property: 'transform', defaultUnit: 'px'};
Attributes.translateZ              = {defaultValue: 0, property: 'transform', defaultUnit: 'px'};
Attributes.translate3d             = {defaultValue: {x:0, y:0, z:0}, property: 'transform', calculator: '3d', defaultUnit: 'px'};

Attributes.scale                   = {defaultValue: {x:1, y:1}, property: 'transform', calculator: '2d'};
Attributes.scaleX                  = {defaultValue: 1, property: 'transform'};
Attributes.scaleY                  = {defaultValue: 1, property: 'transform'};
Attributes.scaleZ                  = {defaultValue: 1, property: 'transform'};
Attributes.scale3d                 = {defaultValue: {x:1, y:1, z:1}, property: 'transform', calculator: '3d'};

Attributes.skew                    = {defaultValue: {x:0, y:0}, defaultUnit: 'deg', property: 'transform', calculator: '2d'};
Attributes.skewX                   = {defaultValue: 0, defaultUnit: 'deg', property: 'transform'};
Attributes.skewY                   = {defaultValue: 0, defaultUnit: 'deg', property: 'transform'};

Attributes.backface                = {defaultValue: 0};
Attributes.visibility              = {defaultValue: 1};

Attributes.backgroundColor         = {defaultValue: Color(), calculator: 'rgba'};
Attributes.color                   = {defaultValue: Color(), calculator: 'rgba'};
Attributes.borderTopColor          = {defaultValue: Color(), calculator: 'rgba'};
Attributes.borderRightColor        = {defaultValue: Color(), calculator: 'rgba'};
Attributes.borderBottomColor       = {defaultValue: Color(), calculator: 'rgba'};
Attributes.borderLeftColor         = {defaultValue: Color(), calculator: 'rgba'};
Attributes.borderColor             = {defaultValue: Color(), calculator: 'rgba'};
Attributes.outlineColor            = {defaultValue: Color(), calculator: 'rgba'};
Attributes.textDecorationColor     = {defaultValue: Color(), calculator: 'rgba'};

Attributes.textShadowX             = {defaultValue: 0, defaultUnit: 'px', property: 'textShadow'};
Attributes.textShadowY             = {defaultValue: 0, defaultUnit: 'px', property: 'textShadow'};
Attributes.textShadowPosition      = {defaultValue: {x: 0, y: 0}, defaultUnit: 'px', calculator: '2d', property: 'textShadow'};
Attributes.textShadowBlur          = {defaultValue: 0, defaultUnit: 'px', property: 'textShadow'};
Attributes.textShadowColor         = {defaultValue: Color(), calculator: 'rgba', property: 'textShadow'};

Attributes.shadowX                 = {defaultValue: 0, defaultUnit: 'px', property: 'shadow'};
Attributes.shadowY                 = {defaultValue: 0, defaultUnit: 'px', property: 'shadow'};
Attributes.shadowPosition          = {defaultValue: {x: 0, y: 0}, defaultUnit: 'px', calculator: '2d', property: 'shadow'};
Attributes.shadowBlur              = {defaultValue: 0, defaultUnit: 'px', property: 'shadow'};
Attributes.shadowSpread            = {defaultValue: 0, defaultUnit: 'px', property: 'shadow'};
Attributes.shadowColor             = {defaultValue: Color(), calculator: 'rgba', property: 'shadow'};
Attributes.shadowInset             = {defaultValue: 0, property: 'shadow'};

Attributes.scrollTop               = {defaultValue: 0};
Attributes.scrollLeft              = {defaultValue: 0};


/**
 * Returns an attribute based on the given input. If the input is an object it's assumed to be an attribute and it's
 * returned immediately. If the input is a string the attribute with the given name is returned. Otherwise
 * the default attribute is returned.
 *
 * @param {Object|String} attr
 */
function $attribute(attr)
{
  if ( isObject( attr ) && isDefined( attr.defaultValue ) )
  {
    return attr;
  }
  if ( isString( attr ) && attr in Attributes )
  {
    return Attributes[ attr ];
  }

  return Attributes['default'];
}



function factory(nm, relativeTo)
{
  return {

    get: function(e, anim)
    {
      if (anim.animating[nm] === false)
      {
        var style = $style( e, nm );
        var converted = $convert( e, style, anim.units[ nm ], relativeTo );

        if ( converted !== false )
        {
          anim.frame[ nm ] = converted;
          anim.animating[ nm  ] = true;
        }
      }
    },
    set: function(e, anim)
    {
      anim.styles[ nm ] = anim.value( nm );
    },
    unset: function(e, anim, attr)
    {
      e.style[ nm ] = null;
    }
  };
}

function factoryDerivable(nm, relativeTo, deriver)
{
  return {

    get: function(e, anim)
    {
      if (anim.animating[nm] === false)
      {
        var style = $style( e, nm );
        var converted = $convert( e, style, anim.units[ nm ], relativeTo );

        if ( converted !== false )
        {
          anim.frame[ nm ] = converted;
          anim.animating[ nm ] = true;
        }
        else if ( isFunction( deriver ) )
        {
          converted = $convert( e, deriver( e ), anim.units[ nm ], relativeTo );

          if ( converted !== false )
          {
            anim.frame[ nm ] = converted;
            anim.animating[ nm ] = true;
          }
        }
      }
    },
    set: function(e, anim)
    {
      anim.styles[ nm ] = anim.value( nm );
    },
    unset: function(e, anim, attr)
    {
      e.style[ nm ] = null;
    }
  };
}

function factoryColor(nm)
{
  return {

    get: function(e, anim)
    {
      if (anim.animating[nm] === false)
      {
        var style = $style( e, nm );
        var parsed = Color.parse( style );

        if (parsed !== false)
        {
          anim.frame[nm] = parsed;
          anim.animating[nm] = true;
        }
      }
    },
    set: function(e, anim)
    {
      anim.styles[ nm ] = Color.format( anim.frame[nm] );
    },
    unset: function(e, anim, attr)
    {
      e.style[ nm ] = null;
    }
  };
}

function factoryNumberAttribute(nm)
{
  return {

    get: function(e, anim)
    {
      if (anim.animating[nm] === false)
      {
        var parsed = parseFloat( e[ nm ] );

        if (isFinite(parsed))
        {
          anim.frame[nm] = parsed;
          anim.animating[nm] = true;
        }
      }
    },
    set: function(e, anim)
    {
      anim.attributes[ nm ] = anim.frame[nm];
    },
    unset: function(e, anim, attr)
    {
      e[ nm ] = null;
    }
  };
}


var Properties = {};

Properties.noop =
{
  get: function(e, anim)
  {
  },
  set: function(e, anim)
  {
  },
  unset: function(e, anim)
  {
  }
};

Properties.padding                  = factory( 'padding', 'parentWidth' );
Properties.paddingTop               = factory( 'paddingTop', 'parentWidth' );
Properties.paddingRight             = factory( 'paddingRight', 'parentWidth' );
Properties.paddingBottom            = factory( 'paddingBottom', 'parentWidth' );
Properties.paddingLeft              = factory( 'paddingLeft', 'parentWidth' );

Properties.margin                   = factory( 'margin', 'parentWidth' );
Properties.marginTop                = factory( 'marginTop', 'parentWidth' );
Properties.marginRight              = factory( 'marginRight', 'parentWidth' );
Properties.marginBottom             = factory( 'marginBottom', 'parentWidth' );
Properties.marginLeft               = factory( 'marginLeft', 'parentWidth' );

Properties.borderRadius             = factory( 'borderRadius', 'width' );
Properties.borderTopLeftRadius      = factory( 'borderTopLeftRadius', 'width' );
Properties.borderTopRightRadius     = factory( 'borderTopRightRadius', 'width' );
Properties.borderBottomLeftRadius   = factory( 'borderBottomLeftRadius', 'width' );
Properties.borderBottomRightRadius  = factory( 'borderBottomRightRadius', 'width' );

Properties.borderWidth              = factory( 'borderWidth' );
Properties.borderTopWidth           = factory( 'borderTopWidth' );
Properties.borderRightWidth         = factory( 'borderRightWidth' );
Properties.borderBottomWidth        = factory( 'borderBottomWidth' );
Properties.borderLeftWidth          = factory( 'borderLeftWidth' );

Properties.outlineWidth             = factory( 'outlineWidth' );
Properties.textIndent               = factory( 'textIndent', 'parentWidth' );
Properties.tabSize                  = factory( 'tabSize', 'parentWidth' );
Properties.borderSpacing            = factory( 'borderSpacing' );
Properties.fontSize                 = factory( 'fontSize', 'parentFontSize' );
Properties.lineHeight               = factory( 'lineHeight', 'fontSize' );
Properties.letterSpacing            = factory( 'letterSpacing' );
Properties.wordSpacing              = factory( 'wordSpacing' );

Properties.zIndex                   = factory( 'zIndex' );

Properties.color                    = factoryColor( 'color' );
Properties.backgroundColor          = factoryColor( 'backgroundColor' );
Properties.borderTopColor           = factoryColor( 'borderTopColor' );
Properties.borderRightColor         = factoryColor( 'borderRightColor' );
Properties.borderBottomColor        = factoryColor( 'borderBottomColor' );
Properties.borderLeftColor          = factoryColor( 'borderLeftColor' );
Properties.borderColor              = factoryColor( 'borderColor' );
Properties.outlineColor             = factoryColor( 'outlineColor' );
Properties.textDecorationColor      = factoryColor( 'textDecorationColor' );

Properties.minWidth                 = factory( 'minWidth', 'parentWidth' );
Properties.maxWidth                 = factory( 'maxWidth', 'parentWidth' );
Properties.minHeight                = factory( 'minHeight', 'parentHeight' );
Properties.maxHeight                = factory( 'maxHeight', 'parentHeight' );

Properties.width                    = factoryDerivable('width', 'parentWidth', function(e) { return e.offsetWidth + 'px'; });
Properties.height                   = factoryDerivable('height', 'parentHeight', function(e) { return e.offsetHeight + 'px'; });

Properties.top                      = factoryDerivable('top', 'parentHeight', function(e) { return e.offsetTop + 'px'; });
Properties.right                    = factoryDerivable('right', 'parentWidth', function(e) { return (e.parentNode.scrollWidth - (e.offsetLeft + e.offsetWidth)) + 'px'; });
Properties.bottom                   = factoryDerivable('bottom', 'parentHeight', function(e) { return (e.parentNode.scrollHeight - (e.offsetTop + e.offsetHeight)) + 'px'; });
Properties.left                     = factoryDerivable('left', 'parentWidth', function(e) { return e.offsetLeft + 'px'; });

Properties.scrollTop                = factoryNumberAttribute( 'scrollTop' );
Properties.scrollLeft               = factoryNumberAttribute( 'scrollLeft' );

Properties.zIndex.set = function(e, anim)
{
  anim.styles.zIndex = Math.floor( anim.frame.zIndex );
};

Properties.visibility =
{
  get: function(e, anim)
  {
    if (anim.animating.visibility === false)
    {
      var style = $style( e, 'visibility' );
      anim.frame.visibility = style === 'hidden' ? 0.0 : 1.0;
      anim.animating = true;
    }
  },
  set: function(e, anim)
  {
    anim.styles.visibility = anim.frame.visibility < 0.5 ? 'hidden' : 'visible';
  },
  unset: function(e, anim)
  {
    e.style.visibility = null;
  }
};

Properties.backface = (function()
{
  var css = $prefix(['WebkitBackfaceVisibility', 'MozBackfaceVisibility', 'msBackfaceVisibility', 'BackfaceVisibility']);

  if ( !css )
  {
    return Properties.noop;
  }

  return {

    get: function(e, anim)
    {
      if ( anim.animating.backface === false )
      {
        var style = $style( e, css );

        anim.frame.backface = (style === 'visible') ? 1.0 : 0.0;
        anim.animating.backface = true;
      }
    },
    set: function(e, anim)
    {
      anim.styles[css] = anim.frame.backface < 0.5 ? 'none' : 'visible';
    },
    unset: function(e, anim)
    {
      e.style[ css ] = null;
    }

  };

})();

Properties.transformOrigin = (function()
{
  var css = $prefix(['WebkitTransformOrigin', 'MozTransformOrigin', 'OTransformOrigin', 'msTransformOrigin', 'transformOrigin']);

  if ( !css )
  {
    return Properties.noop;
  }

  var keywords =
  {
    'left':   '0%',
    'center': '50%',
    'right':  '100%',
    'top':    '0%',
    'bottom': '100%'
  };

  var setOriginAttribute = function(e, value, anim, attr, relativeTo )
  {
    if (anim.animating[attr] === false)
    {
      if ( value in keywords )
      {
        value = keywords[ value ];
      }

      var converted = $convert( e, value, anim.units[ attr ], relativeTo );

      if ( converted !== false )
      {
        anim.frame[ attr ] = converted;
        anim.animating[ attr ] = true;
      }
    }
  };

  var setOrigin = function(e, split, anim)
  {
    if (anim.animating.origin === false)
    {
      if ((split.length === 1) ||
          (split.length === 2 && split[0] === split[1]) ||
          (split.length === 3 && split[0] === split[1] && split[1] === split[2]))
      {
        setOriginAttribute( e, split[0], anim, 'origin', 'width' );
      }
    }
  };

  return {

    get: function(e, anim)
    {
      var style = $style( e, css );

      if (style)
      {
        var origin = style.toLowerCase();
        var split = origin.split(' ');

        switch (split.length)
        {
        case 3:
          setOriginAttribute( e, split[0], anim, 'originX', 'width' );
          setOriginAttribute( e, split[1], anim, 'originY', 'height' );
          setOriginAttribute( e, split[2], anim, 'originZ' );
          setOrigin( e, split, anim );
          break;
        case 2:
          setOriginAttribute( e, split[0], anim, 'originX', 'width' );
          setOriginAttribute( e, split[1], anim, 'originY', 'height' );
          setOrigin( e, split, anim );
          break;
        case 1:
          setOriginAttribute( e, split[0], anim, 'originX', 'width' );
          setOriginAttribute( e, split[0], anim, 'originY', 'height' );
          setOrigin( e, split, anim );
          break;
        }
      }
    },
    set: function(e, anim)
    {
      var style = null;

      if ( isDefined( anim.frame.originZ ) )
      {
        style = anim.valueOr( 'originX', 'origin', 'x' ) + ' ' + anim.valueOr( 'originY', 'origin', 'y' ) + ' ' + anim.valueOr( 'originZ', 'origin', 'z' );
      }
      else
      {
        style = anim.valueOr( 'originX', 'origin', 'x' ) + ' ' + anim.valueOr( 'originY', 'origin', 'y' );
      }

      anim.styles[css] = style;
    },
    unset: function(e, anim, attr)
    {
      unset( e, anim, attr, this, css, null );
    }
  };

})();

Properties.transform = (function()
{
  var css = $prefix(['WebkitTransform', 'MozTransform', 'OTransform', 'msTransform', 'transform']);

  if ( !css )
  {
    return Properties.noop;
  }

  var parse = function( e, value, anim, attr, relativeTo )
  {
    var desiredUnit = anim.units[ attr ];
    var converted = $convert( e, value, desiredUnit, relativeTo );

    if ( converted !== false )
    {
      return converted;
    }

    // TODO show convert this to desiredUnit, however defaultValue may be non-scalar.
    return anim.getAttribute( attr ).defaultValue;
  };

  var getter1d = function(e, anim, parsed, attr)
  {
    return parse( e, parsed[1], anim, attr, 'width' );
  };
  var getter2d = function(e, anim, parsed, attr)
  {
    return {
      x: parse( e, parsed[1], anim, attr, 'width' ),
      y: parse( e, parsed[2], anim, attr, 'height' )
    };
  };
  var getter3d = function(e, anim, parsed, attr)
  {
    return {
      x: parse( e, parsed[1], anim, attr, 'width' ),
      y: parse( e, parsed[2], anim, attr, 'height' ),
      z: parse( e, parsed[3], anim, attr )
    };
  };
  var getter4d = function(e, anim, parsed, attr)
  {
    return {
      x: parse( e, parsed[1], anim, attr, 'width' ),
      y: parse( e, parsed[2], anim, attr, 'height' ),
      z: parse( e, parsed[3], anim, attr ),
      angle: parse( e, parsed[4], anim, attr )
    };
  };

  var setter1d = function(attr, value, unit)
  {
    return attr + '(' + value + unit + ')';
  };
  var setter2d = function(attr, value, unit)
  {
    return attr + '(' + value.x + unit + ',' + value.y + unit + ')';
  };
  var setter3d = function(attr, value, unit)
  {
    return attr + '(' + value.x + unit + ',' + value.y + unit + ',' + value.z + unit + ')';
  };
  var setter4d = function(attr, value, unit)
  {
    return attr + '(' + value.x + ',' + value.y + ',' + value.z + ',' + value.angle + unit + ')';
  };
  var combine = function(ax, ay, bx, by, ascl, bscl)
  {
    return {
      x: (ascl * ax) + (bscl * bx),
      y: (ascl * ay) + (bscl * by)
    };
  };
  var place1d = function(anim, e, attr, value, relativeTo)
  {
    if ( anim.animating[ attr ] === false )
    {
      anim.frame[ attr ] = $convert( e, value, anim.units[ attr ], relativeTo );
      anim.animating[ attr ] = true;
    }
  };
  var place2d = function(anim, e, attr, valueX, valueY, relativeToX, relativeToY)
  {
    if ( anim.animating[ attr ] === false )
    {
      anim.frame[ attr ] = {
        x: $convert( e, valueX, anim.units[ attr ], relativeToX ),
        y: $convert( e, valueY, anim.units[ attr ], relativeToY )
      };
      anim.animating[ attr ] = true;
    }
  };
  var place3d = function(anim, e, attr, valueX, valueY, valueZ, relativeToX, relativeToY, relativeToZ)
  {
    if ( anim.animating[ attr ] === false )
    {
      anim.frame[ attr ] = {
        x: $convert( e, valueX, anim.units[ attr ], relativeToX ),
        y: $convert( e, valueY, anim.units[ attr ], relativeToY ),
        z: $convert( e, valueZ, anim.units[ attr ], relativeToZ )
      };
      anim.animating[ attr ] = true;
    }
  };
  var place4d = function(anim, e, attr, valueX, valueY, valueZ, valueRotate, relativeToX, relativeToY, relativeToZ, relativeToRotate)
  {
    if ( anim.animating[ attr ] === false )
    {
      anim.frame[ attr ] = {
        x: $convert( e, valueX, anim.units[ attr ], relativeToX ),
        y: $convert( e, valueY, anim.units[ attr ], relativeToY ),
        z: $convert( e, valueZ, anim.units[ attr ], relativeToZ ),
        angle: $convert( e, valueRotate, anim.units[ attr ], relativeToRotate )
      };
      anim.animating[ attr ] = true;
    }
  };

  var regexes =
  {
    translate:    /translate\(([^,]+)\s*,\s*([^\)]+)\)/i,
    translate3d:  /translate3d\(([^,]+)\s*,\s*([^,]+)\s*,\s*([^\)]+)\)/i,
    translateX:   /translateX\(([^\)]+)\)/i,
    translateY:   /translateY\(([^\)]+)\)/i,
    translateZ:   /translateZ\(([^\)]+)\)/i,
    scale:        /scale\(([^,]+)\s*,\s*([^\)]+)\)/i,
    scale3d:      /scale3d\(([^,]+)\s*,\s*([^,]+)\s*,\s*([^\)]+)\)/i,
    scaleX:       /scaleX\(([^\)]+)\)/i,
    scaleY:       /scaleY\(([^\)]+)\)/i,
    scaleZ:       /scaleZ\(([^\)]+)\)/i,
    rotate:       /rotate\(([^\)]+)\)/i,
    skew:         /skew\(([^,]+)\s*,\s*([^\)]+)\)/i,
    skewX:        /skewX\(([^\)]+)\)/i,
    skewY:        /skewY\(([^\)]+)\)/i,
    rotate3d:     /rotate3d\(([^,]+)\s*,\s*([^,]+)\s*,\s*([^,]+)\s*,\s*([^\)]+)\)/i,
    rotateX:      /rotateX\(([^\)]+)\)/i,
    rotateY:      /rotateY\(([^\)]+)\)/i,
    rotateZ:      /rotateZ\(([^\)]+)\)/i
  };

  var matrix = /matrix\(([^,]+)\s*,\s*([^,]+)\s*,\s*([^,]+)\s*,\s*([^,]+)\s*,\s*([^,]+)\s*,\s*([^,]+)\)/i;

  var getters =
  {
    translate:    getter2d,
    translate3d:  getter3d,
    translateX:   getter1d,
    translateY:   getter1d,
    translateZ:   getter1d,
    scale:        getter2d,
    scale3d:      getter3d,
    scaleX:       getter1d,
    scaleY:       getter1d,
    scaleZ:       getter1d,
    rotate:       getter1d,
    rotate3d:     getter4d,
    rotateX:      getter1d,
    rotateY:      getter1d,
    rotateZ:      getter1d,
    skew:         getter2d,
    skewX:        getter1d,
    skewY:        getter1d
  };

  var setters =
  {
    translate:    setter2d,
    translate3d:  setter3d,
    translateX:   setter1d,
    translateY:   setter1d,
    translateZ:   setter1d,
    scale:        setter2d,
    scale3d:      setter3d,
    scaleX:       setter1d,
    scaleY:       setter1d,
    scaleZ:       setter1d,
    rotate:       setter1d,
    rotate3d:     setter4d,
    rotateX:      setter1d,
    rotateY:      setter1d,
    rotateZ:      setter1d,
    skew:         setter2d,
    skewX:        setter1d,
    skewY:        setter1d
  };

  var props = new FastMap( regexes );
  var regex = props.values;
  var attrs = props.keys;
  props.setters = [];
  props.getters = [];

  for (var prop in getters)
  {
    var i = props.indexOf( prop );

    props.getters[ i ] = getters[ prop ];
    props.setters[ i ] = setters[ prop ];
  }

  return {

    get: function(e, anim)
    {
      var style = $style( e, css );

      var matrixParsed = matrix.exec( style );

      if ( matrixParsed )
      {
        var a = parseFloat( matrixParsed[ 1 ] );
        var b = parseFloat( matrixParsed[ 2 ] );
        var c = parseFloat( matrixParsed[ 3 ] );
        var d = parseFloat( matrixParsed[ 4 ] );
        var tx = parseFloat( matrixParsed[ 5 ] );
        var ty = parseFloat( matrixParsed[ 6 ] );

        // Make sure the matrix is invertible
        if ((a * d - b * c) !== 0)
        {
          // Take care of translation
          var translateX = tx + 'px';
          var translateY = ty + 'px';

          // Compute X scale factor and normalize first row.
          var scaleX = Math.sqrt( a * a + b * b );
          if ( scaleX !== 0 )
          {
            a /= scaleX;
            b /= scaleX;
          }

          // Compute shear factor and make 2nd row orthogonal to 1st.
          var skew = a * c + b * d;
          var combined = combine( c, d, a, b, 1.0, -skew );
          c = combined.x;
          d = combined.y;

          // Now, compute Y scale and normalize 2nd row.
          var scaleY = Math.sqrt( c * c + d * d );
          if ( scaleY !== 0 )
          {
            c /= scaleY;
            d /= scaleY;
            skew /= scaleY;
          }

          // Now, get the rotation out
          var rotate = Math.atan2( b, a ) + 'rad';

          // Place values in animator.
          place2d( anim, e, 'translate', translateX, translateY, 'width', 'height' );
          place3d( anim, e, 'translate3d', translateX, translateY, 0, 'width', 'height' );
          place1d( anim, e, 'translateX', translateX, 'width' );
          place1d( anim, e, 'translateY', translateY, 'height' );
          place2d( anim, e, 'scale', scaleX, scaleY );
          place1d( anim, e, 'scaleX', scaleX );
          place1d( anim, e, 'scaleY', scaleY );
          place3d( anim, e, 'scale3d', scaleX, scaleY, 1 );
          place1d( anim, e, 'rotate', rotate );
          place4d( anim, e, 'rotate3d', 0, 0, 1, rotate );
          place1d( anim, e, 'rotateZ', rotate );
          place2d( anim, e, 'skew', skew, skew );
          place1d( anim, e, 'skewX', skew );
          place1d( anim, e, 'skewY', skew );

          return;
        }
      }

      for (var attr in anim.animating)
      {
        var i = props.indexOf( attr );

        if ( i !== -1 && anim.animating[ attr ] === false )
        {
          var parsed = regex[ i ].exec( style );

          if ( parsed )
          {
            anim.frame[ attr ] = props.getters[ i ]( e, anim, parsed, attr );
            anim.animating[ attr ] = true;
          }
        }
      }
    },
    set: function(e, anim)
    {
      var transforms = [];

      for (var i = 0; i < attrs.length; i++)
      {
        var attr = attrs[ i ];

        if ( attr in anim.frame )
        {
          transforms.push( props.setters[ i ]( attr, anim.frame[ attr ], anim.units[ attr ] ) );
        }
      }

      if (transforms.length)
      {
        anim.styles[ css ] = transforms.join( ' ' );
      }
    },
    unset: function(e, anim, attr)
    {
      unset( e, anim, attr, this, css, '' );
    }
  };

})();

Properties.opacity = (function()
{
  var css = $prefix(['WebkitOpacity', 'MozOpacity', 'KhtmlOpacity', 'opacity']);

  if ( !css )
  {
    return Properties.noop;
  }

  return {

    get: function(e, anim)
    {
      if (anim.animating.opacity === false)
      {
        var style = $style( e, css );
        var opacity = parseFloat( style );

        if ( !isNaN(opacity) )
        {
          anim.frame.opacity = opacity;
          anim.animating.opacity = true;
        }
      }
    },
    set: function(e, anim)
    {
      anim.styles[ css ] = clamp( anim.frame.opacity, 0, 1 );
    },
    unset: function(e, anim)
    {
      e.style[ css ] = null;
    }
  };

})();

Properties.shadow = (function()
{
  var css = $prefix(['WebkitBoxShadow', 'MozBoxShadow', 'boxShadow']);

  if ( !css )
  {
    return Properties.noop;
  }

  var parsePart = function( e, anim, attr, value, relativeTo )
  {
    if ( anim.updating[ attr ] === false && value )
    {
      var parsed = $convert( e, value, anim.units[ attr ], relativeTo );

      if ( parsed !== false )
      {
        anim.frame[ attr ] = parsed;
        anim.updating[ attr ] = true;
      }
    }
  };

  return {

    get: function(e, anim)
    {
      var style = $style( e, css );
      var parts = style.split( ' ' );

      if ( parts.length < 3 )
      {
        return;
      }

      var inset = 0;

      if ( parts[ 0 ] === 'inset' )
      {
        inset = 1;
        parts.shift();
      }

      var x = parts[ 0 ];
      var y = parts[ 1 ];
      var blur = false, spread = false, color = false;

      switch ( parts.length ) {
        case 3:
          color = parts[ 2 ];
          break;
        case 4:
          blur = parts[ 2 ];
          color = parts[ 3 ];
          break;
        case 5:
          blur = parts[ 2 ];
          spread = parts[ 3 ];
          color = parts[ 4 ];
          break;
      }

      parsePart( e, anim, 'shadowX', x, 'width' );
      parsePart( e, anim, 'shadowY', y, 'height' );
      parsePart( e, anim, 'shadowBlur', blur, 'width' );
      parsePart( e, anim, 'shadowSpread', spread, 'width' );

      if ( anim.updating.shadowPosition === false )
      {
        var parsedX = $convert( e, x, anim.units.shadowPosition, 'width' );
        var parsedY = $convert( e, y, anim.units.shadowPosition, 'height' );

        if ( parsedX !== false && parsedY !== false )
        {
          anim.frame.shadowPosition = {
            x: parsedX,
            y: parsedY
          };
          anim.updating.shadowPosition = true;
        }
      }

      if ( anim.updating.shadowInset === false )
      {
        anim.frame.shadowInset = inset;
        anim.updating.shadowInset = true;
      }

      if ( anim.updating.shadowColor === false )
      {
        var parsed = Color.parse( color );

        if ( parsed !== false )
        {
          anim.frame.shadowColor = parsed;
          anim.updating.shadowColor = true;
        }
      }

    },

    set: function(e, anim)
    {
      var style = '';

      if ( anim.frame.shadowInset )
      {
        style += 'inset '; // TODO test - fixed but not sure
      }

      style += anim.valueOr( 'shadowX', 'shadowPosition', 'x' ) + ' ';
      style += anim.valueOr( 'shadowY', 'shadowPosition', 'y' ) + ' ';

      if ( isNumber( anim.frame.shadowBlur ) )
      {
        style += anim.value( 'shadowBlur' ) + ' ';
      }

      if ( isNumber( anim.frame.shadowSpread ) )
      {
        style += anim.value( 'shadowSpread' ) + ' ';
      }

      style += Color.format( anim.frame.shadowColor );

      anim.styles[ css ] = style;
    },

    unset: function(e, anim, attr)
    {
      unset( e, anim, attr, this, css, null );
    }

  };

})();


Properties.textShadow = (function()
{
  var css = $prefix(['WebkitTextShadow', 'MozTextShadow', 'textShadow']);

  if ( !css )
  {
    return Properties.noop;
  }

  var parsePart = function( e, anim, attr, value, relativeTo )
  {
    if ( anim.updating[ attr ] === false && value )
    {
      var parsed = $convert( e, value, anim.units[ attr ], relativeTo );

      if ( parsed !== false )
      {
        anim.frame[ attr ] = parsed;
        anim.updating[ attr ] = true;
      }
    }
  };

  return {

    get: function(e, anim)
    {
      var style = $style( e, css );
      var parts = style.split( ' ' );

      if ( parts.length < 3 )
      {
        return;
      }

      var x = parts[ 0 ];
      var y = parts[ 1 ];
      var blur = false, color = false;

      switch ( parts.length ) {
        case 3:
          color = parts[ 2 ];
          break;
        case 4:
          blur = parts[ 2 ];
          color = parts[ 3 ];
          break;
      }

      parsePart( e, anim, 'textShadowX', x, 'width' );
      parsePart( e, anim, 'textShadowY', y, 'height' );
      parsePart( e, anim, 'textShadowBlur', blur, 'width' );

      if ( anim.updating.textShadowPosition === false )
      {
        var parsedX = $convert( e, x, anim.units.textShadowPosition, 'width' );
        var parsedY = $convert( e, y, anim.units.textShadowPosition, 'height' );

        if ( parsedX !== false && parsedY !== false )
        {
          anim.frame.textShadowPosition = {
            x: parsedX,
            y: parsedY
          };
          anim.updating.textShadowPosition = true;
        }
      }

      if ( anim.updating.textShadowColor === false )
      {
        var parsed = Color.parse( color );

        if ( parsed !== false )
        {
          anim.frame.textShadowColor = parsed;
          anim.updating.textShadowColor = true;
        }
      }
    },

    set: function(e, anim)
    {
      var style = '';

      if ( anim.frame.shadowInset )
      {
        style += 'inset '; // TODO test - fixed but not sure
      }

      style += anim.valueOr( 'textShadowX', 'textShadowPosition', 'x' ) + ' ';
      style += anim.valueOr( 'textShadowY', 'textShadowPosition', 'y' ) + ' ';

      if ( isNumber( anim.frame.textShadowBlur ) )
      {
        style += anim.value( 'textShadowBlur' ) + ' ';
      }

      if ( isNumber( anim.frame.textShadowSpread ) )
      {
        style += anim.value( 'textShadowSpread' ) + ' ';
      }

      style += Color.format( anim.frame.textShadowColor );

      anim.styles[ css ] = style;
    },

    unset: function(e, anim, attr)
    {
      unset( e, anim, attr, this, css, null );
    }

  };

})();

Properties.filter = (function()
{
  var css = $prefix(['WebkitFilter', 'MozFilter', 'OFilter', 'msFilter', 'filter']);

  if ( !css )
  {
    return Properties.noop;
  }

  var methods =
  {
    grayscale:  'grayscale',
    sepia:      'sepia',
    saturate:   'saturate',
    hueRotate:  'hue-rotate',
    invert:     'invert',
    brightness: 'brightness',
    contrast:   'contrast',
    blur:       'blur'
  };

  var patterns = {};

  for (var attr in methods)
  {
    patterns[attr] = new RegExp( methods[attr] + '\(([^\)]+)\)', 'i');
  }

  return {

    get: function(e, anim)
    {
      var style = $style( e, css );

      for (var attr in patterns)
      {
        if ( anim.animating[attr] === false )
        {
          var parsed = patterns[attr].exec( style );

          if ( parsed )
          {
            var converted = $convert( e, parsed[1], anim.units[ attr ] );

            if ( converted !== false )
            {
              anim.frame[ attr ] = converted;
              anim.animating[ attr ] = true;
            }
          }
        }
      }
    },
    set: function(e, anim)
    {
      // we don't check anim.updated[attr] here since the current value of a transform property is important

      var filters = [];

      for (var attr in methods)
      {
        if ( attr in anim.frame )
        {
          filters.push( methods[attr] + '(' + anim.value( attr ) + ')' );
        }
      }

      if (filters.length)
      {
        anim.styles[ css ] = filters.join(' ');
      }
    },
    unset: function(e, anim, attr)
    {
      unset( e, anim, attr, this, css, null );
    }
  };

})();

Properties.center =
{
  get: function(e, anim)
  {
    var cx = (e.offsetLeft + e.offsetWidth * 0.5) + 'px';
    var cy = (e.offsetTop + e.offsetHeight * 0.5) + 'px';

    if ( anim.animating.center === false )
    {
      var desiredUnit = anim.units.center;
      var ccx = $convert( e, cx, desiredUnit, 'parentWidth' );
      var ccy = $convert( e, cy, desiredUnit, 'parentHeight' );

      if ( ccx !== false && ccy !== false )
      {
        anim.frame.center = {
          x: ccx,
          y: ccy
        };
        anim.animating.center = true;
      }
    }
    if ( anim.animating.centerX === false )
    {
      var desiredUnit = anim.units.centerX;
      var ccx = $convert( e, cx, desiredUnit, 'parentWidth' );

      if ( ccx !== false )
      {
        anim.frame.centerX = ccx;
        anim.animating.centerX = true;
      }
    }
    if ( anim.animating.centerY === false )
    {
      var desiredUnit = anim.units.centerY;
      var ccy = $convert( e, cy, desiredUnit, 'parentHeight' );

      if ( ccy !== false )
      {
        anim.frame.centerY = ccy;
        anim.animating.centerY = true;
      }
    }
  },
  preset: function(e, anim)
  {
    anim.cached.width  = $convert( e, e.offsetWidth + 'px', anim.units.centerX || anim.units.center, 'parentWidth' );
    anim.cached.height = $convert( e, e.offsetHeight + 'px', anim.units.centerY || anim.units.center, 'parentHeight' );
  },
  set: function(e, anim)
  {
    var rw = anim.cached.width * 0.5;
    var rh = anim.cached.height * 0.5;

    if ( anim.updated.center )
    {
      anim.styles.left = (anim.frame.center.x - rw) + anim.units.center;
      anim.styles.top = (anim.frame.center.y - rh) + anim.units.center;
    }
    if ( anim.updated.centerX )
    {
      anim.styles.left = (anim.frame.centerX - rw) + anim.units.centerX;
    }
    if ( anim.updated.centerY )
    {
      anim.styles.top = (anim.frame.centerY - rh) + anim.units.centerY;
    }
  },
  unset: function(e, anim, attr)
  {

  }
};

Properties.orbit =
{
  DEGREE_TO_RADIAN: Math.PI / 180.0,

  RADIAN_TO_DEGREE: 180.0 / Math.PI,

  get: function(e, anim)
  {
    var ox = (e.parentNode.scrollWidth * 0.5);
    var oy = (e.parentNode.scrollHeight * 0.5);
    var cx = (e.offsetLeft + e.offsetWidth * 0.5);
    var cy = (e.offsetTop + e.offsetHeight * 0.5);
    var dx = cx - ox;
    var dy = cy - oy;

    if ( anim.animating.orbitOffset === false )
    {
      var cunit = anim.units.orbitOffset;
      var cox = $convert( e, ox + 'px', cunit, 'parentWidth' );
      var coy = $convert( e, oy + 'px', cunit, 'parentHeight' );

      if ( cox !== false && coy !== false )
      {
        anim.frame.orbitOffset = {
          x: cox,
          y: coy
        };
        anim.animating.orbitOffset = false;
      }
    }

    if ( anim.animating.distance === false )
    {
      anim.frame.distance = Math.sqrt( dx * dx + dy * dy );
      anim.animating.distance = true;
    }

    if ( anim.animating.angle === false )
    {
      anim.frame.angle = Math.atan2( dy, dx ) * this.RADIAN_TO_DEGREE;
      anim.animating.angle = true;
    }
  },

  preset: function(e, anim)
  {
    anim.cached.parentWidth = e.parentNode.scrollWidth;
    anim.cached.parentHeight = e.parentNode.scrollHeight;
    anim.cached.width = e.offsetWidth;
    anim.cached.height = e.offsetHeight;
  },

  set: function(e, anim)
  {
    // TODO calculator this correctly
    var cunit = anim.units.orbitOffset || '%';
    var orbitX = anim.frame.orbitOffset ? anim.frame.orbitOffset.x : 50;
    var orbitY = anim.frame.orbitOffset ? anim.frame.orbitOffset.y : 50;

    var originUnit = anim.units.origin || '%';
    var originX = anim.frame.origin ? anim.frame.origin.x : 50;
    var originY = anim.frame.origin ? anim.frame.origin.y : 50;

    var cox = $convert( e, orbitX + cunit, 'px', anim.cached.parentWidth / 100.0 );
    var coy = $convert( e, orbitY + cunit, 'px', anim.cached.parentHeight / 100.0 );

    var ox = $convert( e, originX + originUnit, 'px', anim.cached.width / 100.0 );
    var oy = $convert( e, originY + originUnit, 'px', anim.cached.height / 100.0 );

    var angle = (anim.frame.angle || 0.0) * this.DEGREE_TO_RADIAN;
    var distance = anim.frame.distance || 0.0;

    var cos = Math.cos( angle ) * distance;
    var sin = Math.sin( angle ) * distance;

    anim.styles.left = (cox + cos - ox) + 'px';
    anim.styles.top = (coy + sin - oy) + 'px';
  },

  unset: function(e, anim, attr)
  {

  }
};


/**
 * Returns a property for the given name.
 *
 * @param  {String|Object}
 * @return {Object}
 */
function $property(prop)
{
  if ( isObject( prop ) && isFunction( prop.get ) && isFunction( prop.set ) )
  {
    return prop;
  }
  if ( isString( prop ) && prop in Properties )
  {
    return Properties[ prop ];
  }

  throw prop + ' is not a valid property';
}


/**
 * Instantiates a new AnimatorDom given a subject.
 *
 * @param {HTMLElement} e
 * @class AnimatorDom
 * @constructor
 * @extends Animator
 */
function AnimatorDom(subject)
{
  this.reset( subject );
  this.properties = new FastMap();
  this.propertiesPreset = new FastMap();
  this.attributeToProperty = {};
  this.animating = {};
  this.cached = {};
  this.units = {};
  this.styles = {};
  this.attributes = {};
  this.styled = false;
  this.stylesUpdated = false;
}

/**
 * Extends anim8.Animator
 */
Class.extend( AnimatorDom, Animator,
{
  preupdate: function(now)
  {
    // If there are events with paths that contain computed values we should
    // populate the frame directly from the HTML element.
    var aa = this.attrimatorsAdded;

    if ( aa.length )
    {
      var properties = {};

      for (var i = aa.length - 1; i >= 0; i--)
      {
        var attrimator = aa[ i ];
        var attr = attrimator.attribute;

        if ( !(attr in this.frame) && attrimator.hasComputed() )
        {
          properties[ this.attributeToProperty[ attr ] ] = true;

          this.animating[ attr ] = false;
        }
      }

      for (var prop in properties)
      {
        this.properties.get( prop ).get( this.subject, this );
      }

      for (var i = aa.length - 1; i >= 0; i--)
      {
        var attrimator = aa[ i ];
        var attr = attrimator.attribute;

        this.setDefault( attr );

        attrimator.start( now, this );
      }

      aa.length = 0;
    }

    // If a property currently being animated requires some heads up before it
    // gets or sets a value, notify it. TODO removed dead properties.
    var presets = this.propertiesPreset.values;
    for (var i = presets.length - 1; i >= 0; i--)
    {
      presets[ i ].preset( this.subject, this );
    }

    this.trigger('preupdate');

    return this;
  },

  update: function(now)
  {
    this._update( now );

    this.getStyles();
    this.stylesUpdated = true;

    return this;
  },

  apply: function()
  {
    if ( !this.styled && !this.stylesUpdated )
    {
      this.getStyles();
    }

    if ( this.styled )
    {
      for (var prop in this.styles)
      {
         this.subject.style[ prop ] = this.styles[ prop ];
      }

      for (var prop in this.attributes)
      {
        this.subject[ prop ] = this.attributes[ prop ];
      }

      for (var attr in this.frame)
      {
        this.updated[ attr ] = false;
      }

      this.styled = false;
    }

    this.stylesUpdated = false;

    this.trigger('apply');

    this.trimAttrimators();

    return this;
  },

  placeAttrimator: function( attrimator )
  {
    this._placeAttrimator( attrimator );

    var attr = attrimator.attribute;
    var attribute = this.getAttribute( attr );
    var propertyName = attribute.propertyName;
    var property = attribute.property;

    this.properties.put( propertyName, property );

    this.attributeToProperty[ attr ] = propertyName;
    this.units[ attr ] = attribute.defaultUnit;

    if ( attrimator.input && attrimator.input.units && attr in attrimator.input.units )
    {
      this.units[ attr ] = attrimator.input.units[ attr ];
    }

    if ( isFunction( property.preset ) )
    {
      this.propertiesPreset.put( propertyName, property );
    }

    return this;
  },
  restore: function()
  {
    var props = this.properties.values;

    for (var i = props.length - 1; i >= 0; i--)
    {
      props[ i ].unset( this.subject, this, true );
    }

    this.frame = {};

    return this;
  },
  unset: function( attributes )
  {
    var attributes = toArray( coalesce( attributes, this.frame ) );

    for (var i = attributes.length - 1; i >= 0; i--)
    {
      var attr = attributes[ i ];
      var prop = this.attributeToProperty[ attr ];
      var property = this.properties.get( prop );

      if ( property )
      {
        property.unset( this.subject, this, attr );
      }

      this.attrimators.remove( attr );

      delete this.frame[ attr ];
    }

    return this;
  },
  set: function( attributes )
  {
    var props = {};
    var updated = {};
    var units = {};
    var styles = {};
    var attrs = {};

    for (var attr in attributes)
    {
      var attribute = this.getAttribute( attr );
      var value     = attributes[ attr ];

      units[ attr ] = attribute.defaultUnit;

      if ( isString( value ) )
      {
        var parsed = $parseValue( value, attribute.defaultUnit );

        if ( parsed !== false )
        {
          units[ attr ] = parsed.unit || attribute.defaultUnit;
          value = parsed.value;
        }
      }

      var parsed = attribute.parse( value );

      if ( parsed !== false )
      {
        props[ attribute.propertyName ] = attribute.property;
        attributes[ attr ] = parsed;
        updated[ attr ] = true;
      }
    }

    var flash =
    {
      source: this,

      units: units,

      frame: attributes,

      updated: updated,

      styles: styles,

      attributes: attrs,

      cached: {},

      get: function(attributes)
      {
        return this.source.get( attributes );
      },

      value: function(attr)
      {
        return attributes[ attr ] + units[ attr ];
      },

      valueOr: function(attr, other, subproperty)
      {
        var value = attributes[ attr ];

        if ( !isDefined( value ) )
        {
          value = attributes[ attr = other ];

          if ( isDefined( subproperty ) )
          {
            value = value[ subproperty ];
          }
        }

        return value + units[ attr ];
      }
    };

    for (var prop in props)
    {
      var property = props[ prop ];

      if ( isFunction( property.preset ) )
      {
        props[ prop ].preset( this.subject, flash );
      }
    }

    for (var prop in props)
    {
      props[ prop ].set( this.subject, flash );
    }

    for (var prop in styles)
    {
      this.subject.style[ prop ] = styles[ prop ];
    }

    for (var prop in attrs)
    {
      this.subject[ prop ] = attrs[ prop ];
    }

    return this;
  },

  /**
   * Builds the styles map in preparation to be applied.
   *
   * @method getStyles
   */
  getStyles: function()
  {
    this.styles = {};
    this.attributes = {};

    var applyProperties = {};

    for (var attr in this.frame)
    {
      if ( this.updated[ attr ] )
      {
        var prop = this.attributeToProperty[ attr ];

        if ( this.properties.has( prop ) )
        {
          applyProperties[ prop ] = true;
        }
      }
    }

    for (var prop in applyProperties)
    {
      this.properties.get( prop ).set( this.subject, this );

      this.styled = true;
    }
  },

  /**
   * Gets the current attribute values for all attributes specified. The argument must be an object
   * where the key is the name of an attribute and the value is the desired unit.
   *
   * @method get
   * @param {Object} attributes
   * @return {Object}
   */
  get: function(attributes)
  {
    var props = {};
    var animating = {};
    var units = {};
    var out = {};

    for (var attr in attributes)
    {
      var attribute = this.getAttribute( attr );

      animating[ attr ] = false;
      units[ attr ] = attributes[ attr ] || attribute.defaultUnit;
      props[ attribute.propertyName ] = attribute.property;
      out[ attr ] = attribute.cloneDefault();
    }

    var flash =
    {
      source: this,

      units: units,

      frame: out,

      animating: animating,

      unit: function(attr)
      {
        return units[ attr ];
      }
    };

    for (var prop in props)
    {
      props[ prop ].get( this.subject, flash );
    }

    return out;
  },

  /**
   * Returns a function that returns the current value for the given attribute when invoked.
   *
   * @param {String} attribute
   * @param {String} desiredUnit
   * @param {String} relativeTo
   * @return {Function}
   */
  ref: function(attribute, desiredUnit, relativeTo)
  {
    var animator = this;
    var request = {};

    return function()
    {
      if ( attribute in animator.frame && isNumber( animator.frame[ attribute ] ) )
      {
        return $convert( animator.e, animator.value( attribute ), desiredUnit, relativeTo );
      }

      request[ attribute ] = desiredUnit;

      var current = animator.get( request );

      if ( isDefined( current[ attribute ] ) )
      {
        return current[ attribute ];
      }

      return animator.getAttribute( attribute ).defaultValue;
    };
  },

  /**
   * Returns the value for the given attribute as a string with the current units.
   *
   * @method value
   * @param {String} attr
   * @return {String}
   */
  value: function(attr)
  {
    return this.frame[ attr ] + this.units[ attr ];
  },

  /**
   * Returns the value for the given attribute as a string with the current units.
   * if the attribute doesn't exist a secondary one is looked up. If that value
   * is an object and contains the given subproperty the value is resolved once again.
   *
   * @method valueOr
   * @param {String} attr
   * @param {String} other
   * @param [String] subproperty
   * @return {String}
   */
  valueOr: function(attr, other, subproperty)
  {
    var value = this.frame[ attr ];

    if ( !isDefined( value ) )
    {
      value = this.frame[ attr = other ];

      if ( isDefined( subproperty ) )
      {
        value = value[ subproperty ];
      }
    }

    return value + this.units[ attr ];
  },

  /**
   * Tweens a single attribute to a target value.
   *
   * **See:** {{#crossLink "Core/anim8.options:method"}}{{/crossLink}}
   *
   * @method tweenTo
   * @param {String} attr
   * @param {T} target
   * @param {String|Array|Object} [options]
   * @param {Boolean} [cache=false]
   * @param {String} [unit]
   * @chainable
   */
  tweenTo: function(attr, target, options, cache, unit)
  {
    this.convertExisting( attr, unit );
    this._tweenTo( attr, target, options, cache );
    this.units[ attr ] = unit || this.units[ attr ];
    return this;
  },

  /**
   * Tweens multiple attributes to target values.
   *
   * **See:** {{#crossLink "Core/anim8.options:method"}}{{/crossLink}}
   *
   * @method tweenManyTo
   * @param {Object} targets
   * @param {String|Array|Object} [options]
   * @param {Boolean} [cache=false]
   * @param {Object} [units]
   * @chainable
   */
  tweenManyTo: function(targets, options, cache, units)
  {
    this.convertExistingMany( units );
    this._tweenManyTo( targets, options, cache );
    override( this.units, units );
    return this;
  },

  /**
   * Tweens a single attribute from a starting value to the current value.
   *
   * **See:** {{#crossLink "Core/anim8.options:method"}}{{/crossLink}}
   *
   * @method tweenFrom
   * @param {String} attr
   * @param {T} starting
   * @param {String|Array|Object} [options]
   * @param {Boolean} [cache=false]
   * @param {String} [unit]
   * @chainable
   */
  tweenFrom: function(attr, starting, options, cache, unit)
  {
    this.convertExisting( attr, unit );
    this._tweenFrom( attr, starting, options, cache );
    this.units[ attr ] = unit || this.units[ attr ];
    return this;
  },

  /**
   * Tweens multiple attributes from starting values to the current values.
   *
   * **See:** {{#crossLink "Core/anim8.options:method"}}{{/crossLink}}
   *
   * @method tweenManyFrom
   * @param {Object} startings
   * @param {String|Array|Object} [options]
   * @param {Boolean} [cache=false]
   * @param {Object} [units]
   * @chainable
   */
  tweenManyFrom: function(startings, options, cache, units)
  {
    this.convertExistingMany( units );
    this._tweenManyFrom( startings, options, cache );
    override( this.units, units );
    return this;
  },

  /**
   * Tweens an attribute from a starting value to an ending value.
   *
   * **See:** {{#crossLink "Core/anim8.options:method"}}{{/crossLink}}
   *
   * @method tween
   * @param {String} attr
   * @param {T} starts
   * @param {T} ends
   * @param {String|Array|Object} [options]
   * @param {Boolean} [cache=false]
   * @param {String} [unit]
   * @chainable
   */
  tween: function(attr, starts, ends, options, cache, unit)
  {
    this.convertExisting( attr, unit );
    this._tween( attr, starts, ends, options, cache );
    this.units[ attr ] = unit || this.units[ attr ];
    return this;
  },

  /**
   * Tweens multiple attributes from starting values to ending values.
   *
   * **See:** {{#crossLink "Core/anim8.options:method"}}{{/crossLink}}
   *
   * @method tweenMany
   * @param {Object} starts
   * @param {Object} ends
   * @param {String|Array|Object} [options]
   * @param {Boolean} [cache=false]
   * @param {Object} [units]
   * @chainable
   */
  tweenMany: function(starts, ends, options, cache, units)
  {
    this.convertExistingMany( units );
    this._tweenMany( starts, ends, options, cache );
    override( this.units, units );
    return this;
  },

  /**
   * Moves an attribute relative to its current value.
   *
   * **See:** {{#crossLink "Core/anim8.options:method"}}{{/crossLink}}
   *
   * @method move
   * @param {String} attr
   * @param {T} amount
   * @param {String|Array|Object} [options]
   * @param {Boolean} [cache=false]
   * @param {String} [unit]
   * @chainable
   */
  move: function(attr, amount, options, cache, unit)
  {
    this.convertExisting( attr, unit );
    this._move( attr, amount, options, cache );
    this.units[ attr ] = unit || this.units[ attr ];
    return this;
  },

  /**
   * Moves multiple attribute relative to their current value.
   *
   * **See:** {{#crossLink "Core/anim8.options:method"}}{{/crossLink}}
   *
   * @method moveMany
   * @param {Object} amounts
   * @param {String|Array|Object} [options]
   * @param {Boolean} [cache=false]
   * @param {Object} [units]
   * @chainable
   */
  moveMany: function(amounts, options, cache, units)
  {
    this.convertExistingMany( units );
    this._moveMany( amounts, options, cache );
    override( this.units, units );
    return this;
  },

  /**
   * Follows the attribute along the given path definition.
   *
   * **See:** {{#crossLink "Core/anim8.options:method"}}{{/crossLink}}
   *
   * @method follow
   * @param {String} attr
   * @param {Path|Object|String} path
   * @param {Object} [options]
   * @param {Boolean} [cache=false]
   * @param {String} [unit]
   * @chainable
   */
  follow: function(attr, path, options, cache, unit)
  {
    this.convertExisting( attr, unit );
    this._follow( attr, path, options, cache );
    this.units[ attr ] = unit || this.units[ attr ];
    return this;
  },

  /**
   * Converts any existing attributes to the desired units.
   *
   * @method convertExistingMany
   * @param  {Object} units
   */
  convertExistingMany: function(units)
  {
    if ( units && isObject( units ) )
    {
      var current = this.get( units );

      for (var attr in current)
      {
        this.frame[ attr ] = current[ attr ];
      }
    }
  },

  /**
   * Converts any existing attribute to the desired unit.
   *
   * @method convertExisting
   * @param  {String} attr
   * @param  {String} toUnit
   */
  convertExisting: function(attr, toUnit)
  {
    if ( toUnit && attr in this.frame && attr in this.units && this.units[ attr ] !== toUnit )
    {
      var request = {};
      request[ attr ] = toUnit;

      this.convertExistingMany( request );
    }
  }

});


/**
 * A factory for HTML Elements
 */
function FactoryDom()
{
  this.cached = {};
  this.ids = 0;
  this.elementAttribute = 'anim8';
  this.priority = 5;
  this.attributes = {};
}

Class.extend( FactoryDom, Factory,
{

  /**
   * Determines whether the given subject is valid for this factory to create Animators for.
   *
   * @param  {any} subject
   * @return {Boolean}
   */
  is: function(subject)
  {
    return isElement( subject );
  },

  /**
   * Returns an animator given a subject.
   *
   * @param  {HTMLElement} subject
   * @return {anim8.Animator}
   */
  animatorFor: function(subject)
  {
    var animatorId = subject.getAttribute( this.elementAttribute );

    if (!(animatorId in this.cached))
    {
      var animator = new AnimatorDom( subject );

      subject.setAttribute( this.elementAttribute, animatorId = animator.id = ++this.ids );

      animator.factory = this;

      this.cached[animatorId] = animator;
    }

    return this.cached[ animatorId ];
  },

  /**
   * Destroys the animator by unlinking the animator from the subject.
   *
   * @param  {anim8.Animator} animator
   */
  destroy: function(animator)
  {
    delete this.cached[ animator.id ];
  },

  /**
   * Returns the attribute descriptor for the given attribute.
   *
   * @param  {String} attr
   * @return {Object}
   */
  attribute: function(attr)
  {
    var attribute = this.attributes[ attr ];

    if ( !attribute )
    {
      attribute = this.attributes[ attr ] = $attribute( attr );

      var calculatorName = attribute.calculator;
      var calculator = $calculator( calculatorName );
      var defaultValue = calculator.parse( attribute.defaultValue, calculator.ZERO );
      var propertyName = coalesce( attribute.property, attr );
      var property = $property( propertyName );
      var defaultUnit = attribute.defaultUnit || '';

      attribute.calculatorName = calculatorName;
      attribute.calculator = calculator;
      attribute.defaultValue = defaultValue;
      attribute.name = attr;
      attribute.propertyName = propertyName;
      attribute.property = property;
      attribute.defaultUnit = defaultUnit;
      attribute.parse = function(value, ignoreRelative) {
        return this.calculator.parse( value, this.defaultValue, ignoreRelative );
      };
      attribute.cloneDefault = function() {
        return this.calculator.clone( this.defaultValue );
      };
    }

    return attribute;
  }

});

var browser =
{
  IE: (function() {
    if (!(window.ActiveXObject || this.ActiveXObject) && ("ActiveXObject" in window || "ActiveXObject" in this)) { return 11; }
    if (!document.all) { return false; }
    if (!document.compatMode) { return 5; }
    if (!(window.XMLHttpRequest || this.XMLHttpRequest)) { return 6; }
    if (!document.querySelector) { return 7; }
    if (!document.addEventListener) { return 8; }
    if (!(window.atob || this.atob)) { return 9; }
    return 10;
  })()
};

var Matrix = {

  identity: function() {
    return {
      m11: 1.0, m12: 0.0, m21: 0.0, m22: 1.0
    };
  },

  multiply: function(a, b) {
    return {
      m11: (a.m11 * b.m11) + (a.m12 * b.m21),
      m22: (a.m21 * b.m12) + (a.m22 * b.m22),
      m21: (a.m21 * b.m11) + (a.m22 * b.m21),
      m12: (a.m11 * b.m12) + (a.m12 * b.m22)
    };
  },

  rotate: function(radians) {
    var cos = Math.cos( radians );
    var sin = Math.sin( radians );
    return {
      m11: cos,
      m12: -sin,
      m21: sin,
      m22: cos
    };
  },

  scale: function(scaleX, scaleY) {
    return {
      m11: scaleX,
      m12: 0.0,
      m21: 0.0,
      m22: scaleY
    };
  },

  skew: function(skewX, skewY) {
    return {
      m11: 1.0,
      m12: Math.tan( skewX ),
      m21: Math.tan( skewY ),
      m22: 1.0
    };
  },

  transform: function(matrix, x, y) {
    return {
      x: matrix.m11 * x + matrix.m12 * y,
      y: matrix.m21 * x + matrix.m22 * y
    };
  },

  adjustment: function(matrix, w, h) {
    var x0 = w * matrix.m11;
    var x1 = h * matrix.m12;
    var x2 = w * matrix.m11 + h * matrix.m12;
    var xmin = Math.min( x0, Math.min( x1, Math.min( x2, 0 ) ) );
    var xmax = Math.max( x0, Math.max( x1, Math.max( x2, 0 ) ) );

    var y0 = w * matrix.m21;
    var y1 = h * matrix.m22;
    var y2 = w * matrix.m21 + h * matrix.m22;
    var ymin = Math.min( y0, Math.min( y1, Math.min( y2, 0 ) ) );
    var ymax = Math.max( y0, Math.max( y1, Math.max( y2, 0 ) ) );

    return {
      x: xmax - xmin,
      y: ymax - ymin
    };
  }

};

function concatenateStyle(anim, style, value)
{
  if ( isDefined( anim.styles[ style ] ) )
  {
    anim.styles[ style ] += ' ' + value;
  }
  else
  {
    anim.styles[ style ] = value;
  }
}

function setProperty(attr, property)
{
  var attribute = Attributes[ attr ];

  if ( isString( attribute.property ) || !isDefined( attribute.property ) )
  {
    attribute.property = property;
  }
  else
  {
    attribute.propertyName = property;
    attribute.property = $property( property );
  }
}

/* transform, blur, opacity, margin top & left IE <= 8 */
if ( browser.IE && browser.IE <= 8 )
{
  Properties.ieTransform =
  {
    presettings:
    {
      width: {
        savedAs: 'width',
        property: 'width',
        relativeTo: 'parentWidth',
        defaultProperty: 'offsetWidth',
        toUnit: 'px'
      },
      height: {
        savedAs: 'height',
        property: 'height',
        relativeTo: 'parentHeight',
        defaultProperty: 'offsetHeight',
        toUnit: 'px'
      },
      rotate: {
        savedAs: 'rotate',
        property: 'rotate',
        toUnit: 'rad'
      },
      rotate3d: {
        savedAs: 'rotate',
        property: 'rotate3d',
        subproperty: 'angle',
        toUnit: 'rad'
      },
      skewX: {
        savedAs: 'skewX',
        property: 'skewX',
        toUnit: 'rad'
      },
      skew2dX: {
        savedAs: 'skewX',
        property: 'skew',
        subproperty: 'x',
        toUnit: 'rad'/*,
        remove: true*/
      },
      skewY: {
        savedAs: 'skewY',
        property: 'skewY',
        toUnit: 'rad'
      },
      skew2dY: {
        savedAs: 'skewY',
        property: 'skew',
        subproperty: 'y',
        toUnit: 'rad'/*,
        remove: true*/
      },
      translateX: {
        savedAs: 'translateX',
        property: 'translateX',
        relativeTo: 'width',
        toUnit: 'px'
      },
      translate2dX: {
        savedAs: 'translateX',
        property: 'translate',
        subproperty: 'x',
        toUnit: 'px',
        relativeTo: 'width'
      },
      translate3dX: {
        savedAs: 'translateX',
        property: 'translate3d',
        subproperty: 'x',
        toUnit: 'px',
        relativeTo: 'width'/*,
        remove: true*/
      },
      translateY: {
        savedAs: 'translateY',
        property: 'translateY',
        relativeTo: 'height',
        toUnit: 'px'
      },
      translate2dY: {
        savedAs: 'translateY',
        property: 'translate',
        subproperty: 'y',
        toUnit: 'px',
        relativeTo: 'height'
      },
      translate3dY: {
        savedAs: 'translateY',
        property: 'translate3d',
        subproperty: 'y',
        toUnit: 'px',
        relativeTo: 'height'/*,
        remove: true*/
      },
      scaleX: {
        savedAs: 'scaleX',
        property: 'scaleX'
      },
      scale2dX: {
        savedAs: 'scaleX',
        property: 'scale',
        subproperty: 'x'
      },
      scale3dX: {
        savedAs: 'scaleX',
        property: 'scale3d',
        subproperty: 'x'/*,
        remove: true*/
      },
      scaleY: {
        savedAs: 'scaleY',
        property: 'scaleY'
      },
      scale2dY: {
        savedAs: 'scaleY',
        property: 'scale',
        subproperty: 'y'
      },
      scale3dY: {
        savedAs: 'scaleY',
        property: 'scale3d',
        subproperty: 'y'/*,
        remove: true*/
      },
      originX: {
        savedAs: 'originX',
        property: 'originX',
        toUnit: '%',
        relativeTo: 'width'
      },
      origin2dX: {
        savedAs: 'originX',
        property: 'origin',
        subproperty: 'x',
        toUnit: '%',
        relativeTo: 'width'/*,
        remove: true*/
      },
      originY: {
        savedAs: 'originY',
        property: 'originY',
        toUnit: '%',
        relativeTo: 'height'
      },
      origin2dY: {
        savedAs: 'originY',
        property: 'origin',
        subproperty: 'y',
        toUnit: '%',
        relativeTo: 'height'/*,
        remove: true*/
      },
      marginLeft: {
        savedAs: 'marginLeft',
        property: 'marginLeft',
        toUnit: 'px',
        relativeTo: 'parentWidth'
      },
      marginTop: {
        savedAs: 'marginTop',
        property: 'marginTop',
        toUnit: 'px',
        relativeTo: 'parentWidth'
      },
      blur: {
        savedAs: 'blur',
        property: 'blur',
        toUnit: 'px',
        relativeTo: 'parentWidth',
        remove: true
      },
      opacity: {
        savedAs: 'opacity',
        property: 'opacity',
        remove: true
      }
    },
    get: function(e, anim)
    {
      var settings = this.presettings;

      Properties.marginLeft.get( e, anim );
      Properties.marginTop.get( e, anim );

      this.getFramed( e, anim, settings.rotate );
      this.getFramed( e, anim, settings.rotate3d );
      this.getFramed( e, anim, settings.skewX );
      this.getFramed( e, anim, settings.skewY );
      this.getFramed( e, anim, settings.skew2dX );
      this.getFramed( e, anim, settings.skew2dY );
      this.getFramed( e, anim, settings.translateX );
      this.getFramed( e, anim, settings.translate2dX );
      this.getFramed( e, anim, settings.translate3dX );
      this.getFramed( e, anim, settings.translateY );
      this.getFramed( e, anim, settings.translate2dY );
      this.getFramed( e, anim, settings.translate3dY );
      this.getFramed( e, anim, settings.scaleX );
      this.getFramed( e, anim, settings.scale2dX );
      this.getFramed( e, anim, settings.scale3dX );
      this.getFramed( e, anim, settings.scaleY );
      this.getFramed( e, anim, settings.scale2dY );
      this.getFramed( e, anim, settings.scale3dY );
      this.getFramed( e, anim, settings.originX );
      this.getFramed( e, anim, settings.originY );
      this.getFramed( e, anim, settings.origin2dX );
      this.getFramed( e, anim, settings.origin2dY );

      this.getFramed( e, anim, settings.blur );
      this.getFramed( e, anim, settings.opacity );
    },
    getFramed: function(e, flash, def)
    {
      var anim = coalesce( flash.source, flash );
      var attr = def.property;
      var value = anim.frame[ attr ];

      if ( isBoolean( flash.animating[ attr ] ) && isDefined( value ) )
      {
        var fromUnit = def.toUnit;
        var toUnit = flash.units[ attr ];

        if ( def.subproperty )
        {
          var attribute = anim.getAttribute( attr );

          if ( !flash.frame[ attr ] )
          {
            flash.frame[ attr ] = attribute.cloneDefault();
          }

          var converted = value[ def.subproperty ];

          if ( fromUnit !== toUnit )
          {
            converted = $convert( e, converted + fromUnit, toUnit, def.relativeTo );
          }

          if ( converted !== false )
          {
            flash.frame[ attr ][ def.subproperty ] = converted;
            flash.animating[ attr ] = true;
          }
        }
        else
        {
          var converted = value;

          if ( fromUnit !== toUnit )
          {
            converted = $convert( e, converted + fromUnit, toUnit, def.relativeTo );
          }

          if ( converted !== false )
          {
            flash.frame[ attr ] = converted;
            flash.animating[ attr ] = true;
          }
        }
      }

      return ( flash.animating[ attr ] === true );
    },
    resolveRelativeTo: function(anim, relativeTo)
    {
      var cached = anim.cached[ relativeTo ];

      if ( isNumber( cached ) )
      {
        return cached * 0.01;
      }

      return relativeTo;
    },
    cacheConverted: function(e, anim, def)
    {
      var value = anim.frame[ def.property ];
      var canConvert = isDefined( value );

      if ( canConvert )
      {
        var valueUnit = anim.units[ def.property ];
        var actualValue = ( def.subproperty ? value[ def.subproperty ] : value );

        if ( valueUnit !== def.toUnit )
        {
          var united = actualValue + valueUnit;
          var relativeTo = this.resolveRelativeTo( anim, def.relativeTo );

          actualValue = $convert( e, united, def.toUnit, relativeTo );
        }

        if ( actualValue !== false )
        {
          anim.cached[ def.savedAs ] = actualValue;
        }
        else
        {
          canConvert = false;
        }
      }
      else if ( def.remove )
      {
        delete anim.cached[ def.savedAs ];
      }

      return canConvert;
    },
    cacheValue: function(e, anim, def)
    {
      var value = anim.frame[ def.property ];
      var hasValue = isDefined( value );

      if ( hasValue )
      {
        anim.cached[ def.savedAs ] = ( def.subproperty ? value[ def.subproperty ] : value );
      }
      else if ( def.remove )
      {
        delete anim.cached[ def.savedAs ];
      }

      return hasValue;
    },
    cacheDimension: function(e, anim, def)
    {
      if ( !this.cacheConverted( e, anim, def ) )
      {
        var computedValue = $style( e, def.property );

        anim.cached[ def.savedAs ] = $convert( e, computedValue, 'px', def.relativeTo ) || e[ def.defaultProperty ];
      }
    },
    cachedOrDefault: function(e, anim, value, attribute, toUnit, relativeTo)
    {
      if ( isDefined( value ) )
      {
        return value;
      }
      if ( attribute.defaultUnit === toUnit )
      {
        return attribute.defaultValue;
      }

      relativeTo = this.resolveRelativeTo( anim, relativeTo );

      return $convert( e, attribute.defaultValue + attribute.defaultUnit, toUnit, relativeTo );
    },
    preset: function(e, anim)
    {
      var cached = anim.cached;
      var settings = this.presettings;
      var attrs = Attributes;

      this.cacheDimension( e, anim, settings.width );

      this.cacheDimension( e, anim, settings.height );

      if ( !this.cacheConverted( e, anim, settings.rotate ) )
      {
        this.cacheConverted( e, anim, settings.rotate3d );
      }

      if ( !this.cacheConverted( e, anim, settings.skewX ) )
      {
        this.cacheConverted( e, anim, settings.skew2dX );
      }

      if ( !this.cacheConverted( e, anim, settings.skewY ) )
      {
        this.cacheConverted( e, anim, settings.skew2dY );
      }

      if ( isDefined( cached.skewX ) || isDefined( cached.skewY ) )
      {
        cached.skewX = this.cachedOrDefault( e, anim, cached.skewX, attrs.skewX, 'rad' );
        cached.skewY = this.cachedOrDefault( e, anim, cached.skewY, attrs.skewY, 'rad' );
      }

      if ( !this.cacheConverted( e, anim, settings.translateX ) )
      {
        if ( !this.cacheConverted( e, anim, settings.translate2dX ) )
        {
          this.cacheConverted( e, anim, settings.translate3dX );
        }
      }

      if ( !this.cacheConverted( e, anim, settings.translateY ) )
      {
        if ( !this.cacheConverted( e, anim, settings.translate2dY ) )
        {
          this.cacheConverted( e, anim, settings.translate3dY );
        }
      }

      if ( isDefined( cached.translateX ) || isDefined( cached.translateY ) )
      {
        cached.translateX = this.cachedOrDefault( e, anim, cached.translateX, attrs.translateX, 'px', 'width' );
        cached.translateY = this.cachedOrDefault( e, anim, cached.translateY, attrs.translateY, 'px', 'height' );
      }

      if ( !this.cacheValue( e, anim, settings.scaleX ) )
      {
        if ( !this.cacheValue( e, anim, settings.scale2dX ) )
        {
          this.cacheValue( e, anim, settings.scale3dX );
        }
      }

      if ( !this.cacheValue( e, anim, settings.scaleY ) )
      {
        if ( !this.cacheValue( e, anim, settings.scale2dY ) )
        {
          this.cacheValue( e, anim, settings.scale3dY );
        }
      }

      if ( !this.cacheConverted( e, anim, settings.originX ) )
      {
        this.cacheConverted( e, anim, settings.origin2dX );
      }

      if ( !this.cacheConverted( e, anim, settings.originY ) )
      {
        this.cacheConverted( e, anim, settings.origin2dY );
      }

      cached.originX = this.cachedOrDefault( e, anim, cached.originX, attrs.originX, '%', 'width' );
      cached.originY = this.cachedOrDefault( e, anim, cached.originY, attrs.originY, '%', 'height' );

      if ( !isDefined( cached.baseMarginLeft ) )
      {
        var margins = anim.get({
          marginLeft: 'px',
          marginTop: 'px'
        });

        cached.baseMarginLeft = margins.marginLeft || 0;
        cached.baseMarginTop = margins.marginTop || 0;
      }

      if ( this.cacheConverted( e, anim, settings.marginLeft ) )
      {
        cached.marginLeft -= cached.baseMarginLeft;
      }
      else
      {
        cached.marginLeft = 0;
      }

      if ( this.cacheConverted( e, anim, settings.marginTop ) )
      {
        cached.marginTop -= cached.baseMarginTop;
      }
      else
      {
        cached.marginTop = 0;
      }

      this.cacheValue( e, anim, settings.opacity );

      this.cacheConverted( e, anim, settings.blur );
    },
    unset: function(e, anim, attr)
    {
      if ( attr === true )
      {
        e.style.filter = '';
        e.style.marginLeft = (anim.cached.baseMarginLeft || 0) + 'px';
        e.style.marginTop = (anim.cached.baseMarginTop || 0) + 'px';

        anim.cached = {};
      }
    },
    set: function(e, anim)
    {
      var cached = anim.cached;
      var attrs = Attributes;
      var w = cached.width;
      var h = cached.height;
      var anchorX = cached.originX * 0.01;
      var anchorY = cached.originY * 0.01;
      var dx = 0;
      var dy = 0;
      var matrix = Matrix.identity();

      if ( isDefined( cached.scaleX ) || isDefined( cached.scaleY ) )
      {
        matrix = Matrix.multiply( matrix, Matrix.scale(
          coalesce( cached.scaleX, attrs.scaleX.defaultValue ),
          coalesce( cached.scaleY, attrs.scaleY.defaultValue )
        ));
      }

      if ( isDefined( cached.skewX ) )
      {
        matrix = Matrix.multiply( matrix, Matrix.skew( cached.skewX, cached.skewY ) );
      }

      if ( isDefined( cached.rotate ) )
      {
        matrix = Matrix.multiply( matrix, Matrix.rotate( cached.rotate ) );
      }

      if ( isDefined( cached.translateX ) )
      {
        dx += cached.translateX;
        dy += cached.translateY;
      }

      // Calculate the new size of the element based on the matrix. We need to
      // adjust by the difference because IE is special.
      var newSize = Matrix.adjustment( matrix, w, h );
      dx += (w - newSize.x) * 0.5;
      dy += (h - newSize.y) * 0.5;

      // Adjust for a non-centered transformation
      var hw = w * 0.5;
      var hh = h * 0.5;
      var origin = Matrix.transform( matrix, hw, hh );
      dx += (hw - origin.x) * (anchorX * 2 - 1);
      dy += (hh - origin.y) * (anchorY * 2 - 1);

      // If margin is already specified, add it to the new margin value.
      dx += cached.marginLeft + cached.baseMarginLeft;
      dy += cached.marginTop + cached.baseMarginTop;

      // Set the margin to account for a lack of translation.
      anim.styles.marginLeft = dx + 'px';
      anim.styles.marginTop = dy + 'px';

      // The array of filter operations
      var filters = [];

      // Transformations
      if ( matrix.m11 !== 1.0 || matrix.m12 !== 0.0 || matrix.m21 !== 0.0 || matrix.m22 !== 1.0 )
      {
        var transformFilter = 'progid:DXImageTransform.Microsoft.Matrix(SizingMethod=\'auto expand\'' +
          ', M11=' + matrix.m11 + ', M12=' + matrix.m12 +
          ', M21=' + matrix.m21 + ', M22=' + matrix.m22 + ')';

        filters.push( transformFilter );
      }

      // Opacity
      if ( isNumber( cached.opacity ) && !isNaN( cached.opacity )  )
      {
        var opacityFilter = 'progid:DXImageTransform.Microsoft.Alpha(Opacity=' + ( cached.opacity * 100 ) + ')';

        filters.push( opacityFilter );
      }

      // Blur
      if ( isNumber( cached.blur ) && !isNaN( cached.blur ) )
      {
        var blurFilter = 'progid:DXImageTransform.Microsoft.Blur(PixelRadius=\'' + ( cached.blur ) + 'px\')';

        filters.push( blurFilter );
      }

      // Set the filter properties!
      anim.styles.filter = filters.join( ' ' );

      // Force layout
      anim.styles.zoom = 1;
    }
  };

  var attributes = [
    'marginLeft', 'marginTop',
    'blur',
    'opacity',
    'origin', 'originX', 'originY',
    'translate', 'translateX', 'translateY', 'translateZ', 'translate3d',
    'rotate', 'rotate3d',
    'skew', 'skewX', 'skewY',
    'scale', 'scaleX', 'scaleY', 'scaleZ', 'scale3d'
  ];

  for ( var i = 0; i < attributes.length; i++ )
  {
    setProperty( attributes[ i ], 'ieTransform' );
  }
}

/* minWidth < IE 8 */
if ( browser.IE && browser.IE < 8 )
{
  Properties.minWidth.set = function(e, anim)
  {
    anim.styles.width = 'expression( this.scrollWidth \< ' + (anim.frame.minWidth + 1) + ' ? "' + anim.frame.minWidth + anim.units.minWidth + '" : "auto")';
  };
}

/* maxWidth < IE 8 */
if ( browser.IE && browser.IE < 8 )
{
  Properties.maxWidth.set = function(e, anim)
  {
    anim.styles.width = 'expression( this.scrollWidth > ' + (anim.frame.maxWidth - 1) + ' ? "' + anim.frame.maxWidth + anim.units.maxWidth + '" : "auto")';
  };
}

/* minHeight < IE 8 */
if ( browser.IE && browser.IE < 8 )
{
  Properties.minHeight.set = function(e, anim)
  {
    anim.styles.height = 'expression( this.scrollHeight \< ' + (anim.frame.minHeight + 1) + ' ? "' + anim.frame.minHeight + anim.units.minHeight + '" : "auto")';
  };
}
/* maxHeight < IE 8 */
if ( browser.IE && browser.IE < 8 )
{
  Properties.maxHeight.set = function(e, anim)
  {
    anim.styles.height = 'expression( this.scrollHeight > ' + (anim.frame.maxHeight - 1) + ' ? "' + anim.frame.maxHeight + anim.units.maxHeight + '" : "auto")';
  };
}



  // Register Factory
  anim8.Factories['default'] = anim8.Factories['dom'] = new FactoryDom();

  // Classes
  anim8.AnimatorDom = AnimatorDom;
  anim8.FactoryDom = FactoryDom;

  // Functions
  anim8.isElement = isElement;

  // Variables
  anim8.browser = browser;
  anim8.Matrix = Matrix;

  // Namespace
  anim8.dom = {
    Attributes:           Attributes,
    attribute:            $attribute,
    convert:              $convert,
    style:                $style,
    parseValue:           $parseValue,
    property:             $property,
    prefix:               $prefix,
    concatenateStyle:     concatenateStyle,
    setProperty:          setProperty,
    unset:                unset,
    factory:              factory,
    factoryDerivable:     factoryDerivable,
    factoryColor:         factoryColor
  };

  return anim8;

}));