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