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