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