1 /**
  2  * Hilo
  3  * Copyright 2015 alibaba.com
  4  * Licensed under the MIT License
  5  */
  6 
  7 /**
  8  * <iframe src='../../../examples/Tween.html?noHeader' width = '550' height = '130' scrolling='no'></iframe>
  9  * <br/>
 10  * Demo:
 11  * <pre>
 12  * ticker.addTick(Hilo.Tween);//Tween works after being added to ticker
 13  *
 14  * var view = new View({x:5, y:10});
 15  * Hilo.Tween.to(view, {
 16  *     x:100,
 17  *     y:20,
 18  *     alpha:0
 19  * }, {
 20  *     duration:1000,
 21  *     delay:500,
 22  *     ease:Hilo.Ease.Quad.EaseIn,
 23  *     onComplete:function(){
 24  *         console.log('complete');
 25  *     }
 26  * });
 27  * </pre>
 28  * @class Tween class makes tweening (easing, slow motion).
 29  * @param {Object} target Tween target object.
 30  * @param {Object} fromProps Beginning properties of target tweening object.
 31  * @param {Object} toProps Ending properties of target tweening object.
 32  * @param {Object} params Tweening parameters, include all writable Tween class properties.
 33  * @module hilo/tween/Tween
 34  * @requires hilo/core/Class
 35  * @property {Object} target Tween target object, readonly!
 36  * @property {Int} duration Tweening duration, measure in ms.
 37  * @property {Int} delay Tweenning delay time, measure in ms.
 38  * @property {Boolean} paused Is tweening paused, default value is false.
 39  * @property {Boolean} loop Does tweening loop, default value is false.
 40  * @property {Boolean} reverse Does tweening reverse, default value is false.
 41  * @property {Int} repeat Repeat times of tweening, default value is 0.
 42  * @property {Int} repeatDelay Delay time of repeating tweening, measure in ms.
 43  * @property {Function} ease Tweening transform function, default value is null.
 44  * @property {Int} time Time that tweening taken, measure in ms, readonly!
 45  * @property {Function} onStart Function invoked on the beginning of tweening. Require 1 parameter: tween. default value is null.
 46  * @property {Function} onUpdate Function invoked on tweening update. Require 2 parameters: ratio, tween.  default value is null.
 47  * @property {Function} onComplete Function invoked on the end of tweening. Require 1 parameter: tween.  default value is null.
 48  */
 49 var Tween = (function(){
 50 
 51 function now(){
 52     return +new Date();
 53 }
 54 
 55 return Class.create(/** @lends Tween.prototype */{
 56     constructor: function(target, fromProps, toProps, params){
 57         var me = this;
 58 
 59         me.target = target;
 60         me._startTime = 0;
 61         me._seekTime = 0;
 62         me._pausedTime = 0;
 63         me._pausedStartTime = 0;
 64         me._reverseFlag = 1;
 65         me._repeatCount = 0;
 66 
 67         //no fromProps if pass 3 arguments
 68         if(arguments.length == 3){
 69             params = toProps;
 70             toProps = fromProps;
 71             fromProps = null;
 72         }
 73 
 74         for(var p in params) me[p] = params[p];
 75         me._fromProps = fromProps;
 76         me._toProps = toProps;
 77 
 78         //for old version compatiblity
 79         if(!params.duration && params.time){
 80             me.duration = params.time || 0;
 81             me.time = 0;
 82         }
 83     },
 84 
 85     target: null,
 86     duration: 1000,
 87     delay: 0,
 88     paused: false,
 89     loop: false,
 90     reverse: false,
 91     repeat: 0,
 92     repeatDelay: 0,
 93     ease: null,
 94     time: 0, //ready only
 95 
 96     isStart:false,
 97     isComplete:false,
 98     onStart: null,
 99     onUpdate: null,
100     onComplete: null,
101 
102     /**
103      * Set beginning properties and ending properties of tweening object.
104      * @param {Object} fromProps Beginning properties of target tweening object.
105      * @param {Object} toProps Ending properties of target tweening object.
106      * @returns {Tween} Current Tween, for chain calls.
107      */
108     setProps: function(fromProps, toProps){
109         var me = this, target = me.target,
110             propNames = fromProps || toProps,
111             from = me._fromProps = {}, to = me._toProps = {};
112 
113         fromProps = fromProps || target;
114         toProps = toProps || target;
115 
116         for(var p in propNames){
117             to[p] = toProps[p] || 0;
118             target[p] = from[p] = fromProps[p] || 0;
119         }
120         return me;
121     },
122 
123     /**
124      * Starting the tweening.
125      * @returns {Tween} Current Tween, for chain calls.
126      */
127     start: function(){
128         var me = this;
129         me._startTime = now() + me.delay;
130         me._seekTime = 0;
131         me._pausedTime = 0;
132         me._reverseFlag = 1;
133         me._repeatCount = 0;
134         me.paused = false;
135         me.isStart = false;
136         me.isComplete = false;
137         Tween.add(me);
138         return me;
139     },
140 
141     /**
142      * Stop the tweening.
143      * @returns {Tween} Current Tween, for chain calls.
144      */
145     stop: function(){
146         Tween.remove(this);
147         return this;
148     },
149 
150     /**
151      * Pause the tweening.
152      * @returns {Tween} Current Tween, for chain calls.
153      */
154     pause: function(){
155         var me = this;
156         me.paused = true;
157         me._pausedStartTime = now();
158         return me;
159     },
160 
161     /**
162      * Continue to play the tweening.
163      * @returns {Tween} Current Tween, for chain calls.
164      */
165     resume: function(){
166         var me = this;
167         me.paused = false;
168         if(me._pausedStartTime) me._pausedTime += now() - me._pausedStartTime;
169         me._pausedStartTime = 0;
170         return me;
171     },
172 
173     /**
174      * Tween jumps to some point.
175      * @param {Number} time The time to jump to, range from 0 to duration.
176      * @param {Boolean} pause Is paused.
177      * @returns {Tween} Current Tween, for chain calls.
178      */
179     seek: function(time, pause){
180         var me = this, current = now();
181         me._startTime = current;
182         me._seekTime = time;
183         me._pausedTime = 0;
184         if(pause !== undefined) me.paused = pause;
185         me._update(current, true);
186         Tween.add(me);
187         return me;
188     },
189 
190     /**
191      * Link next Tween. The beginning time of next Tween depends on the delay value. If delay is a string that begins with '+' or '-', next Tween will begin at (delay) ms after or before the current tween is ended. If delay is out of previous situation, next Tween will begin at (delay) ms after the beginning point of current Tween.
192      * @param {Tween} tween Tween to link.
193      * @returns {Tween} next Tween, for chain calls.
194      */
195     link: function(tween){
196         var me = this, delay = tween.delay, startTime = me._startTime;
197 
198         var plus, minus;
199         if(typeof delay === 'string'){
200             plus = delay.indexOf('+') == 0;
201             minus = delay.indexOf('-') == 0;
202             delay = plus || minus ? Number(delay.substr(1)) * (plus ? 1 : -1) : Number(delay);
203         }
204         tween.delay = delay;
205         tween._startTime = plus || minus ? startTime + me.duration + delay : startTime + delay;
206 
207         me._next = tween;
208         Tween.remove(tween);
209         return tween;
210     },
211 
212     /**
213      * Private render method inside Tween class.
214      * @private
215      */
216     _render: function(ratio){
217         var me = this, target = me.target, fromProps = me._fromProps, p;
218         for(p in fromProps) target[p] = fromProps[p] + (me._toProps[p] - fromProps[p]) * ratio;
219     },
220 
221     /**
222      * Private update method inside Tween class.
223      * @private
224      */
225     _update: function(time, forceUpdate){
226         var me = this;
227         if(me.paused && !forceUpdate) return;
228         if(me.isComplete) return true;
229 
230         //elapsed time
231         var elapsed = time - me._startTime - me._pausedTime + me._seekTime;
232         if(elapsed < 0) return;
233 
234         //elapsed ratio
235         var ratio = elapsed / me.duration, complete = false, callback;
236         ratio = ratio <= 0 ? 0 : ratio >= 1 ? 1 : ratio;
237         var easeRatio = me.ease ? me.ease(ratio) : ratio;
238 
239         if(me.reverse && me.isStart){
240             //backward
241             if(me._reverseFlag < 0) {
242                 ratio = 1 - ratio;
243                 easeRatio = 1 - easeRatio;
244             }
245             //forward
246             if(ratio < 1e-7){
247                 //repeat complete or not loop
248                 if((me.repeat > 0 && me._repeatCount++ >= me.repeat) || (me.repeat == 0 && !me.loop)){
249                     complete = true;
250                 }else{
251                     me._startTime = now();
252                     me._pausedTime = 0;
253                     me._reverseFlag *= -1;
254                 }
255             }
256         }
257 
258         //start callback
259         if(!me.isStart) {
260             me.setProps(me._fromProps, me._toProps);
261             me.isStart = true;
262             if(me.onStart){
263                 me.onStart.call(me, me);
264             }
265         }
266         me.time = elapsed;
267 
268         //render & update callback
269         me._render(easeRatio);
270         (callback = me.onUpdate) && callback.call(me, ratio, me);
271 
272         //check if complete
273         if(ratio >= 1){
274             if(me.reverse){
275                 me._startTime = now();
276                 me._pausedTime = 0;
277                 me._reverseFlag *= -1;
278             }else if(me.loop || me.repeat > 0 && me._repeatCount++ < me.repeat){
279                 me._startTime = now() + me.repeatDelay;
280                 me._pausedTime = 0;
281             }else{
282                 me.isComplete = true;
283             }
284         }
285 
286         //next tween
287         var next = me._next;
288         if(next && next.time <= 0){
289             var nextStartTime = next._startTime;
290             if(nextStartTime > 0 && nextStartTime <= time){
291                 //parallel tween
292                 next._render(ratio);
293                 next.time = elapsed;
294                 Tween.add(next);
295             }else if(me.isComplete && (nextStartTime < 0 || nextStartTime > time)){
296                 //next tween
297                 next.start();
298             }
299         }
300 
301         //complete
302         if(me.isComplete){
303             (callback = me.onComplete) && callback.call(me, me);
304             return true;
305         }
306     },
307 
308     Statics: /** @lends Tween */ {
309         /**
310          * @private
311          */
312         _tweens: [],
313 
314         /**
315          * Update all Tween instances.
316          * @returns {Object} Tween。
317          */
318         tick: function(){
319             var tweens = Tween._tweens, tween, i, len = tweens.length;
320 
321             for(i = 0; i < len; i++){
322                 tween = tweens[i];
323                 if(tween && tween._update(now())){
324                     tweens.splice(i, 1);
325                     i--;
326                 }
327             }
328             return Tween;
329         },
330 
331         /**
332          * Add a Tween instance.
333          * @param {Tween} tween Tween object to add.
334          * @returns {Object} Tween。
335          */
336         add: function(tween){
337             var tweens = Tween._tweens;
338             if(tweens.indexOf(tween) == -1) tweens.push(tween);
339             return Tween;
340         },
341 
342         /**
343          * Remove one Tween target.
344          * @param {Tween|Object|Array} tweenOrTarget Tween object, target object or an array of object to remove
345          * @returns {Object} Tween。
346          */
347         remove: function(tweenOrTarget){
348             var i, l;
349             if(tweenOrTarget instanceof Array){
350                 for(i = 0, l = tweenOrTarget.length;i < l;i ++){
351                     Tween.remove(tweenOrTarget[i]);
352                 }
353                 return Tween;
354             }
355 
356             var tweens = Tween._tweens;
357             if(tweenOrTarget instanceof Tween){
358                 i = tweens.indexOf(tweenOrTarget);
359                 if(i > -1) tweens.splice(i, 1);
360             }else{
361                 for(i = 0; i < tweens.length; i++){
362                     if(tweens[i].target === tweenOrTarget){
363                         tweens.splice(i, 1);
364                         i--;
365                     }
366                 }
367             }
368 
369             return Tween;
370         },
371 
372         /**
373          * Remove all Tween instances.
374          * @returns {Object} Tween。
375          */
376         removeAll: function(){
377             Tween._tweens.length = 0;
378             return Tween;
379         },
380 
381         /**
382          * Create a tween, make target object easing from beginning properties to ending properties.
383          * @param {Object|Array} target Tweening target or tweening target array.
384          * @param fromProps Beginning properties of target tweening object.
385          * @param toProps Ending properties of target tweening object.
386          * @param params Tweening parameters.
387          * @returns {Tween|Array} An tween instance or an array of tween instance.
388          */
389         fromTo: function(target, fromProps, toProps, params){
390             params = params || {};
391             var isArray = target instanceof Array;
392             target = isArray ? target : [target];
393 
394             var tween, i, stagger = params.stagger, tweens = [];
395             for(i = 0; i < target.length; i++){
396                 tween = new Tween(target[i], fromProps, toProps, params);
397                 if(stagger) tween.delay = (params.delay || 0) + (i * stagger || 0);
398                 tween.start();
399                 tweens.push(tween);
400             }
401 
402             return isArray?tweens:tween;
403         },
404 
405         /**
406          * Create a tween, make target object easing from current properties to ending properties.
407          * @param {Object|Array} target Tweening target or tweening target array.
408          * @param toProps Ending properties of target tweening object.
409          * @param params Tweening parameters.
410          * @returns {Tween|Array} An tween instance or an array of tween instance.
411          */
412         to: function(target, toProps, params){
413             return Tween.fromTo(target, null, toProps, params);
414         },
415 
416         /**
417          * Create a tween, make target object easing from beginning properties to current properties.
418          * @param {Object|Array} target Tweening target or tweening target array.
419          * @param fromProps Beginning properties of target tweening object.
420          * @param params Tweening parameters.
421          * @returns {Tween|Array} An tween instance or an array of tween instance.
422          */
423         from: function(target, fromProps, params){
424             return Tween.fromTo(target, fromProps, null, params);
425         }
426     }
427 
428 });
429 
430 })();
431