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 };