/*! * jQuery 2d Transform v@VERSION * http://wiki.github.com/heygrady/transform/ * * Copyright 2010, Grady Kuhnline * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * Date: */ /////////////////////////////////////////////////////// // Transform /////////////////////////////////////////////////////// (function($, window, document, undefined) { /** * @var Regex identify the matrix filter in IE */ var rmatrix = /progid:DXImageTransform\.Microsoft\.Matrix\(.*?\)/, rfxnum = /^([\+\-]=)?([\d+.\-]+)(.*)$/, rperc = /%/; // Steal some code from Modernizr var m = document.createElement( 'modernizr' ), m_style = m.style; function stripUnits(arg) { return parseFloat(arg); } /** * Find the prefix that this browser uses */ function getVendorPrefix() { var property = { transformProperty : '', MozTransform : '-moz-', WebkitTransform : '-webkit-', OTransform : '-o-', msTransform : '-ms-' }; for (var p in property) { if (typeof m_style[p] != 'undefined') { return property[p]; } } return null; } function supportCssTransforms() { if (typeof(window.Modernizr) !== 'undefined') { return Modernizr.csstransforms; } var props = [ 'transformProperty', 'WebkitTransform', 'MozTransform', 'OTransform', 'msTransform' ]; for ( var i in props ) { if ( m_style[ props[i] ] !== undefined ) { return true; } } } // Capture some basic properties var vendorPrefix = getVendorPrefix(), transformProperty = vendorPrefix !== null ? vendorPrefix + 'transform' : false, transformOriginProperty = vendorPrefix !== null ? vendorPrefix + 'transform-origin' : false; // store support in the jQuery Support object $.support.csstransforms = supportCssTransforms(); // IE9 public preview 6 requires the DOM names if (vendorPrefix == '-ms-') { transformProperty = 'msTransform'; transformOriginProperty = 'msTransformOrigin'; } /** * Class for creating cross-browser transformations * @constructor */ $.extend({ transform: function(elem) { // Cache the transform object on the element itself elem.transform = this; /** * The element we're working with * @var jQueryCollection */ this.$elem = $(elem); /** * Remember the matrix we're applying to help the safeOuterLength func */ this.applyingMatrix = false; this.matrix = null; /** * Remember the css height and width to save time * This is only really used in IE * @var Number */ this.height = null; this.width = null; this.outerHeight = null; this.outerWidth = null; /** * We need to know the box-sizing in IE for building the outerHeight and outerWidth * @var string */ this.boxSizingValue = null; this.boxSizingProperty = null; this.attr = null; this.transformProperty = transformProperty; this.transformOriginProperty = transformOriginProperty; } }); $.extend($.transform, { /** * @var Array list of all valid transform functions */ funcs: ['matrix', 'origin', 'reflect', 'reflectX', 'reflectXY', 'reflectY', 'rotate', 'scale', 'scaleX', 'scaleY', 'skew', 'skewX', 'skewY', 'translate', 'translateX', 'translateY'] }); /** * Create Transform as a jQuery plugin * @param Object funcs * @param Object options */ $.fn.transform = function(funcs, options) { return this.each(function() { var t = this.transform || new $.transform(this); if (funcs) { t.exec(funcs, options); } }); }; $.transform.prototype = { /** * Applies all of the transformations * @param Object funcs * @param Object options * forceMatrix - uses the matrix in all browsers * preserve - tries to preserve the values from previous runs */ exec: function(funcs, options) { // extend options options = $.extend(true, { forceMatrix: false, preserve: false }, options); // preserve the funcs from the previous run this.attr = null; if (options.preserve) { funcs = $.extend(true, this.getAttrs(true, true), funcs); } else { funcs = $.extend(true, {}, funcs); // copy the object to prevent weirdness } // Record the custom attributes on the element itself this.setAttrs(funcs); // apply the funcs if ($.support.csstransforms && !options.forceMatrix) { // CSS3 is supported return this.execFuncs(funcs); } else if ($.browser.msie || ($.support.csstransforms && options.forceMatrix)) { // Internet Explorer or Forced matrix return this.execMatrix(funcs); } return false; }, /** * Applies all of the transformations as functions * @param Object funcs */ execFuncs: function(funcs) { var values = []; // construct a CSS string for (var func in funcs) { // handle origin separately if (func == 'origin') { this[func].apply(this, $.isArray(funcs[func]) ? funcs[func] : [funcs[func]]); } else if ($.inArray(func, $.transform.funcs) !== -1) { values.push(this.createTransformFunc(func, funcs[func])); } } this.$elem.css(transformProperty, values.join(' ')); return true; }, /** * Applies all of the transformations as a matrix * @param Object funcs */ execMatrix: function(funcs) { var matrix, tempMatrix, args; var elem = this.$elem[0], _this = this; function normalPixels(val, i) { if (rperc.test(val)) { // this really only applies to translation return parseFloat(val) / 100 * _this['safeOuter' + (i ? 'Height' : 'Width')](); } return toPx(elem, val); } var rtranslate = /translate[X|Y]?/, trans = []; for (var func in funcs) { switch ($.type(funcs[func])) { case 'array': args = funcs[func]; break; case 'string': args = $.map(funcs[func].split(','), $.trim); break; default: args = [funcs[func]]; } if ($.matrix[func]) { if ($.cssAngle[func]) { // normalize on degrees args = $.map(args, $.angle.toDegree); } else if (!$.cssNumber[func]) { // normalize to pixels args = $.map(args, normalPixels); } else { // strip units args = $.map(args, stripUnits); } tempMatrix = $.matrix[func].apply(this, args); if (rtranslate.test(func)) { //defer translation trans.push(tempMatrix); } else { matrix = matrix ? matrix.x(tempMatrix) : tempMatrix; } } else if (func == 'origin') { this[func].apply(this, args); } } // check that we have a matrix matrix = matrix || $.matrix.identity(); // Apply translation $.each(trans, function(i, val) { matrix = matrix.x(val); }); // pull out the relevant values var a = parseFloat(matrix.e(1,1).toFixed(6)), b = parseFloat(matrix.e(2,1).toFixed(6)), c = parseFloat(matrix.e(1,2).toFixed(6)), d = parseFloat(matrix.e(2,2).toFixed(6)), tx = matrix.rows === 3 ? parseFloat(matrix.e(1,3).toFixed(6)) : 0, ty = matrix.rows === 3 ? parseFloat(matrix.e(2,3).toFixed(6)) : 0; //apply the transform to the element if ($.support.csstransforms && vendorPrefix === '-moz-') { // -moz- this.$elem.css(transformProperty, 'matrix(' + a + ', ' + b + ', ' + c + ', ' + d + ', ' + tx + 'px, ' + ty + 'px)'); } else if ($.support.csstransforms) { // -webkit, -o-, w3c // NOTE: WebKit and Opera don't allow units on the translate variables this.$elem.css(transformProperty, 'matrix(' + a + ', ' + b + ', ' + c + ', ' + d + ', ' + tx + ', ' + ty + ')'); } else if ($.browser.msie) { // IE requires the special transform Filter //TODO: Use Nearest Neighbor during animation FilterType=\'nearest neighbor\' var filterType = ', FilterType=\'nearest neighbor\''; //bilinear var style = this.$elem[0].style; var matrixFilter = 'progid:DXImageTransform.Microsoft.Matrix(' + 'M11=' + a + ', M12=' + c + ', M21=' + b + ', M22=' + d + ', sizingMethod=\'auto expand\'' + filterType + ')'; var filter = style.filter || $.curCSS( this.$elem[0], "filter" ) || ""; style.filter = rmatrix.test(filter) ? filter.replace(rmatrix, matrixFilter) : filter ? filter + ' ' + matrixFilter : matrixFilter; // Let's know that we're applying post matrix fixes and the height/width will be static for a bit this.applyingMatrix = true; this.matrix = matrix; // IE can't set the origin or translate directly this.fixPosition(matrix, tx, ty); this.applyingMatrix = false; this.matrix = null; } return true; }, /** * Sets the transform-origin * This really needs to be percentages * @param Number x length * @param Number y length */ origin: function(x, y) { // use CSS in supported browsers if ($.support.csstransforms) { if (typeof y === 'undefined') { this.$elem.css(transformOriginProperty, x); } else { this.$elem.css(transformOriginProperty, x + ' ' + y); } return true; } // correct for keyword lengths switch (x) { case 'left': x = '0'; break; case 'right': x = '100%'; break; case 'center': // no break case undefined: x = '50%'; } switch (y) { case 'top': y = '0'; break; case 'bottom': y = '100%'; break; case 'center': // no break case undefined: y = '50%'; //TODO: does this work? } // store mixed values with units, assumed pixels this.setAttr('origin', [ rperc.test(x) ? x : toPx(this.$elem[0], x) + 'px', rperc.test(y) ? y : toPx(this.$elem[0], y) + 'px' ]); //console.log(this.getAttr('origin')); return true; }, /** * Create a function suitable for a CSS value * @param string func * @param Mixed value */ createTransformFunc: function(func, value) { if (func.substr(0, 7) === 'reflect') { // let's fake reflection, false value // falsey sets an identity matrix var m = value ? $.matrix[func]() : $.matrix.identity(); return 'matrix(' + m.e(1,1) + ', ' + m.e(2,1) + ', ' + m.e(1,2) + ', ' + m.e(2,2) + ', 0, 0)'; } //value = _correctUnits(func, value); if (func == 'matrix') { if (vendorPrefix === '-moz-') { value[4] = value[4] ? value[4] + 'px' : 0; value[5] = value[5] ? value[5] + 'px' : 0; } } return func + '(' + ($.isArray(value) ? value.join(', ') : value) + ')'; }, /** * @param Matrix matrix * @param Number tx * @param Number ty * @param Number height * @param Number width */ fixPosition: function(matrix, tx, ty, height, width) { // now we need to fix it! var calc = new $.matrix.calc(matrix, this.safeOuterHeight(), this.safeOuterWidth()), origin = this.getAttr('origin'); // mixed percentages and px // translate a 0, 0 origin to the current origin var offset = calc.originOffset(new $.matrix.V2( rperc.test(origin[0]) ? parseFloat(origin[0])/100*calc.outerWidth : parseFloat(origin[0]), rperc.test(origin[1]) ? parseFloat(origin[1])/100*calc.outerHeight : parseFloat(origin[1]) )); // IE glues the top-most and left-most pixels of the transformed object to top/left of the original object //TODO: This seems wrong in the calculations var sides = calc.sides(); // Protect against an item that is already positioned var cssPosition = this.$elem.css('position'); if (cssPosition == 'static') { cssPosition = 'relative'; } //TODO: if the element is already positioned, we should attempt to respect it (somehow) //NOTE: we could preserve our offset top and left in an attr on the elem var pos = {top: 0, left: 0}; // Approximates transform-origin, tx, and ty var css = { 'position': cssPosition, 'top': (offset.top + ty + sides.top + pos.top) + 'px', 'left': (offset.left + tx + sides.left + pos.left) + 'px', 'zoom': 1 }; this.$elem.css(css); } }; /** * Ensure that values have the appropriate units on them * @param string func * @param Mixed value */ function toPx(elem, val) { var parts = rfxnum.exec($.trim(val)); if (parts[3] && parts[3] !== 'px') { var prop = 'paddingBottom', orig = $.style( elem, prop ); $.style( elem, prop, val ); val = cur( elem, prop ); $.style( elem, prop, orig ); return val; } return parseFloat( val ); } function cur(elem, prop) { if ( elem[prop] != null && (!elem.style || elem.style[prop] == null) ) { return elem[ prop ]; } var r = parseFloat( $.css( elem, prop ) ); return r && r > -10000 ? r : 0; } })(jQuery, this, this.document); /////////////////////////////////////////////////////// // Safe Outer Length /////////////////////////////////////////////////////// (function($, window, document, undefined) { $.extend($.transform.prototype, { /** * @param void * @return Number */ safeOuterHeight: function() { return this.safeOuterLength('height'); }, /** * @param void * @return Number */ safeOuterWidth: function() { return this.safeOuterLength('width'); }, /** * Returns reliable outer dimensions for an object that may have been transformed. * Only use this if the matrix isn't handy * @param String dim height or width * @return Number */ safeOuterLength: function(dim) { var funcName = 'outer' + (dim == 'width' ? 'Width' : 'Height'); if (!$.support.csstransforms && $.browser.msie) { // make the variables more generic dim = dim == 'width' ? 'width' : 'height'; // if we're transforming and have a matrix; we can shortcut. // the true outerHeight is the transformed outerHeight divided by the ratio. // the ratio is equal to the height of a 1px by 1px box that has been transformed by the same matrix. if (this.applyingMatrix && !this[funcName] && this.matrix) { // calculate and return the correct size var calc = new $.matrix.calc(this.matrix, 1, 1), ratio = calc.offset(), length = this.$elem[funcName]() / ratio[dim]; this[funcName] = length; return length; } else if (this.applyingMatrix && this[funcName]) { // return the cached calculation return this[funcName]; } // map dimensions to box sides var side = { height: ['top', 'bottom'], width: ['left', 'right'] }; // setup some variables var elem = this.$elem[0], outerLen = parseFloat($.curCSS(elem, dim, true)), //TODO: this can be cached on animations that do not animate height/width boxSizingProp = this.boxSizingProperty, boxSizingValue = this.boxSizingValue; // IE6 && IE7 will never have a box-sizing property, so fake it if (!this.boxSizingProperty) { boxSizingProp = this.boxSizingProperty = _findBoxSizingProperty() || 'box-sizing'; boxSizingValue = this.boxSizingValue = this.$elem.css(boxSizingProp) || 'content-box'; } // return it immediately if we already know it if (this[funcName] && this[dim] == outerLen) { return this[funcName]; } else { this[dim] = outerLen; } // add in the padding and border if (boxSizingProp && (boxSizingValue == 'padding-box' || boxSizingValue == 'content-box')) { outerLen += parseFloat($.curCSS(elem, 'padding-' + side[dim][0], true)) || 0 + parseFloat($.curCSS(elem, 'padding-' + side[dim][1], true)) || 0; } if (boxSizingProp && boxSizingValue == 'content-box') { outerLen += parseFloat($.curCSS(elem, 'border-' + side[dim][0] + '-width', true)) || 0 + parseFloat($.curCSS(elem, 'border-' + side[dim][1] + '-width', true)) || 0; } // remember and return the outerHeight this[funcName] = outerLen; return outerLen; } return this.$elem[funcName](); } }); /** * Determine the correct property for checking the box-sizing property * @param void * @return string */ var _boxSizingProperty = null; function _findBoxSizingProperty () { if (_boxSizingProperty) { return _boxSizingProperty; } var property = { boxSizing : 'box-sizing', MozBoxSizing : '-moz-box-sizing', WebkitBoxSizing : '-webkit-box-sizing', OBoxSizing : '-o-box-sizing' }, elem = document.body; for (var p in property) { if (typeof elem.style[p] != 'undefined') { _boxSizingProperty = property[p]; return _boxSizingProperty; } } return null; } })(jQuery, this, this.document);