1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // License: Licensed under MIT license (see license.js) 4 // ========================================================================== 5 sc_require("views/view"); 6 7 /** @private 8 Properties that can be animated 9 (Hash for faster lookup) 10 */ 11 SC.ANIMATABLE_PROPERTIES = { 12 top: YES, 13 left: YES, 14 bottom: YES, 15 right: YES, 16 width: YES, 17 height: YES, 18 centerX: YES, 19 centerY: YES, 20 opacity: YES, 21 scale: YES, 22 rotate: YES, 23 rotateX: YES, 24 rotateY: YES, 25 rotateZ: YES 26 }; 27 28 29 /** 30 States that the view's layout can be set to if its animation is cancelled. 31 32 ### START 33 34 The previous layout of the view before calling animate. 35 36 For example, 37 38 myView.set('layout', { left: 0, top: 0, width: 100, bottom: 0 }); 39 myView.animate('left', 300, { duration: 1.5 }); 40 41 // later.. 42 myView.cancelAnimation(SC.LayoutState.START); 43 44 myView.get('layout'); // => { left: 0, top: 0, width: 100, bottom: 0 } 45 46 ### CURRENT 47 48 The current layout of the view while it is animating. 49 50 For example, 51 52 myView.set('layout', { left: 0, top: 0, width: 100, bottom: 0 }); 53 myView.animate('left', 300, { duration: 1.5 }); 54 55 // later.. 56 myView.cancelAnimation(SC.LayoutState.CURRENT); 57 myView.get('layout'); // => { left: 150, top: 0, width: 100, bottom: 0 } 58 59 ### END 60 61 The final layout of the view if the animation completed. 62 63 For example, 64 65 myView.set('layout', { left: 0, top: 0, width: 100, bottom: 0 }); 66 myView.animate('left', 300, { duration: 1.5 }); 67 68 // later.. 69 myView.cancelAnimation(SC.LayoutState.END); 70 myView.get('layout'); // => { left: 300, top: 0, width: 100, bottom: 0 } 71 72 @readonly 73 @enum {Number} 74 */ 75 SC.LayoutState = { 76 START: 1, 77 CURRENT: 2, 78 END: 3 79 }; 80 81 82 SC.View.reopen( 83 /** @scope SC.View.prototype */ { 84 85 /** @private Shared object used to avoid continually initializing/destroying objects. */ 86 _SC_DECOMPOSED_TRANSFORM_MAP: null, 87 88 /* @private Internal variable to store the active (i.e. applied) animations. */ 89 _activeAnimations: null, 90 91 /* @private Internal variable to store the count of active animations. */ 92 _activeAnimationsLength: null, 93 94 /* @private Internal variable to store the animation layout until the next run loop when it can be safely applied. */ 95 _animateLayout: null, 96 97 /* @private Internal variable to store the pending (i.e. not yet applied) animations. */ 98 _pendingAnimations: null, 99 100 /* @private Internal variable to store the previous layout for in case the animation is cancelled and meant to return to original point. */ 101 _prevLayout: null, 102 103 /** 104 Method protocol. 105 106 The method you provide to SC.View.prototype.animate should accept the 107 following parameter(s). 108 109 @name AnimateCallback 110 @function 111 @param {object} animationResult The result of the animation. 112 @param {boolean} animationResult.isCancelled Whether the animation was cancelled or not. 113 @param {event} [animationResult.evt] The transitionend event if it exists. 114 @param {SC.View} animationResult.view The animated view. 115 */ 116 117 /** 118 Animate a group of layout properties using CSS animations. 119 120 On supported platforms, this will apply the proper CSS transition style 121 in order to animate the view to the new layout. The properties object 122 should contain the names of the layout properties to animate with the new 123 layout values as values. 124 125 # Options 126 127 To control the transition, you must provide an options object that contains 128 at least the duration property and optionally the timing and delay 129 properties. The options properties are as follows: 130 131 - duration: The duration of the transition in seconds. The default value is 0.25. 132 133 - timing: The transition timing function. This may be a predefined CSS timing 134 function (e.g. 'linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out') or 135 it may be an array of values to make a cubic bezier (e.g. [0, 0, 0.58, 1.0]). 136 The default value is 'ease'. 137 138 ** 'linear' - Specifies a transition effect with the same speed from start to end (equivalent to cubic-bezier(0,0,1,1)) 139 ** 'ease' - Specifies a transition effect with a slow start, then fast, then end slowly (equivalent to cubic-bezier(0.25,0.1,0.25,1)) 140 ** 'ease-in' - Specifies a transition effect with a slow start (equivalent to cubic-bezier(0.42,0,1,1)) 141 ** 'ease-out' - Specifies a transition effect with a slow end (equivalent to cubic-bezier(0,0,0.58,1)) 142 ** 'ease-in-out' - Specifies a transition effect with a slow start and end (equivalent to cubic-bezier(0.42,0,0.58,1)) 143 ** 'cubic-bezier(n,n,n,n)' - Define your own values in the cubic-bezier function. Possible values are numeric values from 0 to 1 144 145 - delay: The transition delay in seconds. The default value is 0. 146 147 For example, 148 149 var myView = SC.View.create({ 150 layout: { top: 10, left: 10, width: 200, height: 400 } 151 }); 152 153 MyApp.mainPane.appendChild(myView); 154 155 // The view will animate to the new top & left values. 156 myView.animate( 157 { top: 200, left: 200 }, // properties 158 { duration: 0.75, timing: 'ease-out', delay: 0.5 } // options 159 ); 160 161 # Callbacks 162 163 To execute code when the transition completes, you may provide an optional 164 target and/or method. When the given group of transitions completes, 165 the callback function will be called once and passed an animationResult object with 166 properties containing the `event`, the `view` and a boolean `isCancelled` which 167 indicates if the animation had been cancelled or not. The format of the 168 target and method follows the standard SproutCore format, where if the 169 target is not given then the view itself will be the target. The 170 method can be a function or a property path to look up on the target. 171 172 For example, 173 174 // Passing a function for method. 175 myView.animate( 176 { top: 200, left: 200 }, // properties 177 { duration: 0.75 }, // options 178 function (animationResult) { // method 179 // `this` will be myView 180 } 181 ); 182 183 // Passing a target and method. 184 myView.animate( 185 { scale: 0, opacity: 0 }, // properties 186 { duration: 1.5 }, // options 187 MyApp.statechart, // target 188 'myViewDidShrink' // method 189 ); 190 191 The animate functions are intelligent in how they apply animations and 192 calling animate in a manner that would affect an ongoing animation (i.e. 193 animating left again while it is still in transition) will result in 194 the ongoing animation callback firing immediately with isCancelled set to 195 true and adjusting the transition to accomodate the new settings. 196 197 Note: This may not work if you are not using SproutCore for view layout, 198 which means you should not use `animate` if the view has `useStaticLayout` 199 set to true. 200 201 ## A note about Hardware Acceleration. 202 203 If a view has a fixed layout (i.e. view.get('isFixedLayout') == true) then 204 it will be eligible for hardware accelerated position transitions. Having a 205 fixed layout, simply means that the view has a fixed size (width and height) 206 and a fixed position (left and top). If the view is eligible for hardware 207 acceleration, it must also set wantsAcceleratedLayer to true for animate to 208 use hardware accelerated transitions when animating its position. 209 210 Occassionally, you may wish to animate a view with a non-fixed layout. To 211 do so with hardware acceleration, you should convert the view to a fixed 212 layout temporarily and then set it back to a flexible layout after the 213 transition is complete. 214 215 For example, 216 217 // Flexible layout. 218 myView.set('layout', { left: 0, top: 10, right: 0, bottom: 10 }); 219 220 // Prepare to animate by converting to a fixed layout. 221 frame = myView.get('frame'); 222 height = frame.height; 223 width = frame.width; 224 myView.adjust({ right: null, bottom: null, height: height, width: width }); 225 226 // Animate (will be hardware accelerated if myView.get('wantsAcceleratedLayout') is true). 227 myView.animate('left', width, { duration: 1 }, function () { 228 // Revert back to flexible layout. 229 myView.adjust({ right: -width, bottom: 10 }); 230 }); 231 232 @param {Object|String} properties Hash of property names with new layout values or a single property name. 233 @param {Number} [value] The new layout value for a single property (only provide if the first parameter is a String). 234 @param {Number|Object} Duration or hash of transition options. 235 @param {Object} [target=this] The target for the method. 236 @param {AnimateCallback|String} [method] The method to run when the transition completes. May be a function or a property path. 237 @returns {SC.View} receiver 238 */ 239 animate: function (key, value, options, target, method) { 240 var cur, curAnim, 241 valueDidChange = NO, 242 optionsDidChange = NO, 243 hash, layout, 244 optionsType, 245 pendingAnimations = this._pendingAnimations, 246 timing; 247 248 //@if(debug) 249 // Provide a little developer support if they are doing something that may not work. 250 if (this.get('useStaticLayout')) { 251 SC.warn("Developer Warning: SC.View:animate() was called on a view with useStaticLayout and may not work. If you are using CSS to layout the view (i.e. useStaticLayout: YES), then you should manage the animation manually."); 252 } 253 //@endif 254 255 // Normalize arguments 256 // TODO: Revisit .animate() arguments re: overloading. 257 if (typeof key === SC.T_STRING) { 258 hash = {}; 259 hash[key] = value; 260 } else { 261 method = target; 262 target = options; 263 options = value; 264 hash = key; 265 } 266 267 optionsType = SC.typeOf(options); 268 if (optionsType === SC.T_NUMBER) { 269 options = { duration: options }; 270 } else if (optionsType !== SC.T_HASH) { 271 throw new Error("Must provide options hash!"); 272 } 273 274 if (options.callback) { 275 method = options.callback; 276 delete options.callback; 277 } 278 279 // Callback. We need to keep the callback for each group of animations separate. 280 if (method === undefined) { 281 method = target; 282 target = this; 283 } 284 285 // Support `null` being passed in for the target, rather than dropping the argument. 286 if (!target) target = this; 287 288 if (method) { 289 if (typeof method === "string") method = target[method]; 290 options.target = target; 291 options.method = method; 292 } 293 294 // In the case of zero duration, just adjust and call the callback. 295 if (options.duration === 0) { 296 this.invokeNext(function () { 297 this.adjust(hash); 298 this.runAnimationCallback(options, null, false); 299 }); 300 return this; 301 } 302 303 // In the case that the view is not in the standard visible state, adjust instead of animate. 304 if (!this.get('isVisibleInWindow')) { 305 this.invokeNext(function () { 306 this.adjust(hash); 307 this.runAnimationCallback(options, null); 308 // Note: we may need to find a way to alert the callback that the animation was successful 309 // but instantaneous. 310 }); 311 return this; 312 } 313 314 // Timing function 315 timing = options.timing; 316 if (timing) { 317 if (typeof timing !== SC.T_STRING) { 318 options.timing = "cubic-bezier(" + timing[0] + ", " + timing[1] + ", " + 319 timing[2] + ", " + timing[3] + ")"; 320 } // else leave as is (assume proper CSS timing String) 321 } else { 322 options.timing = 'ease'; 323 } 324 325 // Delay 326 if (SC.none(options.delay)) { options.delay = 0; } 327 328 // Get the layout (may be a previous layout already animating). 329 if (!this._prevLayout) { 330 this._prevLayout = SC.clone(this.get('explicitLayout')); 331 } 332 333 if (!pendingAnimations) { pendingAnimations = this._pendingAnimations = {}; } 334 335 // Get the layout (may be a partially adjusted one already queued up). 336 layout = this._animateLayout || SC.clone(this.get('explicitLayout')); 337 338 // Handle old style rotation. 339 if (!SC.none(hash.rotate)) { 340 //@if(debug) 341 SC.Logger.warn('Developer Warning: Please animate rotateZ instead of rotate.'); 342 //@endif 343 if (SC.none(hash.rotateZ)) { 344 hash.rotateZ = hash.rotate; 345 } 346 delete hash.rotate; 347 } 348 349 // Go through the new animated properties and check for conflicts with 350 // previous calls to animate and changes to the current layout. 351 for (var property in hash) { 352 // Fast path. 353 if (!hash.hasOwnProperty(property) || !SC.ANIMATABLE_PROPERTIES[property]) { 354 355 //@if(debug) 356 if (!SC.ANIMATABLE_PROPERTIES[property]) { 357 SC.warn("Developer Warning: The property `%@` is not animatable using SC.View:animate().".fmt(property)); 358 } 359 //@endif 360 continue; 361 } 362 363 value = hash[property]; 364 cur = layout[property]; 365 curAnim = pendingAnimations[property]; 366 367 if (SC.none(value)) { throw new Error("Can only animate to an actual value!"); } 368 369 // If the new adjustment changes the previous adjustment's options before 370 // it has rendered, overwrite the previous adjustment. 371 if (curAnim && (curAnim.duration !== options.duration || 372 curAnim.timing !== options.timing || 373 curAnim.delay !== options.delay)) { 374 optionsDidChange = YES; 375 this.runAnimationCallback(curAnim, null, YES); 376 } 377 378 if (cur !== value || optionsDidChange) { 379 valueDidChange = YES; 380 layout[property] = value; 381 382 // Always update the animate hash to the newest options which may have been altered before this was applied. 383 pendingAnimations[property] = options; 384 } 385 } 386 387 // Only animate to new values. 388 if (valueDidChange) { 389 // When animating height or width with centerX or centerY, we need to animate the margin property also to get a smooth change. 390 if (!SC.none(pendingAnimations.height) && !SC.none(layout.centerY) && SC.none(pendingAnimations.centerY)) { 391 // Don't animate less than 2px difference b/c the margin-top value won't differ. 392 if (Math.abs(hash.height - this.get('layout').height) >= 2) { 393 pendingAnimations.centerY = options; 394 } 395 } 396 397 if (!SC.none(pendingAnimations.width) && !SC.none(layout.centerX) && SC.none(pendingAnimations.centerX)) { 398 // Don't animate less than 2px difference b/c the margin-left value won't differ. 399 if (Math.abs(hash.width - this.get('layout').width) >= 2) { 400 pendingAnimations.centerX = options; 401 } 402 } 403 404 this._animateLayout = layout; 405 406 // Always run the animation asynchronously so that the original layout is guaranteed to be applied to the DOM. 407 this.invokeNext('_animate'); 408 } else if (!optionsDidChange) { 409 this.invokeNext(function () { 410 this.runAnimationCallback(options, null, false); 411 }); 412 } 413 414 return this; 415 }, 416 417 /** @private */ 418 _animate: function () { 419 // Check for _animateLayout. If an invokeNext call to animate *this* occurs 420 // while flushing the invokeNext queue *before* this method runs, an extra 421 // call to _animate will run. Has unit test. 422 var animationLayout = this._animateLayout; 423 if (animationLayout) { 424 this.willRenderAnimations(); 425 426 // Clear the layout cache value first so that it is not present when layout changes next. 427 this._animateLayout = null; 428 429 // Apply the animation layout. 430 this.set('layout', animationLayout); 431 432 // Route. 433 if (this.get('viewState') === SC.CoreView.ATTACHED_SHOWN) { 434 this.set('viewState', SC.CoreView.ATTACHED_SHOWN_ANIMATING); 435 } 436 } 437 }, 438 439 /** @private 440 Animates through the given frames. 441 442 @param {Array} frames The array of frame objects. 443 @param {AnimateCallback} callback The callback function to call when the final frame is done animating. 444 @param {Number} initialDelay The delay before the first frame begins animating. 445 @returns {SC.View} receiver 446 */ 447 // TODO: Do this using CSS animations instead. 448 _animateFrames: function (frames, callback, initialDelay, _sc_frameCount) { 449 // Normalize the private argument `_sc_frameCount`. 450 if (SC.none(_sc_frameCount)) { _sc_frameCount = 0; } 451 452 var frame = frames[_sc_frameCount]; 453 454 this.animate(frame.value, { 455 delay: initialDelay, 456 duration: frame.duration, 457 timing: frame.timing 458 }, function (data) { 459 _sc_frameCount += 1; 460 461 // Keep iterating while frames exist and the animations weren't cancelled. 462 if (!data.isCancelled && _sc_frameCount < frames.length) { 463 // Only delay on the first animation. Increase count to the next frame. 464 this._animateFrames(frames, callback, 0, _sc_frameCount); 465 } else { 466 // Done. 467 if (callback) callback(data); 468 } 469 }); 470 471 return this; 472 }, 473 474 /** 475 Cancels the animation, adjusting the view's layout immediately to one of 476 three values depending on the `layoutState` parameter. 477 478 If no `layoutState` is given or if SC.LayoutState.END is given, the view 479 will be adjusted to its final layout. If SC.LayoutState.START is given, 480 the view will be adjusted back to its initial layout and if 481 SC.LayoutState.CURRENT is given, the view will stop at its current layout 482 value, which will be some transient value between the start and end values. 483 484 Note: The animation callbacks will be called with the animationResult object's 485 isCancelled property set to YES. 486 487 @param {SC.LayoutState} [layoutState=SC.LayoutState.END] The layout to immediately adjust the view to. 488 @returns {SC.View} this 489 */ 490 cancelAnimation: function (layoutState) { 491 var activeAnimations = this._activeAnimations, 492 pendingAnimations = this._pendingAnimations, 493 animation, 494 key, 495 layout, 496 didCancel = NO; 497 498 switch (layoutState) { 499 case SC.LayoutState.START: 500 // Revert back to the start layout. 501 layout = this._prevLayout; 502 break; 503 case SC.LayoutState.CURRENT: 504 // Stop at the current layout. 505 layout = this.get('liveAdjustments'); 506 break; 507 default: 508 layout = this._animateLayout; 509 } 510 511 // Route. 512 if (this.get('viewState') === SC.CoreView.ATTACHED_SHOWN_ANIMATING) { 513 this.set('viewState', SC.CoreView.ATTACHED_SHOWN); 514 } 515 516 // Immediately remove the pending animations while calling the callbacks. 517 for (key in pendingAnimations) { 518 animation = pendingAnimations[key]; 519 didCancel = YES; 520 521 // Update the animation hash. Do this first, so callbacks can check for active animations. 522 delete pendingAnimations[key]; 523 524 // Run the callback. 525 this.runAnimationCallback(animation, null, YES); 526 } 527 528 // Immediately remove the animation styles while calling the callbacks. 529 for (key in activeAnimations) { 530 animation = activeAnimations[key]; 531 didCancel = YES; 532 533 // Update the animation hash. Do this first, so callbacks can check for active animations. 534 delete activeAnimations[key]; 535 536 // Remove the animation style without triggering a layout change. 537 this.removeAnimationFromLayout(key, YES); 538 539 // Run the callback. 540 this.runAnimationCallback(animation, null, YES); 541 } 542 543 // Adjust to final position. 544 if (didCancel && !!layout) { 545 this.set('layout', layout); 546 } 547 548 // Clean up. 549 this._prevLayout = this._activeAnimations = this._pendingAnimations = this._animateLayout = null; 550 551 return this; 552 }, 553 554 /** @private 555 This method is called after the layout style is applied to the layer. If 556 the platform didn't support CSS transitions, the callbacks will be fired 557 immediately and the animations removed from the queue. 558 */ 559 didRenderAnimations: function () { 560 // Transitions not supported or the document is not visible. 561 if (!SC.platform.supportsCSSTransitions || document.hidden) { 562 var pendingAnimations = this._pendingAnimations; 563 564 for (var key in pendingAnimations) { 565 this.removeAnimationFromLayout(key, NO); 566 this.runAnimationCallback(pendingAnimations[key], null, NO); 567 } 568 569 // Route. 570 if (this.get('viewState') === SC.CoreView.ATTACHED_SHOWN_ANIMATING) { 571 this.set('viewState', SC.CoreView.ATTACHED_SHOWN); 572 } 573 574 // Reset the placeholder variables now that the layout style has been applied. 575 this._activeAnimations = this._pendingAnimations = null; 576 } 577 }, 578 579 /** @private Decompose a transformation matrix. */ 580 // TODO: Add skew support 581 _sc_decompose3DTransformMatrix: function (matrix, expectsScale) { 582 var ret = SC.View._SC_DECOMPOSED_TRANSFORM_MAP, // Shared object used to avoid continually initializing/destroying 583 toDegrees = 180 / Math.PI; 584 // determinant; 585 586 // Create the decomposition map once. Note: This is a shared object, all properties must be overwritten each time. 587 if (!ret) { ret = SC.View._SC_DECOMPOSED_TRANSFORM_MAP = {}; } 588 589 // Calculate the scale. 590 if (expectsScale) { 591 ret.scaleX = Math.sqrt((matrix.m11 * matrix.m11) + (matrix.m12 * matrix.m12) + (matrix.m13 * matrix.m13)); 592 // if (matrix.m11 < 0) ret.scaleX = ret.scaleX * -1; 593 ret.scaleY = Math.sqrt((matrix.m21 * matrix.m21) + (matrix.m22 * matrix.m22) + (matrix.m23 * matrix.m23)); 594 ret.scaleZ = Math.sqrt((matrix.m31 * matrix.m31) + (matrix.m32 * matrix.m32) + (matrix.m33 * matrix.m33)); 595 596 // Decompose scale from the matrix. 597 matrix = matrix.scale(1 / ret.scaleX, 1 / ret.scaleY, 1 / ret.scaleZ); 598 } else { 599 ret.scaleX = 1; 600 ret.scaleY = 1; 601 ret.scaleZ = 1; 602 } 603 604 // console.log("scales: %@, %@, %@".fmt(ret.scaleX, ret.scaleY, ret.scaleZ)); 605 606 // Find the 3 Euler angles. Note the order applied using SC.CSS_TRANSFORM_NAMES in layout_style.js. 607 ret.rotateZ = -Math.atan2(matrix.m21, matrix.m11) * toDegrees; // Between -180° and 180° 608 // ret.rotateY = Math.atan2(-matrix.m31, Math.sqrt((matrix.m32 * matrix.m32) + (matrix.m33 * matrix.m33))) * toDegrees; // Between -90° and 90° 609 // ret.rotateX = Math.atan2(matrix.m32, matrix.m33) * toDegrees; // Between -180° and 180° 610 611 // console.log("rotations: %@, %@, %@".fmt(ret.rotateX, ret.rotateY, ret.rotateZ)); 612 613 // if (ret.rotateX < 0) { ret.rotateX = 360 + ret.rotateX; } // Convert to 0° to 360° 614 // if (ret.rotateY < 0) { ret.rotateY = 180 + ret.rotateY; } // Convert to 0° to 180° 615 if (ret.rotateZ < 0) { ret.rotateZ = 360 + ret.rotateZ; } // Convert to 0° to 360° 616 617 // Pull out the translate values directly. 618 ret.translateX = matrix.m41; 619 ret.translateY = matrix.m42; 620 ret.translateZ = matrix.m43; 621 622 // console.log("translations: %@, %@, %@".fmt(ret.translateX, ret.translateY, ret.translateZ)); 623 624 return ret; 625 }, 626 627 /** @private Replace scientific E notation values with fixed decimal values. */ 628 _sc_removeENotationFromMatrixString: function (matrixString) { 629 var components, 630 numbers, 631 ret; 632 633 components = matrixString.split(/\(|\)/); 634 numbers = components[1].split(','); 635 for (var i = 0, len = numbers.length; i < len; i++) { 636 var number = numbers[i]; 637 638 // Transform E notation into fixed decimal (20 is maximum allowed). 639 if (number.indexOf('e') > 0) { 640 numbers[i] = window.parseFloat(number).toFixed(20); 641 } 642 } 643 644 ret = components[0] + '(' + numbers.join(', ') + ')'; 645 646 return ret; 647 }, 648 649 /** @private 650 Returns the live values of the properties being animated on a view while it 651 is animating. Getting the layout of the view after a call to animate will 652 include the final values, some of which will not be the same as what they 653 are while the animation is in progress. 654 655 Depending on the property being animated, determining the actual value can 656 be quite difficult. For instance, accelerated views will animate certain 657 properties using a browser specific CSS transition on a CSS transform and 658 the current value may be a CSSMatrix that needs to be mapped back to a 659 regular layout format. 660 661 This property is used by cancelAnimation() to stop the animation in its 662 current place. 663 664 PRIVATE - because we may want to rename this function and change its output 665 666 @returns {Object} 667 */ 668 liveAdjustments: function () { 669 var activeAnimations = this._activeAnimations, 670 el = this.get('layer'), 671 ret = {}, 672 transformKey = SC.browser.experimentalCSSNameFor('transform'); 673 674 if (activeAnimations) { 675 for (var key in activeAnimations) { 676 var value = document.defaultView.getComputedStyle(el)[key]; 677 678 // If a transform is being transitioned, decompose the matrices. 679 if (key === transformKey) { 680 var CSSMatrixClass = SC.browser.experimentalNameFor(window, 'CSSMatrix'), 681 matrix; 682 683 if (CSSMatrixClass !== SC.UNSUPPORTED) { 684 685 // Convert scientific E number representations to fixed numbers. 686 // In WebKit at least, these throw exceptions when used to generate the matrix. To test, 687 // paste the following in a browser console: 688 // new WebKitCSSMatrix('matrix(-1, 1.22464679914735e-16, -1.22464679914735e-16, -1, 0, 0)') 689 value = this._sc_removeENotationFromMatrixString(value); 690 matrix = new window[CSSMatrixClass](value); 691 692 /* jshint eqnull:true */ 693 var layout = this.get('layout'), 694 scaleLayout = layout.scale, 695 expectsScale = scaleLayout != null, 696 decomposition = this._sc_decompose3DTransformMatrix(matrix, expectsScale); 697 698 // The rotation decompositions aren't working properly, ignore them. 699 // Set rotateX. 700 // if (layout.rotateX != null) { 701 // ret.rotateX = decomposition.rotateX; 702 // } 703 704 // // Set rotateY. 705 // if (layout.rotateY != null) { 706 // ret.rotateY = decomposition.rotateY; 707 // } 708 709 // Set rotateZ. 710 if (layout.rotateZ != null) { 711 ret.rotateZ = decomposition.rotateZ; 712 } 713 714 // Set scale. 715 if (expectsScale) { 716 // If the scale was set in the layout as an Array, return it as an Array. 717 if (SC.typeOf(scaleLayout) === SC.T_ARRAY) { 718 ret.scale = [decomposition.scaleX, decomposition.scaleY]; 719 720 // If the scale was set in the layout as an Object, return it as an Object. 721 } else if (SC.typeOf(scaleLayout) === SC.T_HASH) { 722 ret.scale = { x: decomposition.scaleX, y: decomposition.scaleY }; 723 724 // Return it as a single value. 725 } else { 726 ret.scale = decomposition.scaleX; 727 } 728 } 729 730 // Set top & left. 731 if (this.get('hasAcceleratedLayer')) { 732 ret.left = decomposition.translateX; 733 ret.top = decomposition.translateY; 734 } 735 } else { 736 matrix = value.match(/^matrix\((.*)\)$/)[1].split(/,\s*/); 737 // If the view has translated position, retrieve translateX & translateY. 738 if (matrix && this.get('hasAcceleratedLayer')) { 739 ret.left = parseInt(matrix[4], 10); 740 ret.top = parseInt(matrix[5], 10); 741 } 742 } 743 744 // Determine the current style. 745 } else { 746 value = window.parseFloat(value, 10); 747 748 // Account for centerX & centerY animations (margin-left & margin-top). 749 if (key === 'centerX') { 750 value = value + parseInt(document.defaultView.getComputedStyle(el).width, 10) / 2; // Use the actual width. 751 } else if (key === 'centerY') { 752 value = value + parseInt(document.defaultView.getComputedStyle(el).height, 10) / 2; // Use the actual height. 753 } 754 755 ret[key] = value; 756 } 757 } 758 } 759 760 return ret; 761 }.property(), 762 763 /** @private Removes the animation CSS from the layer style. */ 764 removeAnimationFromLayout: function (propertyName, shouldUpdateStyle) { 765 var activeAnimations = this._activeAnimations, 766 layer = this.get('layer'); 767 768 if (!!layer && shouldUpdateStyle) { 769 var updatedCSS = []; 770 771 // Calculate the transition CSS that should remain. 772 for (var key in activeAnimations) { 773 if (key !== propertyName) { 774 updatedCSS.push(activeAnimations[key].css); 775 } 776 } 777 778 layer.style[SC.browser.experimentalStyleNameFor('transition')] = updatedCSS.join(', '); 779 } 780 }, 781 782 /** @deprecated 783 Resets animation, stopping all existing animations. 784 */ 785 resetAnimation: function () { 786 //@if(debug) 787 // Reset gives the connotation that the animation would go back to the start layout, but that is not the case. 788 SC.warn('Developer Warning: resetAnimation() has been renamed to cancelAnimation(). Please rename all calls to resetAnimation() with cancelAnimation().'); 789 //@endif 790 791 return this.cancelAnimation(); 792 }, 793 794 /** @private */ 795 runAnimationCallback: function (animation, evt, cancelled) { 796 var method = animation.method, 797 target = animation.target; 798 799 if (method) { 800 // We're using invokeNext so we don't trigger any layout changes from 801 // the callback until the current layout is updated. 802 // this.invokeNext(function () { 803 method.call(target, { event: evt, view: this, isCancelled: cancelled }); 804 // }, this); 805 806 // Always clear the method from the hash to prevent it being called 807 // multiple times for animations in the group. 808 delete animation.method; 809 delete animation.target; 810 } 811 }, 812 813 /** @private 814 Called when animation ends, should not usually be called manually 815 */ 816 transitionDidEnd: function (evt) { 817 var propertyName = evt.originalEvent.propertyName, 818 activeAnimations = this._activeAnimations, 819 animation; 820 821 // Fix up the centerX & centerY properties. 822 if (propertyName === 'margin-left') { propertyName = 'centerX'; } 823 if (propertyName === 'margin-top') { propertyName = 'centerY'; } 824 animation = activeAnimations ? activeAnimations[propertyName] : null; 825 826 if (animation) { 827 // Update the animation hash. Do this first, so callbacks can check for active animations. 828 delete activeAnimations[propertyName]; 829 830 // Remove the animation style without triggering a layout change. 831 this.removeAnimationFromLayout(propertyName, YES); 832 833 // Clean up the internal hash. 834 this._activeAnimationsLength -= 1; 835 if (this._activeAnimationsLength === 0) { 836 // Route. 837 if (this.get('viewState') === SC.CoreView.ATTACHED_SHOWN_ANIMATING) { 838 this.set('viewState', SC.CoreView.ATTACHED_SHOWN); 839 } 840 841 this._activeAnimations = this._prevLayout = null; 842 } 843 844 // Run the callback. 845 this.runAnimationCallback(animation, evt, NO); 846 } 847 }, 848 849 /** @private 850 This method is called before the layout style is applied to the layer. If 851 animations have been defined for the view, they will be included in 852 this._pendingAnimations. This method will clear out any conflicts between 853 pending and active animations. 854 */ 855 willRenderAnimations: function () { 856 // Only apply the style if supported by the platform and the document is visible. 857 if (SC.platform.supportsCSSTransitions && !document.hidden) { 858 var pendingAnimations = this._pendingAnimations; 859 860 if (pendingAnimations) { 861 var activeAnimations = this._activeAnimations; 862 863 if (!activeAnimations) { 864 this._activeAnimationsLength = 0; 865 activeAnimations = {}; 866 } 867 868 for (var key in pendingAnimations) { 869 if (!pendingAnimations.hasOwnProperty(key)) { continue; } 870 871 var activeAnimation = activeAnimations[key], 872 pendingAnimation = pendingAnimations[key]; 873 874 if (activeAnimation) { 875 this.runAnimationCallback(activeAnimation, null, YES); 876 } 877 878 activeAnimations[key] = pendingAnimation; 879 this._activeAnimationsLength += 1; 880 } 881 882 this._activeAnimations = activeAnimations; 883 this._pendingAnimations = null; 884 } 885 } 886 } 887 888 }); 889