Upgrade to ExtJS 4.0.0 - Released 04/26/2011
[extjs.git] / src / fx / Animator.js
1 /**
2  * @class Ext.fx.Animator
3  * Animation instance
4
5 This class is used to run keyframe based animations, which follows the CSS3 based animation structure. 
6 Keyframe animations differ from typical from/to animations in that they offer the ability to specify values 
7 at various points throughout the animation.
8
9 __Using Keyframes__
10 The {@link #keyframes} option is the most important part of specifying an animation when using this 
11 class. A key frame is a point in a particular animation. We represent this as a percentage of the
12 total animation duration. At each key frame, we can specify the target values at that time. Note that
13 you *must* specify the values at 0% and 100%, the start and ending values. There is also a {@link keyframe}
14 event that fires after each key frame is reached.
15
16 __Example Usage__
17 In the example below, we modify the values of the element at each fifth throughout the animation.
18
19     Ext.create('Ext.fx.Animator', {
20         target: Ext.getBody().createChild({
21             style: {
22                 width: '100px',
23                 height: '100px',
24                 'background-color': 'red'
25             }
26         }),
27         duration: 10000, // 10 seconds
28         keyframes: {
29             0: {
30                 opacity: 1,
31                 backgroundColor: 'FF0000'
32             },
33             20: {
34                 x: 30,
35                 opacity: 0.5    
36             },
37             40: {
38                 x: 130,
39                 backgroundColor: '0000FF'    
40             },
41             60: {
42                 y: 80,
43                 opacity: 0.3    
44             },
45             80: {
46                 width: 200,
47                 y: 200    
48             },
49             100: {
50                 opacity: 1,
51                 backgroundColor: '00FF00'
52             }
53         }
54     });
55
56  * @markdown
57  */
58 Ext.define('Ext.fx.Animator', {
59
60     /* Begin Definitions */
61
62     mixins: {
63         observable: 'Ext.util.Observable'
64     },
65
66     requires: ['Ext.fx.Manager'],
67
68     /* End Definitions */
69
70     isAnimator: true,
71
72     /**
73      * @cfg {Number} duration
74      * Time in milliseconds for the animation to last. Defaults to 250.
75      */
76     duration: 250,
77
78     /**
79      * @cfg {Number} delay
80      * Time to delay before starting the animation. Defaults to 0.
81      */
82     delay: 0,
83
84     /* private used to track a delayed starting time */
85     delayStart: 0,
86
87     /**
88      * @cfg {Boolean} dynamic
89      * Currently only for Component Animation: Only set a component's outer element size bypassing layouts.  Set to true to do full layouts for every frame of the animation.  Defaults to false.
90      */
91     dynamic: false,
92
93     /**
94      * @cfg {String} easing
95
96 This describes how the intermediate values used during a transition will be calculated. It allows for a transition to change
97 speed over its duration. 
98
99 - backIn
100 - backOut
101 - bounceIn
102 - bounceOut
103 - ease
104 - easeIn
105 - easeOut
106 - easeInOut
107 - elasticIn
108 - elasticOut
109 - cubic-bezier(x1, y1, x2, y2)
110
111 Note that cubic-bezier will create a custom easing curve following the CSS3 transition-timing-function specification `{@link http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag}`. The four values specify points P1 and P2 of the curve
112 as (x1, y1, x2, y2). All values must be in the range [0, 1] or the definition is invalid.
113
114      * @markdown
115      */
116     easing: 'ease',
117
118     /**
119      * Flag to determine if the animation has started
120      * @property running
121      * @type boolean
122      */
123     running: false,
124
125     /**
126      * Flag to determine if the animation is paused. Only set this to true if you need to
127      * keep the Anim instance around to be unpaused later; otherwise call {@link #end}.
128      * @property paused
129      * @type boolean
130      */
131     paused: false,
132
133     /**
134      * @private
135      */
136     damper: 1,
137
138     /**
139      * @cfg {Number} iterations
140      * Number of times to execute the animation. Defaults to 1.
141      */
142     iterations: 1,
143
144     /**
145      * Current iteration the animation is running.
146      * @property currentIteration
147      * @type int
148      */
149     currentIteration: 0,
150
151     /**
152      * Current keyframe step of the animation.
153      * @property keyframeStep
154      * @type Number
155      */
156     keyframeStep: 0,
157
158     /**
159      * @private
160      */
161     animKeyFramesRE: /^(from|to|\d+%?)$/,
162
163     /**
164      * @cfg {Ext.fx.target} target
165      * The Ext.fx.target to apply the animation to.  If not specified during initialization, this can be passed to the applyAnimator
166      * method to apply the same animation to many targets.
167      */
168
169      /**
170       * @cfg {Object} keyframes
171       * Animation keyframes follow the CSS3 Animation configuration pattern. 'from' is always considered '0%' and 'to'
172       * is considered '100%'.<b>Every keyframe declaration must have a keyframe rule for 0% and 100%, possibly defined using
173       * "from" or "to"</b>.  A keyframe declaration without these keyframe selectors is invalid and will not be available for
174       * animation.  The keyframe declaration for a keyframe rule consists of properties and values. Properties that are unable to
175       * be animated are ignored in these rules, with the exception of 'easing' which can be changed at each keyframe. For example:
176  <pre><code>
177 keyframes : {
178     '0%': {
179         left: 100
180     },
181     '40%': {
182         left: 150
183     },
184     '60%': {
185         left: 75
186     },
187     '100%': {
188         left: 100
189     }
190 }
191  </code></pre>
192       */
193     constructor: function(config) {
194         var me = this;
195         config = Ext.apply(me, config || {});
196         me.config = config;
197         me.id = Ext.id(null, 'ext-animator-');
198         me.addEvents(
199             /**
200              * @event beforeanimate
201              * Fires before the animation starts. A handler can return false to cancel the animation.
202              * @param {Ext.fx.Animator} this
203              */
204             'beforeanimate',
205             /**
206               * @event keyframe
207               * Fires at each keyframe.
208               * @param {Ext.fx.Animator} this
209               * @param {Number} keyframe step number
210               */
211             'keyframe',
212             /**
213              * @event afteranimate
214              * Fires when the animation is complete.
215              * @param {Ext.fx.Animator} this
216              * @param {Date} startTime
217              */
218             'afteranimate'
219         );
220         me.mixins.observable.constructor.call(me, config);
221         me.timeline = [];
222         me.createTimeline(me.keyframes);
223         if (me.target) {
224             me.applyAnimator(me.target);
225             Ext.fx.Manager.addAnim(me);
226         }
227     },
228
229     /**
230      * @private
231      */
232     sorter: function (a, b) {
233         return a.pct - b.pct;
234     },
235
236     /**
237      * @private
238      * Takes the given keyframe configuration object and converts it into an ordered array with the passed attributes per keyframe
239      * or applying the 'to' configuration to all keyframes.  Also calculates the proper animation duration per keyframe.
240      */
241     createTimeline: function(keyframes) {
242         var me = this,
243             attrs = [],
244             to = me.to || {},
245             duration = me.duration,
246             prevMs, ms, i, ln, pct, anim, nextAnim, attr;
247
248         for (pct in keyframes) {
249             if (keyframes.hasOwnProperty(pct) && me.animKeyFramesRE.test(pct)) {
250                 attr = {attrs: Ext.apply(keyframes[pct], to)};
251                 // CSS3 spec allow for from/to to be specified.
252                 if (pct == "from") {
253                     pct = 0;
254                 }
255                 else if (pct == "to") {
256                     pct = 100;
257                 }
258                 // convert % values into integers
259                 attr.pct = parseInt(pct, 10);
260                 attrs.push(attr);
261             }
262         }
263         // Sort by pct property
264         Ext.Array.sort(attrs, me.sorter);
265         // Only an end
266         //if (attrs[0].pct) {
267         //    attrs.unshift({pct: 0, attrs: element.attrs});
268         //}
269
270         ln = attrs.length;
271         for (i = 0; i < ln; i++) {
272             prevMs = (attrs[i - 1]) ? duration * (attrs[i - 1].pct / 100) : 0;
273             ms = duration * (attrs[i].pct / 100);
274             me.timeline.push({
275                 duration: ms - prevMs,
276                 attrs: attrs[i].attrs
277             });
278         }
279     },
280
281     /**
282      * Applies animation to the Ext.fx.target
283      * @private
284      * @param target
285      * @type string/object
286      */
287     applyAnimator: function(target) {
288         var me = this,
289             anims = [],
290             timeline = me.timeline,
291             reverse = me.reverse,
292             ln = timeline.length,
293             anim, easing, damper, initial, attrs, lastAttrs, i;
294
295         if (me.fireEvent('beforeanimate', me) !== false) {
296             for (i = 0; i < ln; i++) {
297                 anim = timeline[i];
298                 attrs = anim.attrs;
299                 easing = attrs.easing || me.easing;
300                 damper = attrs.damper || me.damper;
301                 delete attrs.easing;
302                 delete attrs.damper;
303                 anim = Ext.create('Ext.fx.Anim', {
304                     target: target,
305                     easing: easing,
306                     damper: damper,
307                     duration: anim.duration,
308                     paused: true,
309                     to: attrs
310                 });
311                 anims.push(anim);
312             }
313             me.animations = anims;
314             me.target = anim.target;
315             for (i = 0; i < ln - 1; i++) {
316                 anim = anims[i];
317                 anim.nextAnim = anims[i + 1];
318                 anim.on('afteranimate', function() {
319                     this.nextAnim.paused = false;
320                 });
321                 anim.on('afteranimate', function() {
322                     this.fireEvent('keyframe', this, ++this.keyframeStep);
323                 }, me);
324             }
325             anims[ln - 1].on('afteranimate', function() {
326                 this.lastFrame();
327             }, me);
328         }
329     },
330
331     /*
332      * @private
333      * Fires beforeanimate and sets the running flag.
334      */
335     start: function(startTime) {
336         var me = this,
337             delay = me.delay,
338             delayStart = me.delayStart,
339             delayDelta;
340         if (delay) {
341             if (!delayStart) {
342                 me.delayStart = startTime;
343                 return;
344             }
345             else {
346                 delayDelta = startTime - delayStart;
347                 if (delayDelta < delay) {
348                     return;
349                 }
350                 else {
351                     // Compensate for frame delay;
352                     startTime = new Date(delayStart.getTime() + delay);
353                 }
354             }
355         }
356         if (me.fireEvent('beforeanimate', me) !== false) {
357             me.startTime = startTime;
358             me.running = true;
359             me.animations[me.keyframeStep].paused = false;
360         }
361     },
362
363     /*
364      * @private
365      * Perform lastFrame cleanup and handle iterations
366      * @returns a hash of the new attributes.
367      */
368     lastFrame: function() {
369         var me = this,
370             iter = me.iterations,
371             iterCount = me.currentIteration;
372
373         iterCount++;
374         if (iterCount < iter) {
375             me.startTime = new Date();
376             me.currentIteration = iterCount;
377             me.keyframeStep = 0;
378             me.applyAnimator(me.target);
379             me.animations[me.keyframeStep].paused = false;
380         }
381         else {
382             me.currentIteration = 0;
383             me.end();
384         }
385     },
386
387     /*
388      * Fire afteranimate event and end the animation. Usually called automatically when the
389      * animation reaches its final frame, but can also be called manually to pre-emptively
390      * stop and destroy the running animation.
391      */
392     end: function() {
393         var me = this;
394         me.fireEvent('afteranimate', me, me.startTime, new Date() - me.startTime);
395     }
396 });