1 /** 
  2  * @fileoverview
  3  * anim package
  4  * Animation, Manager
  5  *
  6  * @author Garrett Smith
  7  * <p>
  8  * Sigmoid functions based upon work by Emmanuel Pietriga.
  9  * wobble and spring come from, or are loosely based on Scriptaculous.
 10  *</p>
 11  * <p>
 12  * Animation is a Template that passes a position to its <code>run( pos )</code> method. (you implement run).
 13  * </p> 
 14  * <p>
 15  * <code>anim.Transitions</code> contains effects for speed/timing, such as acceleration and easing.
 16  * </p> 
 17  * @example
 18  * <pre> 
 19  * b = new APE.anim.Animation( "blah" );
 20  * b.run = function(position) { // <-- you implement run.
 21  * 
 22  * };
 23  * b.start(); // <-- then call start.
 24  * </pre> <p>
 25  * </p>
 26  */
 27 
 28 /** 
 29  * @class 
 30  * @namespace APE.anim
 31  */
 32 
 33 APE.namespace("APE.anim");
 34 
 35 /**  
 36  * @constructor Animation
 37  * @param {ufloat} [duration] Number of seconds to run the animation (default is 1).
 38  */
 39 APE.anim.Animation = function( duration ) {
 40     if(typeof duration == "number")
 41         this.duration = duration * 1000; // default 1 sec.
 42     this.timeLimit = this.duration; // for SeekTo()
 43 };
 44 
 45 APE.anim.Animation.prototype = {
 46     
 47     paused : false,
 48 
 49     /** @type {Number} duration of how long the animation will run (milliseconds). */
 50     duration : 1000,
 51 
 52     /** @type {Number} duration of how long the animation will run.
 53      * @internal
 54      * Used internally for seekTo().
 55      */
 56     timeLimit : 1000,
 57 
 58     isReversed : false,
 59 
 60     startOffset : 0,
 61     endOffset : 1,
 62     
 63     startValue : 0,
 64     endValue : 1,
 65 
 66     rationalValue : 0,
 67 
 68     /** 
 69      * @type function
 70      * @return {Number} position value, typically betweeen [0-1].
 71      * @example <pre>
 72      * var a = new APE.anim.Animation();
 73      * a.transition = APE.anim.Transitions.loop;
 74      * </pre>
 75      */
 76     transition : function(p){return p;},
 77 
 78     position : 0,
 79 
 80     /** @event 
 81      * @description fires when the Animation starts, when seekTo is called. */
 82     onstart : function(){},
 83 
 84     /** @event 
 85      * @description fires when stop() is called or the Animation successfully completes. */
 86     onend : function(){},
 87     
 88     /** @event {Function} onplay fires right before run() is called */
 89 
 90     /** 
 91      * @event
 92      * @description If an error occurred, onabort throws the error.
 93      * this can be overridden (shadowed) by adding 
 94      * an onabort() to the Animation instance.
 95      * 
 96      * @example <pre>
 97      * APE.EventRegistry.add( myAnim, "onabort", myErrorHandler );
 98      * function myErrorHandler(ex) { alert(ex.message); }
 99      * </pre>
100      * @param {Error} ex the error that occured.
101      * @throws {Error} the error that occured.
102      */
103     onabort : function(ex) {
104         throw ex;
105     },
106 
107     /** Run must be implemented by user.
108      * @param position {Number} 0-1
109      * 
110      * Implementation generally looks something like:<pre>
111      * anim.run = function( position ) {
112      *   document.body.style.borderWidth = ( 12 * position ) + "px"
113      * };</pre>
114      */
115     run : function() { },
116 
117     /** 
118      * Call this method to start the anim.
119      */
120     start : function() {
121         if(this.paused) return;
122         this.playing = true;
123         this.timeLimit = this.duration;
124         this.endOffset = this.transition(this.endValue);
125         this._start();
126     },
127 
128     /**
129      *  timeLimit is not calculated here.
130      * unregisters Animation, calls onstart(), registers Animation.
131      * @fires onstart
132      * @private
133      */
134     _start : function() {
135         // Unregister the animation before setting startTime.
136         APE.anim.Manager.unregister(this);
137         this._startTime = new Date-0;
138         this.onstart();
139         APE.anim.Manager.register(this);
140         this.started = true;
141     },
142 
143     /** 
144      * Seeks to a certain position along the duration timeline.
145      * 
146      * If seeking backwards, the transitions is played on a mirrored timeline.
147      * This is done to make the animation "turn around", rather than "rewind".
148      *
149      * @param {float} pos Normally [0-1], but can be less than 0 or greater than 1. 
150      * @param {boolean} [transitionBackwards] If true, plays an inverse of the transition when 
151      * the animation is reversed (only applies when <code>pos < this.rationalValue</code>.
152      */
153     seekTo : function(pos, transitionBackwards) {
154         pos = parseFloat(pos);
155         if(!isFinite(pos)) return;
156         if(pos === this.rationalValue) return;
157 
158         // The new distance is the difference between the 
159         // pos and the currentPosition (position).
160         this.startOffset = this.position;
161         this.startValue = this.rationalValue;
162 
163         this.endValue = pos;
164 
165         var distance = Math.abs(pos - this.startValue);
166 
167         // The new timeLimit is a percentage of the the full duration.
168         this.timeLimit = this.duration * distance;
169         
170         this.isReversed = (pos < this.rationalValue);
171         this._transitionBackwards = this.isReversed && transitionBackwards;
172         if(this._transitionBackwards) {
173             this.endOffset = 1-this.transition(1-pos);
174         }
175         else {
176             this.endOffset = this.transition(pos);
177         }
178         this._start();
179     },
180 
181     toggleDirection : function( ) {
182         if(!this.started) {
183             this.start();
184             return;
185         }
186         if(this.isReversed)
187             this.seekTo(1);
188         else {
189             this.seekTo(0, this.position == 1);
190         }
191     },
192 
193     /** 
194      * resets the animation to position 0.
195      */
196     reset : function() {
197         this.position = 0;
198         this.timeLimit = this.duration;
199     },
200     
201     /** 
202      * pauses the animation.
203      */
204     pause : function() {
205         this.paused = true;
206         this.elapsedTime = new Date-this._startTime;
207         APE.anim.Manager.unregister(this);
208     },
209     
210     /** 
211      * unpauses the animation.
212      */
213     resume : function() {
214         this.paused = false;
215 
216         // Pick up from last time frame.
217         this._startTime = new Date - this.elapsedTime;
218         APE.anim.Manager.register(this);
219     },
220     
221     /** 
222      * Called by the anim.Manager
223      * @private
224      */
225     _playFrame : function() {
226 
227         var elapsed = new Date - this._startTime;
228 
229         if(elapsed >= this.timeLimit) {
230             this.run(this.position = this.endOffset);
231             this.rationalValue = this.endValue;
232             this._end();
233             return;
234         }
235         var rationalDistanceTraveled = (elapsed / this.duration);
236 
237         if(this.isReversed) {
238             this.rationalValue = this.startValue - rationalDistanceTraveled;
239             if(this._transitionBackwards)
240                 this.position = 1 - this.transition(1-this.rationalValue);
241             else
242                 this.position = this.transition(this.rationalValue);
243         }
244         else {
245             this.rationalValue = this.startValue + rationalDistanceTraveled;
246             this.position = this.transition(this.rationalValue);
247         }
248 
249         if(typeof this.onplay == "function")
250             this.onplay( this.position );
251         this.run( this.position );
252     },
253 
254     toString : function() {
255         return"Animation {duration millis: " + this.duration + 
256             ", position:" + this.position+"}";
257     },
258 
259     /** Ends the anim.
260      * @param {boolean} ended, if true, calls onend().
261      * Note, this is not a pause() method.
262      */
263     stop : function(ended) {
264         this._end(ended);
265     },
266 
267     /** Cancels the anim where it is; does not call onend()
268      */
269     abort : function(ex) {
270         APE.anim.Manager.unregister(this);
271         this.onabort(ex||{});
272     },
273     
274     _end : function(complete) {
275         APE.anim.Manager.unregister(this);
276         if(complete !== false) {
277             this.onend();
278         }
279     }
280 };
281 
282 /** 
283  * @class 
284  * @protected - for internal use.
285  * Manager is a Template that calls _playFrame() on each registered Animation.
286  * This object is for internal use, tightly coupled to Animation.
287  */
288 APE.anim.Manager = new function() {
289     
290     this.register = register;
291     this.unregister = unregister;
292 
293 
294     var activeAnimations = [],
295         intervalId;
296         
297    /** 
298     * Registers the animation once.
299     * @param {Animation} anim the animation to register.
300     */
301     function register(anim) {
302         if(activeAnimations.length === 0)
303             start.call(this);
304         for(var i = 0; i < activeAnimations.length; i++) {
305             if(activeAnimations[i] === anim) {
306                 return;
307             }
308         }
309         activeAnimations.push(anim);
310     }
311     
312    /** 
313     * Registers the animation in the thread pool once.
314     * @param {Animation} anim the animation to register.
315     */
316     function unregister(anim) {
317         for(var i = 0; i < activeAnimations.length; i++) {
318             if(activeAnimations[i] === anim) {
319                 activeAnimations.splice(i, 1);
320             }
321         }
322         if(activeAnimations.length == 0) {
323             activeAnimations = [];
324             stop.call(this);
325         }
326     }
327 
328     this.toString = function() {
329         return"APE.anim.Manager : activeAnimations:\n" + activeAnimations.join("\n ");
330     };
331 
332     var startTime;
333     /** 
334      * starts run();
335      * @private
336      */
337     function start() {
338         startTime = new Date;
339         var delay = 17;
340         intervalId = window.setInterval(run, delay);
341      }
342 
343     /** 
344      * Plays the frame for each animation.
345      * @private - starts run().
346      */
347     function run() {
348         var i = 0, animation;
349 
350         // Check activeAnimations.length each iteration. 
351         for(; i < activeAnimations.length; i++) {
352 
353         // If an error occurs, cancel the APE.anim and throw the error.
354             animation = activeAnimations[i];
355             try {
356                 animation._playFrame();
357             } 
358             catch(ex) {
359                 // If an error occurs, abort the anim.
360                 animation.abort(ex);
361             }
362         }
363     }
364 
365     /** 
366      * Called automatically when there are no more activeAnimations to run.
367      */
368     function stop() {
369         window.clearInterval(intervalId);
370     }
371 };
372 
373 /** 
374  * @class 
375  * Easing functions.
376  */
377 APE.anim.Transitions = {
378 
379     none : function(pos) { return pos; },
380 
381     accel: function (pos) {
382         return pos*pos*pos;
383     },
384 
385     decel : function(pos) {
386         pos = 1-pos;
387         return 1-(pos*pos*pos);
388     },
389     
390     /**
391      * For better performance, use (1-position) instead.
392      */
393     reverse: function(pos) {
394         return 1-pos;
395     },
396 
397     /**
398      * Sigmoid functions based upon work by Emmanuel Pietriga.
399      * http://www.docjar.net/html/api/com/xerox/VTM/engine/AnimManager.java.html
400      * Copyright (c) Xerox Corporation, XRCE/Contextual Computing, 2002.
401      */
402     sigmoid : function(pos, steepness) {
403         var atan = Math.atan;
404         steepness = steepness || 1;
405         return (atan(steepness*(2*pos-1))/atan(steepness)+1)/(2);
406     },
407 
408     sigmoid2 : function(pos) {
409         var atan = Math.atan;
410         return (atan(2*(2*pos-1))/atan(2)+1)/(2);
411     },
412 
413     sigmoid3 : function(pos) {
414         var atan = Math.atan;
415         return (atan(3*(2*pos-1))/atan(3)+1)/(2);
416     },
417 
418     sigmoid4 : function(pos) {
419         var atan = Math.atan;
420         return (atan(4*(2*pos-1))/atan(4)+1)/(2);
421     },
422 
423     tan : function(pos) {
424         var tan = Math.tan;
425         return (tan(1*(2*pos-1))/tan(1)+1)/(2);
426     },
427 
428     reverseWarp : function(pos) {
429         var tan = Math.tan;
430         return (tan(2*(2*pos-1))/tan(2)+1)/(2);
431     },
432 
433     /** Adapted from paper by Corey McCaffree:
434      * http://dspace.mit.edu/bitstream/1721.1/36904/1/80770344.pdf
435      */
436     easeInEaseOut : function(pos) {
437         var PI = Math.PI;
438         return (Math.atan(pos * PI / 1 - PI/2) + 1) / 2.0038848218538874;
439     },
440     
441     /**
442      * wobble, loop, and spring come from, or are based on Scriptaculous.
443      */
444     wobble: function(pos) {
445         return (-Math.cos(3*pos*Math.PI)/2) + .5;
446     },
447 
448     /** Plays the APE.anim forwards and backwards 
449      *  in the same duration.
450      */
451     loop: function(pos) {
452         return (-Math.cos(2*pos*Math.PI)/2) + .5;
453     },
454 
455     spring : function(pos) { 
456         return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); 
457     },
458     
459     /**
460      * Based on Easing Equations v2.0 
461      * (c) 2003 Robert Penner, all rights reserved. 
462      * This work is subject to the terms in http://www.robertpenner.com/easing_terms_of_use.html
463      * Adapted for Scriptaculous by Ken Snyder (kendsnyder ~at~ gmail ~dot~ com) June 2006 
464      */
465     swingTo : function(pos) { 
466         var s = 1.70158; 
467         return (pos-=1)*pos*((s+1)*pos + s) + 1;
468     },
469 
470     swingToFrom : function(pos) {
471         var s = 1.70158; 
472         if ((pos/=0.5) < 1) return 0.5*(pos*pos*(((s*=(1.525))+1)*pos - s)); 
473         return 0.5*((pos-=2)*pos*(((s*=(1.525))+1)*pos + s) + 2); 
474     },
475 
476     toString : function() {
477         return"APE anim Transitions";
478     }
479 };