1 /** @namespace Animation support. */ 2 Jelo.Anim = function() { 3 /** @private convenience */ 4 var JF = Jelo.Format, 5 jcg = Jelo.CSS.getStyle, 6 jcs = Jelo.CSS.setStyle, 7 pi = function(n) { 8 return parseInt(n, 10); 9 }; 10 11 /** @private constants */ 12 var _ = { 13 d : 0.5, // default duration 14 a : [], // holds animation objects 15 r : [], // contains tasks that need to be removed 16 t : null, // single timer to animate everything 17 f : 60, // desired frames per second 18 now : function() { 19 return new Date().getTime(); 20 } 21 }; 22 _.i = Math.round(1000 / _.f); // interval 23 24 /** @private Strategy pattern to determine animation behavior. */ 25 var S = function() { 26 /** @private percentComplete */ 27 var pc = function() { 28 var d = this.endTime - this.startTime; 29 var r = 1 - ((this.endTime - _.now()) / d); 30 var p = this.easing(r); 31 return Math.round(p * 1000) / 1000; 32 }; 33 return { 34 Border : function() {/* TODO */}, 35 Color : function() { 36 var p = pc.call(this); 37 var f = this.startVal; 38 var t = this.endVal; 39 f = (f.substring(0, 3) === "rgb") 40 ? JF.rgbStringToArray(f) 41 : JF.hexToRGB(f); 42 t = (t.substring(0, 3) === "rgb") 43 ? JF.rgbStringToArray(t) 44 : JF.hexToRGB(t); 45 var v = []; 46 for (var i = 0; i < 3; i++) { 47 var delta = Math.floor(t[i] - f[i]); 48 var current = Math.floor(p * delta); 49 v[i] = current + f[i]; 50 } 51 v = "rgb(" + v[0] + "," + v[1] + "," + v[2] + ")"; 52 jcs(this.element, this.property, v); 53 }, 54 ComboPx : function() { 55 var rx, getVal, x, y, v, x0, y0, p, deltaValue, currDelta, top, right, bottom, left, i; 56 switch (this.property) { 57 case "backgroundPositionX" : 58 rx = /\-?[0-9]+px/; 59 if (rx.test(this.startVal) && rx.test(this.endVal)) { 60 /** @private */ 61 getVal = function(val) { 62 return val.replace(/[^0-9\-]/g, ""); 63 }; 64 p = pc.call(this); 65 y0 = Jelo.css(this.element, "background-position-y"); 66 x = [getVal(this.startVal), getVal(this.endVal)]; 67 y = [getVal(y0), getVal(y0)]; 68 deltaValue = [pi(x[1]) - pi(x[0]), pi(y[1]) - pi(y[0])]; 69 currDelta = [Math.floor(p * deltaValue[0]), Math.floor(p * deltaValue[1])]; 70 v = [currDelta[0] + pi(x[0]), currDelta[1] + pi(y[0])]; 71 jcs(this.element, "background-position", v[0] + "px " + v[1] + "px"); 72 } 73 break; 74 case "backgroundPositionY" : 75 rx = /\-?[0-9]+px/; 76 if (rx.test(this.startVal) && rx.test(this.endVal)) { 77 /** @private */ 78 getVal = function(val) { 79 return val.replace(/[^0-9\-]/g, ""); 80 }; 81 p = pc.call(this); 82 x0 = Jelo.css(this.element, "background-position-x"); 83 x = [getVal(x0), getVal(x0)]; 84 y = [getVal(this.startVal), getVal(this.endVal)]; 85 deltaValue = [pi(x[1]) - pi(x[0]), pi(y[1]) - pi(y[0])]; 86 currDelta = [Math.floor(p * deltaValue[0]), Math.floor(p * deltaValue[1])]; 87 v = [currDelta[0] + pi(x[0]), currDelta[1] + pi(y[0])]; 88 jcs(this.element, "background-position", v[0] + "px " + v[1] + "px"); 89 } 90 break; 91 case "backgroundPosition" : 92 rx = /\-?[0-9]+px \-?[0-9]+px/; 93 if (rx.test(this.startVal) && rx.test(this.endVal)) { 94 /** @private */ 95 getVal = function(val, index) { 96 return val.split(" ")[index].replace(/[^0-9\-]/g, ""); 97 }; 98 p = pc.call(this); 99 x = [getVal(this.startVal, 0), getVal(this.endVal, 0)]; 100 y = [getVal(this.startVal, 1), getVal(this.endVal, 1)]; 101 deltaValue = [pi(x[1]) - pi(x[0]), pi(y[1]) - pi(y[0])]; 102 currDelta = [Math.floor(p * deltaValue[0]), Math.floor(p * deltaValue[1])]; 103 v = [currDelta[0] + pi(x[0]), currDelta[1] + pi(y[0])]; 104 jcs(this.element, this.property, v[0] + "px " + v[1] + "px"); 105 } 106 break; 107 default : 108 rx = /\-?[0-9]+px \-?[0-9]+px \-?[0-9]+px \-?[0-9]+px/; 109 if (rx.test(this.startVal) && rx.test(this.endVal)) { 110 /** @private */ 111 getVal = function(val, index) { 112 return val.split(" ")[index].replace(/[^0-9\-]/g, ""); 113 }; 114 p = pc.call(this); 115 top = [getVal(this.startVal, 0), getVal(this.endVal, 0)]; 116 right = [getVal(this.startVal, 1), getVal(this.endVal, 1)]; 117 bottom = [getVal(this.startVal, 2), getVal(this.endVal, 2)]; 118 left = [getVal(this.startVal, 3), getVal(this.endVal, 3)]; 119 deltaValue = [pi(top[1]) - pi(top[0]), pi(right[1]) - pi(right[0]), 120 pi(bottom[1]) - pi(bottom[0]), pi(left[1]) - pi(left[0])]; 121 currDelta = []; 122 for (i = 0; i < 4; i++) { 123 currDelta[i] = Math.floor(p * deltaValue[i]); 124 } 125 v = [currDelta[0] + pi(top[0]), currDelta[1] + pi(right[0]), currDelta[2] + pi(bottom[0]), 126 currDelta[3] + pi(left[0])]; 127 jcs(this.element, this.property, v[0] + "px " + v[1] + "px " + v[2] + "px " + v[3] + "px"); 128 } 129 } 130 }, 131 Numerical : function() { 132 var p = pc.call(this); 133 var deltaValue = parseFloat(this.endVal, 10) - parseFloat(this.startVal, 10); 134 var currDelta = p * deltaValue; 135 var v = currDelta + parseFloat(this.startVal, 10); 136 switch (this.property) { 137 case "opacity" : 138 v = parseFloat(v); 139 if (v < 0) { 140 v = 0; 141 } 142 if (v > 1) { 143 v = 1; 144 } 145 break; 146 case "zIndex" : 147 v = pi(v); 148 break; 149 } 150 jcs(this.element, this.property, v); 151 }, 152 NumericalPx : function() { 153 var p = pc.call(this); 154 var deltaValue = pi(this.endVal) - pi(this.startVal); 155 var currDelta = Math.floor(p * deltaValue); 156 var v = currDelta + pi(this.startVal); 157 jcs(this.element, this.property, v + this.unit); 158 } 159 }; 160 }(); 161 162 /** @private Strategy pattern to determine easing behavior */ 163 var Easing = { 164 LINEAR : function(x) { // no easing 165 return x; 166 }, 167 IN : function(x) { // accelerate 168 return Math.pow(x, 3); 169 }, 170 OUT : function(x) { // decelerate 171 return 1 - Math.pow(1 - x, 3); 172 }, 173 SMOOTH : function(x) { // accelerate then decelerate 174 return x < 0.5 175 ? 2 * x * x 176 : -2 * Math.pow(x - 1, 2) + 1; 177 }, 178 OVERSHOOT : function(x) { // go past, then come back 179 var s = 1.70158; 180 return (x -= 1) * x * ((s + 1) * x + s) + 1; 181 }, 182 SPRING : function(x) { // repeated overshoot 183 return 1 - (Math.cos(x * 4.5 * Math.PI) * Math.exp(-x * 6)); 184 }, 185 WOBBLE : function(x) { // forward, then back, then forward 186 return (-Math.cos(3 * x * Math.PI) / 2) + 0.5; 187 } 188 }; 189 190 /** @private accessible as Jelo.Anim.ate */ 191 var animate = function(config) { 192 if (!config) { 193 return; 194 } 195 196 if (Jelo.Valid.isArray(config.me)) { 197 var cfg = config; 198 Jelo.each(config.me, function() { 199 cfg.me = this; 200 animate(cfg); 201 }); 202 return; 203 } 204 205 // validate the animation target element 206 var m = config.me || false; 207 if (!m) { 208 throw new Error('Jelo.Anim.ate: Missing required configuration option me:HTMLElement|String'); 209 } else if (m.isArray) { 210 m = m[0]; 211 } else if (typeof m === "string") { 212 m = Jelo.Dom.selectNode(m); 213 } 214 if (!m) { 215 // need to reverify after the "else" clauses 216 throw new Error('Jelo.Anim.ate: Missing required configuration option me:HTMLElement|String'); 217 } 218 219 // validate the css property to animate 220 var css = config.css || false; 221 if (!css || (typeof css !== "string")) { 222 throw new Error('Jelo.Anim.ate: Missing required configuration option css:String'); 223 } 224 var c = JF.hyphenatedToCamelCase(css); 225 226 // validate the starting value 227 var f = config.from; 228 if (!f && typeof f !== "number") { 229 f = jcg(m, c); 230 if (!f && typeof f !== "number") { 231 f = jcg(m, css); 232 } 233 if (!f && typeof f !== "number") { 234 f = 0; // specify config.from to override this behavior 235 } 236 } 237 if (f === "auto") { 238 f = 0; // specify config.from to override this behavior 239 } 240 241 // validate the ending value 242 var t = '' + config.to; 243 if (!t && typeof t !== "number") { 244 throw new Error('Jelo.Anim.ate() Missing required configuration option to:String|Number'); 245 } 246 247 // prepare the pre- and post-animation callback functions 248 var b = (typeof config.before === "function") 249 ? config.before 250 : Jelo.emptyFn; 251 var a = (typeof config.after === "function") 252 ? config.after 253 : Jelo.emptyFn; 254 255 // validate the animation duration 256 var d = parseFloat(config.duration); 257 if (isNaN(d)) { 258 d = _.d; 259 } 260 d = Math.floor(Math.abs(d * 1000)); // convert s to ms 261 262 // validate the animation easing method 263 var e = config.easing || Jelo.Anim.Easing.LINEAR; 264 if ((typeof e === "string") && e.length) { 265 e = Jelo.Anim.Easing[e.toUpperCase()] || Jelo.Anim.Easing.LINEAR; 266 } else if (typeof e != "function") { 267 throw new Error("Jelo.Anim.ate: Easing must be a string such as 'linear', 'out', etc. or a function."); 268 } 269 270 // clean up animation conflicts, also improves performance 271 var temp = []; 272 for (var i = 0; i < _.a.length; i++) { 273 var ti = _.a[i]; 274 if ((ti.element != m) || (JF.hyphenatedToCamelCase(ti.property) != c)) { 275 temp.push(ti); 276 } 277 } 278 _.a = temp; 279 280 // strategy pattern to get an animation function 281 var animFn = function(c) { 282 switch (c) { 283 case "border" : 284 case "borderBottom" : 285 case "borderLeft" : 286 case "borderRight" : 287 case "borderTop" : 288 return S.Border; 289 case "backgroundColor" : 290 case "color" : 291 return S.Color; 292 case "backgroundPositionX" : 293 case "backgroundPositionY" : 294 case "backgroundPosition" : 295 case "margin" : 296 case "padding" : 297 return S.ComboPx; 298 case "lineHeight" : 299 case "opacity" : 300 case "zIndex" : 301 return S.Numerical; 302 case "bottom" : 303 case "height" : 304 case "left" : 305 case "marginBottom" : 306 case "marginLeft" : 307 case "marginRight" : 308 case "marginTop" : 309 case "paddingBottom" : 310 case "paddingLeft" : 311 case "paddingRight" : 312 case "paddingTop" : 313 case "right" : 314 case "top" : 315 case "width" : 316 return S.NumericalPx; 317 default : 318 return Jelo.emptyFn; 319 } 320 }(c); 321 322 // build the actual animation object 323 var animObj = { 324 // for convenience in before and after functions 325 me : config.me, 326 css : config.css, 327 duration : config.duration, 328 before : config.before, 329 after : config.after, 330 from : config.from, 331 to : config.to, 332 333 // used internally 334 element : m, 335 property : c, 336 startVal : f, 337 endVal : t, 338 callback : a, 339 startTime : _.now(), 340 endTime : _.now() + d, 341 fn : animFn, 342 easing : e, 343 unit : function() { 344 var unit = t.replace(/(\-?[0-9]+)(.*)?/, '$2'); 345 return unit.length 346 ? unit 347 : 'px'; 348 }() 349 }; 350 _.a.push(animObj); 351 352 b.call(animObj); 353 run(); 354 }; 355 356 /** @private Handles each frame of animation. */ 357 var run = function() { 358 var /* counter */i, /* callbacks */c, /* r[i] */ri, /* t[i] */ti, /* value */v, /* timer */t; 359 if (!_.t) { 360 _.t = setInterval(function() { 361 if (_.r.length) { 362 c = []; 363 for (i = _.r.length - 1; i >= 0; i--) { 364 ri = _.r[i]; 365 ti = _.a[ri]; 366 v = null; 367 switch (ti.fn) { 368 case S.Border : 369 v = ti.endVal; // TODO: doublecheck this 370 // assignment 371 break; 372 case S.ComboPx : 373 v = ti.endVal; // TODO: doublecheck this 374 // assignment 375 break; 376 case S.Color : 377 v = "#" + ti.endVal.replace(/#/, ""); 378 break; 379 case S.NumericalPx : 380 v = pi(ti.endVal) + ti.unit; 381 break; 382 case S.Numerical : 383 v = parseFloat(ti.endVal); 384 break; 385 default : 386 v = ti.endVal; 387 } 388 jcs(ti.element, ti.property, v); 389 c.push(ti); 390 _.a.splice(ri, 1); 391 } 392 _.r = []; 393 for (i = 0; i < c.length; i++) { 394 ti = c[i]; 395 ti.callback.call(ti); 396 } 397 } 398 if (_.a.length) { 399 for (i = 0; i < _.a.length; i++) { 400 t = _.a[i]; 401 if (_.now() < t.endTime) { 402 t.fn.call(t); 403 } else { 404 _.r.push(i); 405 } 406 } 407 } 408 }, _.i); 409 } 410 }; 411 412 /** @scope Jelo.Anim */ 413 return { 414 /** 415 * Easing functions. 416 * <ul> 417 * <li>LINEAR: no easing, default</li> 418 * <li>IN: accelerate (speed up)</li> 419 * <li>OUT: decelerate (slow down)</li> 420 * <li>SMOOTH: accelerate, then decelerate</li> 421 * <li>OVERSHOOT: go past the end value, then come back. occasionally causes errors in IE depending on the 422 * animated property.</li> 423 * <li>SPRING: repeated overshoot around the end value</li> 424 * <li>WOBBLE: forward, back, then forward again in a single duration</li> 425 * </ul> 426 * 427 * @type Object 428 */ 429 Easing : Easing, 430 /** 431 * Animate a CSS property of a given element. 432 * 433 * @function 434 * @param {Object} config A configuration object. 435 * @param {HTMLElement|String} config.me Object to animate, or a CSS selector for which the first matching 436 * element will be used. 437 * @param {String} config.css Property to animate. 438 * @param {String|Number} [config.from] Starting value for the animation. 439 * @param config.to {String|Number} Ending value for the animation 440 * @param {Function} [config.before] Method to invoke immediately before the animation starts. 441 * @param {Function} [config.after] Method to invoke immediately after the animation finishes. 442 * @param {Number} [config.duration=0.5] How many seconds the animation should last. 443 * @param {Function|String} [config.easing="linear"] How to calculate property values. If a string is supplied, 444 * it must match a Jelo.Anim.Easing property name (case insensitive). 445 */ 446 ate : animate, 447 /** 448 * @function 449 * @return {Boolean} True if there are currently animations in the queue. 450 */ 451 ating : function() { 452 return !!_.a.length; 453 }, 454 /** 455 * Immediately halts and cancels all pending animations. References are not stored, the animations are gone. 456 */ 457 stopAll : function() { 458 _.r = []; 459 _.a = []; 460 if (_.t) { 461 clearTimeout(_.t); 462 _.t = null; 463 } 464 }, 465 /** 466 * Changes the default animation duration, used whenever the duration property is not explicitly set. 467 * 468 * @param {Number} Seconds, as a whole or decimal number. 469 */ 470 setDefaultDuration : function(d) { 471 if (Jelo.Valid.isNumber(d)) { 472 _.d = d; 473 } 474 } 475 }; 476 }(); 477