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