1 goog.provide('lime.animation.Animation'); 2 goog.provide('lime.animation.Easing'); 3 goog.provide('lime.animation.actionManager'); 4 5 6 goog.require('goog.array'); 7 goog.require('goog.events.EventTarget'); 8 goog.require('goog.fx.easing'); 9 goog.require('lime.scheduleManager'); 10 11 12 /** 13 * General object for running animations on nodes 14 * @constructor 15 * extends goog.events.EventTarget 16 */ 17 lime.animation.Animation = function() { 18 goog.events.EventTarget.call(this); 19 20 /** 21 * Array of active target nodes 22 * @type {Array.<lime.Node>} 23 * @protected 24 */ 25 this.targets = []; 26 this.initTargets_ = []; 27 28 this.targetProp_ = {}; 29 30 /** 31 * Animation is playing? 32 * @type {boolean} 33 * @private 34 */ 35 this.isPlaying_ = false; 36 37 this.duration_ = 1.0; 38 39 this.ease = lime.animation.Easing.EASEINOUT; 40 41 this.status_ = 0; 42 43 }; 44 goog.inherits(lime.animation.Animation, goog.events.EventTarget); 45 46 /** 47 * @type {string} 48 */ 49 lime.animation.Animation.prototype.scope = ''; 50 51 /** @typedef {Array.<number|Function>} */ 52 lime.animation.EasingFunction; 53 54 55 /** 56 * Animation event types 57 * @enum {string} 58 */ 59 lime.animation.Event = { 60 START: 'start', 61 STOP: 'stop' 62 }; 63 64 /** 65 * Return duration of the animation in seconds 66 * @return {number} Animation duration. 67 */ 68 lime.animation.Animation.prototype.getDuration = function() { 69 return this.duration_; 70 }; 71 72 /** 73 * Set the duration of the animation in seconds 74 * @param {number} value New duration. 75 * @return {lime.animation.Animation} object itself. 76 */ 77 lime.animation.Animation.prototype.setDuration = function(value) { 78 this.duration_ = value; 79 return this; 80 }; 81 82 /** 83 * Set easing function for current animation 84 * @param {lime.animation.EasingFunction} ease Easing function. 85 * @return {lime.animation.Animation} object itself. 86 */ 87 lime.animation.Animation.prototype.setEasing = function(ease) { 88 this.ease = ease; 89 return this; 90 }; 91 92 /** 93 * Return current easing function 94 * @return {lime.animation.EasingFunction} Easing function. 95 */ 96 lime.animation.Animation.prototype.getEasing = function() { 97 return this.ease; 98 }; 99 100 /** 101 * Add target node to animation 102 * @param {lime.Node} target Target node. 103 * @return {lime.animation.Animation} object itself. 104 */ 105 lime.animation.Animation.prototype.addTarget = function(target) { 106 goog.array.insert(this.targets, target); 107 return this; 108 }; 109 110 /** 111 * Remove target node from animation 112 * @param {lime.Node} target Node to be removed. 113 * @return {lime.animation.Animation} object itself. 114 */ 115 lime.animation.Animation.prototype.removeTarget = function(target) { 116 goog.array.remove(this.targets, target); 117 return this; 118 }; 119 120 /** 121 * Start playing the animation 122 */ 123 lime.animation.Animation.prototype.play = function() { 124 this.playTime_ = 0; 125 this.status_ = 1; 126 this.firstFrame_ = 1; 127 lime.scheduleManager.schedule(this.step_, this); 128 this.dispatchEvent({type: lime.animation.Event.START}); 129 }; 130 131 /** 132 * Stop playing the animstion 133 */ 134 lime.animation.Animation.prototype.stop = function() { 135 if (this.status_ != 0) { 136 var targets = this.initTargets_; 137 if (this.useTransitions() && this.clearTransition) { 138 var i = targets.length; 139 while (--i >= 0) { 140 this.clearTransition(targets[i]); 141 } 142 } 143 this.initTargets_ = []; 144 this.targetProp_ = {}; 145 this.status_ = 0; 146 lime.scheduleManager.unschedule(this.step_, this); 147 this.dispatchEvent({type: lime.animation.Event.STOP}); 148 } 149 }; 150 151 /** 152 * Make property object for target that hold animation helper values. 153 * @param {lime.Node} target Target node. 154 * @return {Object} Properties object. 155 */ 156 lime.animation.Animation.prototype.makeTargetProp = function(target) { 157 return {}; 158 }; 159 160 /** 161 * Get properties object for target. 162 * @param {lime.Node} target Target node. 163 * @return {Object} Properties object. 164 */ 165 lime.animation.Animation.prototype.getTargetProp = function(target) { 166 var uid = goog.getUid(target); 167 if (!goog.isDef(this.targetProp_[uid])) { 168 this.initTarget(target); 169 this.targetProp_[uid] = this.makeTargetProp(target); 170 } 171 return this.targetProp_[uid]; 172 }; 173 174 /** 175 * Initalize the aniamtion for a target 176 * @param {lime.Node} target Target node. 177 */ 178 lime.animation.Animation.prototype.initTarget = function(target) { 179 lime.animation.actionManager.register(this, target); 180 //todo: check if all this status_ mess can be removed now 181 this.status_ = 1; 182 goog.array.insert(this.initTargets_, target); 183 if(this.speed_ && !this.speedCalcDone_ && this.calcDurationFromSpeed_){ 184 this.calcDurationFromSpeed_(); 185 } 186 }; 187 188 /** 189 * Get the director related to the animation targets. 190 * @return {lime.Director} Director. 191 */ 192 lime.animation.Animation.prototype.getDirector = function() { 193 return this.targets[0] ? this.targets[0].getDirector() : null; 194 }; 195 196 /** 197 * Iterate time for animation 198 * @private 199 * @throws {WrongParam} If easing function is not correct 200 * @param {number} dt Time difference since last run. 201 */ 202 lime.animation.Animation.prototype.step_ = function(dt) { 203 if(this.speed_ && !this.speedCalcDone_ && this.calcDurationFromSpeed_){ 204 this.calcDurationFromSpeed_(); 205 } 206 if(this.firstFrame_){ 207 delete this.firstFrame_; 208 dt = 1; 209 } 210 211 this.playTime_ += dt; 212 this.dt_ = dt; 213 var t = this.playTime_ / (this.duration_ * 1000); 214 if (isNaN(t) || t>=1) t = 1; 215 t = this.updateAll(t,this.targets); 216 217 if (t == 1) { 218 this.stop(); 219 } 220 }; 221 222 /** 223 * Update all targets to new values. 224 * @param {number} t Time position of animation[0-1]. 225 * @param {Array.<lime.Node>} targets All target nodes to update. 226 * @return {number} New time position(eased value). 227 */ 228 lime.animation.Animation.prototype.updateAll = function(t,targets){ 229 t = this.getEasing()[0](t); 230 if (isNaN(t)) { 231 t = 1; 232 } 233 234 var i = targets.length; 235 while (--i >= 0) { 236 this.update(t, targets[i]); 237 } 238 return t; 239 } 240 241 /** 242 * Returns true if CSS transitions are used to make the animation. 243 * Performes better on iOS devices. 244 * @return {boolean} Transitions are being used? 245 */ 246 lime.animation.Animation.prototype.useTransitions = function() { 247 return this.duration_ > 0 && lime.style.isTransitionsSupported && 248 this.optimizations_ && 249 // goog.userAgent.MOBILE && // I see no boost on mac, only on iOS 250 !lime.userAgent.ANDROID && // bug in 2.2 http://code.google.com/p/android/issues/detail?id=12451 251 !goog.userAgent.GECKO; // still many bugs on FF4Beta Mac when hardware acceleration in ON 252 }; 253 254 /** 255 * Enable CSS3 transitions to make animation. Usually performes better 256 * but is limited to single parallel transform action. 257 * @param {boolean=} opt_value Enable or disable. 258 * @return {lime.animation.Animation} object itself. 259 */ 260 lime.animation.Animation.prototype.enableOptimizations = function(opt_value) { 261 var bool = goog.isDef(opt_value) ? opt_value : true; 262 this.optimizations_ = bool; 263 return this; 264 }; 265 266 /** 267 * Update targets to new values 268 * @param {number} t Time position of animation[0-1]. 269 * @param {lime.Node} target Target node to update. 270 */ 271 lime.animation.Animation.prototype.update = goog.abstractMethod; 272 273 /** 274 * Clone animation parmaters from another animation 275 * @protected 276 */ 277 lime.animation.Animation.prototype.cloneParam = function(origin){ 278 return this.setDuration(origin.getDuration()).enableOptimizations(origin.optimizations_); 279 } 280 281 /** 282 * Return new animation with reveresed parameters from original 283 * @throws {NotSupported} No reverese animation possible. 284 * @return {?lime.animation.Animation} New animation. 285 */ 286 lime.animation.Animation.prototype.reverse = function() { 287 throw('Reverseform not supported for this animation'); 288 }; 289 290 291 /** 292 * ActionManager. Doesn't let animations that modify same parameters 293 * run together on same targets. 294 * @constructor 295 */ 296 lime.animation.actionManager = new (function() { 297 this.actions = {}; 298 }); 299 300 /** 301 * Register animation in the manager. 302 * @param {lime.animation.Animation} action Action. 303 * @param {lime.Node} target Taget node. 304 * @this {lime.animation.actionManager} 305 */ 306 lime.animation.actionManager.register = function(action, target) { 307 //Todo: probably needs some garbage collection 308 if (!action.scope.length) return; 309 var id = goog.getUid(target); 310 if (!goog.isDef(this.actions[id])) { 311 this.actions[id] = {}; 312 } 313 if (goog.isDef(this.actions[id][action.scope])) { 314 this.actions[id][action.scope].stop(); 315 } 316 this.actions[id][action.scope] = action; 317 }; 318 319 /** 320 * Stop all animations on target. 321 * @param {lime.Node} target Target node. 322 * @this {lime.animation.actionManager} 323 */ 324 lime.animation.actionManager.stopAll = function(target) { 325 // todo: doesn't stop scopless action atm. (like sequence) 326 var id = goog.getUid(target); 327 if (goog.isDef(this.actions[id])) { 328 for (var i in this.actions[id]) { 329 this.actions[id][i].stop(); 330 delete this.actions[id][i]; 331 } 332 } 333 }; 334 335 336 (function(){ 337 338 // www.netzgesta.de/dev/cubic-bezier-timing-function.html 339 // currently used function to determine time 340 // 1:1 conversion to js from webkit source files 341 // UnitBezier.h, WebCore_animation_AnimationBase.cpp 342 var ax=0,bx=0,cx=0,ay=0,by=0,cy=0; 343 // `ax t^3 + bx t^2 + cx t' expanded using Horner's rule. 344 function sampleCurveX(t) {return ((ax*t+bx)*t+cx)*t;}; 345 function sampleCurveY(t) {return ((ay*t+by)*t+cy)*t;}; 346 function sampleCurveDerivativeX(t) {return (3.0*ax*t+2.0*bx)*t+cx;}; 347 // The epsilon value to pass given that the animation is going to run over |dur| seconds. The longer the 348 // animation, the more precision is needed in the timing function result to avoid ugly discontinuities. 349 function solveEpsilon(duration) {return 1.0/(200.0*duration);}; 350 function solve(x,epsilon) {return sampleCurveY(solveCurveX(x,epsilon));}; 351 // Given an x value, find a parametric value it came from. 352 function fabs(n) {if(n>=0) {return n;}else {return 0-n;}}; 353 function solveCurveX(x,epsilon) {var t0,t1,t2,x2,d2,i; 354 // First try a few iterations of Newton's method -- normally very fast. 355 for(t2=x, i=0; i<8; i++) {x2=sampleCurveX(t2)-x; if(fabs(x2)<epsilon) {return t2;} d2=sampleCurveDerivativeX(t2); if(fabs(d2)<1e-6) {break;} t2=t2-x2/d2;} 356 // Fall back to the bisection method for reliability. 357 t0=0.0; t1=1.0; t2=x; if(t2<t0) {return t0;} if(t2>t1) {return t1;} 358 while(t0<t1) {x2=sampleCurveX(t2); if(fabs(x2-x)<epsilon) {return t2;} if(x>x2) {t0=t2;}else {t1=t2;} t2=(t1-t0)*.5+t0;} 359 return t2; // Failure. 360 }; 361 function CubicBezierAtTime(t,p1x,p1y,p2x,p2y,duration) { 362 // Calculate the polynomial coefficients, implicit first and last control points are (0,0) and (1,1). 363 cx=3.0*p1x; bx=3.0*(p2x-p1x)-cx; ax=1.0-cx-bx; cy=3.0*p1y; by=3.0*(p2y-p1y)-cy; ay=1.0-cy-by; 364 // Convert from input time to parametric value in curve, then from that to output time. 365 return solve(t, solveEpsilon(duration)); 366 }; 367 368 369 /** 370 * Return easing function from Bezier curce points 371 * @see http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag 372 * @param {number} p1x Point one X axis value. 373 * @param {number} p1y Point one Y axis value. 374 * @param {number} p2x Point two X axis value. 375 * @param {number} p2y Point two Y axis value. 376 * @return {lime.animation.EasingFunction} Easing function. 377 */ 378 lime.animation.getEasingFunction = function(p1x, p1y, p2x, p2y) { 379 var that = lime.animation; 380 return [function(t) { 381 return CubicBezierAtTime(t,p1x,p1y,p2x,p2y,100); 382 },p1x, p1y, p2x, p2y]; 383 384 }; 385 386 387 388 })(); 389 390 391 392 393 /** 394 * Predefined Easing functions 395 * @enum {lime.animation.EasingFunction} 396 */ 397 lime.animation.Easing = { 398 EASE: lime.animation.getEasingFunction(.25, .1, .25, 1), 399 LINEAR: lime.animation.getEasingFunction(0, 0, 1, 1), 400 EASEIN: lime.animation.getEasingFunction(.42, 0, 1, 1), 401 EASEOUT: lime.animation.getEasingFunction(0, 0, .58, 1), 402 EASEINOUT: lime.animation.getEasingFunction(.42, 0, .58, 1) 403 }; 404 405 406 407