1/* 2 * transform: A jQuery cssHooks adding cross-browser 2d transform capabilities to $.fn.css() and $.fn.animate() 3 * 4 * limitations: 5 * - requires jQuery 1.4.3+ 6 * - Should you use the *translate* property, then your elements need to be absolutely positionned in a relatively positionned wrapper **or it will fail in IE678**. 7 * - transformOrigin is not accessible 8 * 9 * latest version and complete README available on Github: 10 * https://github.com/louisremi/jquery.transform.js 11 * 12 * Copyright 2011 @louis_remi 13 * Licensed under the MIT license. 14 * 15 * This saved you an hour of work? 16 * Send me music http://www.amazon.co.uk/wishlist/HNTU0468LQON 17 * 18 */ 19(function( $, window, document, Math, undefined ) { 20 21/* 22 * Feature tests and global variables 23 */ 24var div = document.createElement("div"), 25 divStyle = div.style, 26 suffix = "Transform", 27 testProperties = [ 28 "O" + suffix, 29 "ms" + suffix, 30 "Webkit" + suffix, 31 "Moz" + suffix 32 ], 33 i = testProperties.length, 34 supportProperty, 35 supportMatrixFilter, 36 supportFloat32Array = "Float32Array" in window, 37 propertyHook, 38 propertyGet, 39 rMatrix = /Matrix([^)]*)/, 40 rAffine = /^\s*matrix\(\s*1\s*,\s*0\s*,\s*0\s*,\s*1\s*(?:,\s*0(?:px)?\s*){2}\)\s*$/, 41 _transform = "transform", 42 _transformOrigin = "transformOrigin", 43 _translate = "translate", 44 _rotate = "rotate", 45 _scale = "scale", 46 _skew = "skew", 47 _matrix = "matrix"; 48 49// test different vendor prefixes of these properties 50while ( i-- ) { 51 if ( testProperties[i] in divStyle ) { 52 $.support[_transform] = supportProperty = testProperties[i]; 53 $.support[_transformOrigin] = supportProperty + "Origin"; 54 continue; 55 } 56} 57// IE678 alternative 58if ( !supportProperty ) { 59 $.support.matrixFilter = supportMatrixFilter = divStyle.filter === ""; 60} 61 62// px isn't the default unit of these properties 63$.cssNumber[_transform] = $.cssNumber[_transformOrigin] = true; 64 65/* 66 * fn.css() hooks 67 */ 68if ( supportProperty && supportProperty != _transform ) { 69 // Modern browsers can use jQuery.cssProps as a basic hook 70 $.cssProps[_transform] = supportProperty; 71 $.cssProps[_transformOrigin] = supportProperty + "Origin"; 72 73 // Firefox needs a complete hook because it stuffs matrix with "px" 74 if ( supportProperty == "Moz" + suffix ) { 75 propertyHook = { 76 get: function( elem, computed ) { 77 return (computed ? 78 // remove "px" from the computed matrix 79 $.css( elem, supportProperty ).split("px").join(""): 80 elem.style[supportProperty] 81 ); 82 }, 83 set: function( elem, value ) { 84 // add "px" to matrices 85 elem.style[supportProperty] = /matrix\([^)p]*\)/.test(value) ? 86 value.replace(/matrix((?:[^,]*,){4})([^,]*),([^)]*)/, _matrix+"$1$2px,$3px"): 87 value; 88 } 89 }; 90 /* Fix two jQuery bugs still present in 1.5.1 91 * - rupper is incompatible with IE9, see http://jqbug.com/8346 92 * - jQuery.css is not really jQuery.cssProps aware, see http://jqbug.com/8402 93 */ 94 } else if ( /^1\.[0-5](?:\.|$)/.test($.fn.jquery) ) { 95 propertyHook = { 96 get: function( elem, computed ) { 97 return (computed ? 98 $.css( elem, supportProperty.replace(/^ms/, "Ms") ): 99 elem.style[supportProperty] 100 ); 101 } 102 }; 103 } 104 /* TODO: leverage hardware acceleration of 3d transform in Webkit only 105 else if ( supportProperty == "Webkit" + suffix && support3dTransform ) { 106 propertyHook = { 107 set: function( elem, value ) { 108 elem.style[supportProperty] = 109 value.replace(); 110 } 111 } 112 }*/ 113 114} else if ( supportMatrixFilter ) { 115 propertyHook = { 116 get: function( elem, computed, asArray ) { 117 var elemStyle = ( computed && elem.currentStyle ? elem.currentStyle : elem.style ), 118 matrix, data; 119 120 if ( elemStyle && rMatrix.test( elemStyle.filter ) ) { 121 matrix = RegExp.$1.split(","); 122 matrix = [ 123 matrix[0].split("=")[1], 124 matrix[2].split("=")[1], 125 matrix[1].split("=")[1], 126 matrix[3].split("=")[1] 127 ]; 128 } else { 129 matrix = [1,0,0,1]; 130 } 131 132 if ( ! $.cssHooks[_transformOrigin] ) { 133 matrix[4] = elemStyle ? parseInt(elemStyle.left, 10) || 0 : 0; 134 matrix[5] = elemStyle ? parseInt(elemStyle.top, 10) || 0 : 0; 135 136 } else { 137 data = $._data( elem, "transformTranslate", undefined ); 138 matrix[4] = data ? data[0] : 0; 139 matrix[5] = data ? data[1] : 0; 140 } 141 142 return asArray ? matrix : _matrix+"(" + matrix + ")"; 143 }, 144 set: function( elem, value, animate ) { 145 var elemStyle = elem.style, 146 currentStyle, 147 Matrix, 148 filter, 149 centerOrigin; 150 151 if ( !animate ) { 152 elemStyle.zoom = 1; 153 } 154 155 value = matrix(value); 156 157 // rotate, scale and skew 158 Matrix = [ 159 "Matrix("+ 160 "M11="+value[0], 161 "M12="+value[2], 162 "M21="+value[1], 163 "M22="+value[3], 164 "SizingMethod='auto expand'" 165 ].join(); 166 filter = ( currentStyle = elem.currentStyle ) && currentStyle.filter || elemStyle.filter || ""; 167 168 elemStyle.filter = rMatrix.test(filter) ? 169 filter.replace(rMatrix, Matrix) : 170 filter + " progid:DXImageTransform.Microsoft." + Matrix + ")"; 171 172 if ( ! $.cssHooks[_transformOrigin] ) { 173 174 // center the transform origin, from pbakaus's Transformie http://github.com/pbakaus/transformie 175 if ( (centerOrigin = $.transform.centerOrigin) ) { 176 elemStyle[centerOrigin == "margin" ? "marginLeft" : "left"] = -(elem.offsetWidth/2) + (elem.clientWidth/2) + "px"; 177 elemStyle[centerOrigin == "margin" ? "marginTop" : "top"] = -(elem.offsetHeight/2) + (elem.clientHeight/2) + "px"; 178 } 179 180 // translate 181 // We assume that the elements are absolute positionned inside a relative positionned wrapper 182 elemStyle.left = value[4] + "px"; 183 elemStyle.top = value[5] + "px"; 184 185 } else { 186 $.cssHooks[_transformOrigin].set( elem, value ); 187 } 188 } 189 }; 190} 191// populate jQuery.cssHooks with the appropriate hook if necessary 192if ( propertyHook ) { 193 $.cssHooks[_transform] = propertyHook; 194} 195// we need a unique setter for the animation logic 196propertyGet = propertyHook && propertyHook.get || $.css; 197 198/* 199 * fn.animate() hooks 200 */ 201$.fx.step.transform = function( fx ) { 202 var elem = fx.elem, 203 start = fx.start, 204 end = fx.end, 205 pos = fx.pos, 206 transform = "", 207 precision = 1E5, 208 i, startVal, endVal, unit; 209 210 // fx.end and fx.start need to be converted to interpolation lists 211 if ( !start || typeof start === "string" ) { 212 213 // the following block can be commented out with jQuery 1.5.1+, see #7912 214 if ( !start ) { 215 start = propertyGet( elem, supportProperty ); 216 } 217 218 // force layout only once per animation 219 if ( supportMatrixFilter ) { 220 elem.style.zoom = 1; 221 } 222 223 // replace "+=" in relative animations (-= is meaningless with transforms) 224 end = end.split("+=").join(start); 225 226 // parse both transform to generate interpolation list of same length 227 $.extend( fx, interpolationList( start, end ) ); 228 start = fx.start; 229 end = fx.end; 230 } 231 232 i = start.length; 233 234 // interpolate functions of the list one by one 235 while ( i-- ) { 236 startVal = start[i]; 237 endVal = end[i]; 238 unit = +false; 239 240 switch ( startVal[0] ) { 241 242 case _translate: 243 unit = "px"; 244 case _scale: 245 unit || ( unit = ""); 246 247 transform = startVal[0] + "(" + 248 Math.round( (startVal[1][0] + (endVal[1][0] - startVal[1][0]) * pos) * precision ) / precision + unit +","+ 249 Math.round( (startVal[1][1] + (endVal[1][1] - startVal[1][1]) * pos) * precision ) / precision + unit + ")"+ 250 transform; 251 break; 252 253 case _skew + "X": 254 case _skew + "Y": 255 case _rotate: 256 transform = startVal[0] + "(" + 257 Math.round( (startVal[1] + (endVal[1] - startVal[1]) * pos) * precision ) / precision +"rad)"+ 258 transform; 259 break; 260 } 261 } 262 263 fx.origin && ( transform = fx.origin + transform ); 264 265 propertyHook && propertyHook.set ? 266 propertyHook.set( elem, transform, +true ): 267 elem.style[supportProperty] = transform; 268}; 269 270/* 271 * Utility functions 272 */ 273 274// turns a transform string into its "matrix(A,B,C,D,X,Y)" form (as an array, though) 275function matrix( transform ) { 276 transform = transform.split(")"); 277 var 278 trim = $.trim 279 , i = -1 280 // last element of the array is an empty string, get rid of it 281 , l = transform.length -1 282 , split, prop, val 283 , prev = supportFloat32Array ? new Float32Array(6) : [] 284 , curr = supportFloat32Array ? new Float32Array(6) : [] 285 , rslt = supportFloat32Array ? new Float32Array(6) : [1,0,0,1,0,0] 286 ; 287 288 prev[0] = prev[3] = rslt[0] = rslt[3] = 1; 289 prev[1] = prev[2] = prev[4] = prev[5] = 0; 290 291 // Loop through the transform properties, parse and multiply them 292 while ( ++i < l ) { 293 split = transform[i].split("("); 294 prop = trim(split[0]); 295 val = split[1]; 296 curr[0] = curr[3] = 1; 297 curr[1] = curr[2] = curr[4] = curr[5] = 0; 298 299 switch (prop) { 300 case _translate+"X": 301 curr[4] = parseInt(val, 10); 302 break; 303 304 case _translate+"Y": 305 curr[5] = parseInt(val, 10); 306 break; 307 308 case _translate: 309 val = val.split(","); 310 curr[4] = parseInt(val[0], 10); 311 curr[5] = parseInt(val[1] || 0, 10); 312 break; 313 314 case _rotate: 315 val = toRadian(val); 316 curr[0] = Math.cos(val); 317 curr[1] = Math.sin(val); 318 curr[2] = -Math.sin(val); 319 curr[3] = Math.cos(val); 320 break; 321 322 case _scale+"X": 323 curr[0] = +val; 324 break; 325 326 case _scale+"Y": 327 curr[3] = val; 328 break; 329 330 case _scale: 331 val = val.split(","); 332 curr[0] = val[0]; 333 curr[3] = val.length>1 ? val[1] : val[0]; 334 break; 335 336 case _skew+"X": 337 curr[2] = Math.tan(toRadian(val)); 338 break; 339 340 case _skew+"Y": 341 curr[1] = Math.tan(toRadian(val)); 342 break; 343 344 case _matrix: 345 val = val.split(","); 346 curr[0] = val[0]; 347 curr[1] = val[1]; 348 curr[2] = val[2]; 349 curr[3] = val[3]; 350 curr[4] = parseInt(val[4], 10); 351 curr[5] = parseInt(val[5], 10); 352 break; 353 } 354 355 // Matrix product (array in column-major order) 356 rslt[0] = prev[0] * curr[0] + prev[2] * curr[1]; 357 rslt[1] = prev[1] * curr[0] + prev[3] * curr[1]; 358 rslt[2] = prev[0] * curr[2] + prev[2] * curr[3]; 359 rslt[3] = prev[1] * curr[2] + prev[3] * curr[3]; 360 rslt[4] = prev[0] * curr[4] + prev[2] * curr[5] + prev[4]; 361 rslt[5] = prev[1] * curr[4] + prev[3] * curr[5] + prev[5]; 362 363 prev = [rslt[0],rslt[1],rslt[2],rslt[3],rslt[4],rslt[5]]; 364 } 365 return rslt; 366} 367 368// turns a matrix into its rotate, scale and skew components 369// algorithm from http://hg.mozilla.org/mozilla-central/file/7cb3e9795d04/layout/style/nsStyleAnimation.cpp 370function unmatrix(matrix) { 371 var 372 scaleX 373 , scaleY 374 , skew 375 , A = matrix[0] 376 , B = matrix[1] 377 , C = matrix[2] 378 , D = matrix[3] 379 ; 380 381 // Make sure matrix is not singular 382 if ( A * D - B * C ) { 383 // step (3) 384 scaleX = Math.sqrt( A * A + B * B ); 385 A /= scaleX; 386 B /= scaleX; 387 // step (4) 388 skew = A * C + B * D; 389 C -= A * skew; 390 D -= B * skew; 391 // step (5) 392 scaleY = Math.sqrt( C * C + D * D ); 393 C /= scaleY; 394 D /= scaleY; 395 skew /= scaleY; 396 // step (6) 397 if ( A * D < B * C ) { 398 A = -A; 399 B = -B; 400 skew = -skew; 401 scaleX = -scaleX; 402 } 403 404 // matrix is singular and cannot be interpolated 405 } else { 406 // In this case the elem shouldn't be rendered, hence scale == 0 407 scaleX = scaleY = skew = 0; 408 } 409 410 // The recomposition order is very important 411 // see http://hg.mozilla.org/mozilla-central/file/7cb3e9795d04/layout/style/nsStyleAnimation.cpp#l971 412 return [ 413 [_translate, [+matrix[4], +matrix[5]]], 414 [_rotate, Math.atan2(B, A)], 415 [_skew + "X", Math.atan(skew)], 416 [_scale, [scaleX, scaleY]] 417 ]; 418} 419 420// build the list of transform functions to interpolate 421// use the algorithm described at http://dev.w3.org/csswg/css3-2d-transforms/#animation 422function interpolationList( start, end ) { 423 var list = { 424 start: [], 425 end: [] 426 }, 427 i = -1, l, 428 currStart, currEnd, currType; 429 430 // get rid of affine transform matrix 431 ( start == "none" || isAffine( start ) ) && ( start = "" ); 432 ( end == "none" || isAffine( end ) ) && ( end = "" ); 433 434 // if end starts with the current computed style, this is a relative animation 435 // store computed style as the origin, remove it from start and end 436 if ( start && end && !end.indexOf("matrix") && toArray( start ).join() == toArray( end.split(")")[0] ).join() ) { 437 list.origin = start; 438 start = ""; 439 end = end.slice( end.indexOf(")") +1 ); 440 } 441 442 if ( !start && !end ) { return; } 443 444 // start or end are affine, or list of transform functions are identical 445 // => functions will be interpolated individually 446 if ( !start || !end || functionList(start) == functionList(end) ) { 447 448 start && ( start = start.split(")") ) && ( l = start.length ); 449 end && ( end = end.split(")") ) && ( l = end.length ); 450 451 while ( ++i < l-1 ) { 452 start[i] && ( currStart = start[i].split("(") ); 453 end[i] && ( currEnd = end[i].split("(") ); 454 currType = $.trim( ( currStart || currEnd )[0] ); 455 456 append( list.start, parseFunction( currType, currStart ? currStart[1] : 0 ) ); 457 append( list.end, parseFunction( currType, currEnd ? currEnd[1] : 0 ) ); 458 } 459 460 // otherwise, functions will be composed to a single matrix 461 } else { 462 list.start = unmatrix(matrix(start)); 463 list.end = unmatrix(matrix(end)) 464 } 465 466 return list; 467} 468 469function parseFunction( type, value ) { 470 var 471 // default value is 1 for scale, 0 otherwise 472 defaultValue = +(!type.indexOf(_scale)), 473 scaleX, 474 // remove X/Y from scaleX/Y & translateX/Y, not from skew 475 cat = type.replace( /e[XY]/, "e" ); 476 477 switch ( type ) { 478 case _translate+"Y": 479 case _scale+"Y": 480 481 value = [ 482 defaultValue, 483 value ? 484 parseFloat( value ): 485 defaultValue 486 ]; 487 break; 488 489 case _translate+"X": 490 case _translate: 491 case _scale+"X": 492 scaleX = 1; 493 case _scale: 494 495 value = value ? 496 ( value = value.split(",") ) && [ 497 parseFloat( value[0] ), 498 parseFloat( value.length>1 ? value[1] : type == _scale ? scaleX || value[0] : defaultValue+"" ) 499 ]: 500 [defaultValue, defaultValue]; 501 break; 502 503 case _skew+"X": 504 case _skew+"Y": 505 case _rotate: 506 value = value ? toRadian( value ) : 0; 507 break; 508 509 case _matrix: 510 return unmatrix( value ? toArray(value) : [1,0,0,1,0,0] ); 511 break; 512 } 513 514 return [[ cat, value ]]; 515} 516 517function isAffine( matrix ) { 518 return rAffine.test(matrix); 519} 520 521function functionList( transform ) { 522 return transform.replace(/(?:\([^)]*\))|\s/g, ""); 523} 524 525function append( arr1, arr2, value ) { 526 while ( value = arr2.shift() ) { 527 arr1.push( value ); 528 } 529} 530 531// converts an angle string in any unit to a radian Float 532function toRadian(value) { 533 return ~value.indexOf("deg") ? 534 parseInt(value,10) * (Math.PI * 2 / 360): 535 ~value.indexOf("grad") ? 536 parseInt(value,10) * (Math.PI/200): 537 parseFloat(value); 538} 539 540// Converts "matrix(A,B,C,D,X,Y)" to [A,B,C,D,X,Y] 541function toArray(matrix) { 542 // remove the unit of X and Y for Firefox 543 matrix = /([^,]*),([^,]*),([^,]*),([^,]*),([^,p]*)(?:px)?,([^)p]*)(?:px)?/.exec(matrix); 544 return [matrix[1], matrix[2], matrix[3], matrix[4], matrix[5], matrix[6]]; 545} 546 547$.transform = { 548 centerOrigin: "margin" 549}; 550 551})( jQuery, window, document, Math ); 552