ux/FlipClock.js
[roojs1] / ux / FlipCounter.js
1 /**
2  * Apple-Style Flip Counter
3  * Version 0.5.3 - May 7, 2011
4  *
5  * Copyright (c) 2010 Chris Nanney
6  * http://cnanney.com/journal/code/apple-style-counter-revisited/
7  *
8  * Licensed under MIT
9  * http://www.opensource.org/licenses/mit-license.php
10  */
11
12 var flipCounter = function(d, options){
13
14         // Default values
15         var defaults = {
16                 value: 0,
17                 inc: 1,
18                 pace: 1000,
19                 auto: true,
20                 tFH: 39,
21                 bFH: 64,
22                 fW: 53,
23                 bOffset: 390
24         };
25
26         var     doc = window.document,
27         divId = typeof d !== 'undefined' && d !== '' ? d : 'flip-counter',
28         div = doc.getElementById(divId);
29
30     var o = {};
31     for (var opt in defaults) {
32         o[opt] = (opt in options) ? options[opt] : defaults[opt];
33     }
34
35         var digitsOld = [], digitsNew = [], subStart, subEnd, x, y, nextCount = null, newDigit, newComma,
36         best = {
37                 q: null,
38                 pace: 0,
39                 inc: 0
40         };
41
42         /**
43          * Sets the value of the counter and animates the digits to new value.
44          *
45          * Example: myCounter.setValue(500); would set the value of the counter to 500,
46          * no matter what value it was previously.
47          *
48          * @param {int} n
49          *   New counter value
50          */
51         this.setValue = function(n){
52                 if (isNumber(n)){
53                         x = o.value;
54                         y = n;
55                         o.value = n;
56                         digitCheck(x,y);
57                 }
58                 return this;
59         };
60
61         /**
62          * Sets the increment for the counter. Does NOT animate digits.
63          */
64         this.setIncrement = function(n){
65                 o.inc = isNumber(n) ? n : defaults.inc;
66                 return this;
67         };
68
69         /**
70          * Sets the pace of the counter. Only affects counter when auto == true.
71          *
72          * @param {int} n
73          *   New pace for counter in milliseconds
74          */
75         this.setPace = function(n){
76                 o.pace = isNumber(n) ? n : defaults.pace;
77                 return this;
78         };
79
80         /**
81          * Sets counter to auto-incrememnt (true) or not (false).
82          *
83          * @param {bool} a
84          *   Should counter auto-increment, true or false
85          */
86         this.setAuto = function(a){
87                 if (a && ! o.auto){
88                         o.auto = true;
89                         doCount();
90                 }
91                 if (! a && o.auto){
92                         if (nextCount) clearNext();
93                         o.auto = false;
94                 }
95                 return this;
96         };
97
98         /**
99          * Increments counter by one animation based on set 'inc' value.
100          */
101         this.step = function(){
102                 if (! o.auto) doCount();
103                 return this;
104         };
105
106         /**
107          * Adds a number to the counter value, not affecting the 'inc' or 'pace' of the counter.
108          *
109          * @param {int} n
110          *   Number to add to counter value
111          */
112         this.add = function(n){
113                 if (isNumber(n)){
114                         x = o.value;
115                         o.value += n;
116                         y = o.value;
117                         digitCheck(x,y);
118                 }
119                 return this;
120         };
121
122         /**
123          * Subtracts a number from the counter value, not affecting the 'inc' or 'pace' of the counter.
124          *
125          * @param {int} n
126          *   Number to subtract from counter value
127          */
128         this.subtract = function(n){
129                 if (isNumber(n)){
130                         x = o.value;
131                         o.value -= n;
132                         if (o.value >= 0){
133                                 y = o.value;
134                         }
135                         else{
136                                 y = "0";
137                                 o.value = 0;
138                         }
139                         digitCheck(x,y);
140                 }
141                 return this;
142         };
143
144         /**
145          * Increments counter to given value, animating by current pace and increment.
146          *
147          * @param {int} n
148          *   Number to increment to
149          * @param {int} t (optional)
150          *   Time duration in seconds - makes increment a 'smart' increment
151          * @param {int} p (optional)
152          *   Desired pace for counter if 'smart' increment
153          */
154         this.incrementTo = function(n, t, p){
155                 if (nextCount) clearNext();
156
157                 // Smart increment
158                 if (typeof t != 'undefined'){
159                         var time = isNumber(t) ? t * 1000 : 10000,
160                         pace = typeof p != 'undefined' && isNumber(p) ? p : o.pace,
161                         diff = typeof n != 'undefined' && isNumber(n) ? n - o.value : 0,
162                         cycles, inc, check, i = 0;
163                         best.q = null;
164
165                         // Initial best guess
166                         pace = (time / diff > pace) ? Math.round((time / diff) / 10) * 10 : pace;
167                         cycles = Math.floor(time / pace);
168                         inc = Math.floor(diff / cycles);
169
170                         check = checkSmartValues(diff, cycles, inc, pace, time);
171
172                         if (diff > 0){
173                                 while (check.result === false && i < 100){
174                                         pace += 10;
175                                         cycles = Math.floor(time / pace);
176                                         inc = Math.floor(diff / cycles);
177
178                                         check = checkSmartValues(diff, cycles, inc, pace, time);
179                                         i++;
180                                 }
181
182                                 if (i == 100){
183                                         // Could not find optimal settings, use best found so far
184                                         o.inc = best.inc;
185                                         o.pace = best.pace;
186                                 }
187                                 else{
188                                         // Optimal settings found, use those
189                                         o.inc = inc;
190                                         o.pace = pace;
191                                 }
192
193                                 doIncrement(n, true, cycles);
194                         }
195
196                 }
197                 // Regular increment
198                 else{
199                         doIncrement(n);
200                 }
201
202         }
203
204         /**
205          * Gets current value of counter.
206          */
207         this.getValue = function(){
208                 return o.value;
209         }
210
211         /**
212          * Stops all running increments.
213          */
214         this.stop = function(){
215                 if (nextCount) clearNext();
216                 return this;
217         }
218
219         //---------------------------------------------------------------------------//
220
221         function doCount(){
222                 x = o.value;
223                 o.value += o.inc;
224                 y = o.value;
225                 digitCheck(x,y);
226                 if (o.auto === true) nextCount = setTimeout(doCount, o.pace);
227         }
228
229         function doIncrement(n, s, c){
230                 var val = o.value,
231                 smart = (typeof s == 'undefined') ? false : s,
232                 cycles = (typeof c == 'undefined') ? 1 : c;
233
234                 if (smart === true) cycles--;
235
236                 if (val != n){
237                         x = o.value,
238                         o.auto = true;
239
240                         if (val + o.inc <= n && cycles != 0) val += o.inc
241                         else val = n;
242
243                         o.value = val;
244                         y = o.value;
245
246                         digitCheck(x,y);
247                         nextCount = setTimeout(function(){doIncrement(n, smart, cycles)}, o.pace);
248                 }
249                 else o.auto = false;
250         }
251
252         function digitCheck(x,y){
253                 digitsOld = splitToArray(x);
254                 digitsNew = splitToArray(y);
255                 var diff,
256                 xlen = digitsOld.length,
257                 ylen = digitsNew.length;
258                 if (ylen > xlen){
259                         diff = ylen - xlen;
260                         while (diff > 0){
261                                 addDigit(ylen - diff + 1, digitsNew[ylen - diff]);
262                                 diff--;
263                         }
264                 }
265                 if (ylen < xlen){
266                         diff = xlen - ylen;
267                         while (diff > 0){
268                                 removeDigit(xlen - diff);
269                                 diff--;
270                         }
271                 }
272                 for (var i = 0; i < xlen; i++){
273                         if (digitsNew[i] != digitsOld[i]){
274                                 animateDigit(i, digitsOld[i], digitsNew[i]);
275                         }
276                 }
277         }
278
279         function animateDigit(n, oldDigit, newDigit){
280                 var speed, step = 0, w, a,
281                 bp = [
282                         '-' + o.fW + 'px -' + (oldDigit * o.tFH) + 'px',
283                         (o.fW * -2) + 'px -' + (oldDigit * o.tFH) + 'px',
284                         '0 -' + (newDigit * o.tFH) + 'px',
285                         '-' + o.fW + 'px -' + (oldDigit * o.bFH + o.bOffset) + 'px',
286                         (o.fW * -2) + 'px -' + (newDigit * o.bFH + o.bOffset) + 'px',
287                         (o.fW * -3) + 'px -' + (newDigit * o.bFH + o.bOffset) + 'px',
288                         '0 -' + (newDigit * o.bFH + o.bOffset) + 'px'
289                 ];
290
291                 if (o.auto === true && o.pace <= 300){
292                         switch (n){
293                                 case 0:
294                                         speed = o.pace/6;
295                                         break;
296                                 case 1:
297                                         speed = o.pace/5;
298                                         break;
299                                 case 2:
300                                         speed = o.pace/4;
301                                         break;
302                                 case 3:
303                                         speed = o.pace/3;
304                                         break;
305                                 default:
306                                         speed = o.pace/1.5;
307                                         break;
308                         }
309                 }
310                 else{
311                         speed = 80;
312                 }
313                 // Cap on slowest animation can go
314                 speed = (speed > 80) ? 80 : speed;
315
316                 function animate(){
317                         if (step < 7){
318                                 w = step < 3 ? 't' : 'b';
319                                 a = doc.getElementById(divId + "_" + w + "_d" + n);
320                                 if (a) a.style.backgroundPosition = bp[step];
321                                 step++;
322                                 if (step != 3) setTimeout(animate, speed);
323                                 else animate();
324                         }
325                 }
326
327                 animate();
328         }
329
330         // Creates array of digits for easier manipulation
331         function splitToArray(input){
332                 return input.toString().split("").reverse();
333         }
334
335         // Adds new digit
336         function addDigit(len, digit){
337                 var li = Number(len) - 1;
338                 newDigit = doc.createElement("ul");
339                 newDigit.className = 'cd';
340                 newDigit.id = divId + '_d' + li;
341                 newDigit.innerHTML = '<li class="t" id="' + divId + '_t_d' + li + '"></li><li class="b" id="' + divId + '_b_d' + li + '"></li>';
342
343                 if (li % 3 == 0){
344                         newComma = doc.createElement("ul");
345                         newComma.className = 'cd';
346                         newComma.innerHTML = '<li class="s"></li>';
347                         div.insertBefore(newComma, div.firstChild);
348                 }
349
350                 div.insertBefore(newDigit, div.firstChild);
351                 doc.getElementById(divId + "_t_d" + li).style.backgroundPosition = '0 -' + (digit * o.tFH) + 'px';
352                 doc.getElementById(divId + "_b_d" + li).style.backgroundPosition = '0 -' + (digit * o.bFH + o.bOffset) + 'px';
353         }
354
355         // Removes digit
356         function removeDigit(id){
357                 var remove = doc.getElementById(divId + "_d" + id);
358                 div.removeChild(remove);
359
360                 // Check for leading comma
361                 var first = div.firstChild.firstChild;
362                 if ((" " + first.className + " ").indexOf(" s ") > -1 ){
363                         remove = first.parentNode;
364                         div.removeChild(remove);
365                 }
366         }
367
368         // Sets the correct digits on load
369         function initialDigitCheck(init){
370                 // Creates the right number of digits
371                 var initial = init.toString(),
372                 count = initial.length,
373                 bit = 1, i;
374                 for (i = 0; i < count; i++){
375                         newDigit = doc.createElement("ul");
376                         newDigit.className = 'cd';
377                         newDigit.id = divId + '_d' + i;
378                         newDigit.innerHTML = newDigit.innerHTML = '<li class="t" id="' + divId + '_t_d' + i + '"></li><li class="b" id="' + divId + '_b_d' + i + '"></li>';
379                         div.insertBefore(newDigit, div.firstChild);
380                         if (bit != (count) && bit % 3 == 0){
381                                 newComma = doc.createElement("ul");
382                                 newComma.className = 'cd';
383                                 newComma.innerHTML = '<li class="s"></li>';
384                                 div.insertBefore(newComma, div.firstChild);
385                         }
386                         bit++;
387                 }
388                 // Sets them to the right number
389                 var digits = splitToArray(initial);
390                 for (i = 0; i < count; i++){
391                         doc.getElementById(divId + "_t_d" + i).style.backgroundPosition = '0 -' + (digits[i] * o.tFH) + 'px';
392                         doc.getElementById(divId + "_b_d" + i).style.backgroundPosition = '0 -' + (digits[i] * o.bFH + o.bOffset) + 'px';
393                 }
394                 // Do first animation
395                 if (o.auto === true) nextCount = setTimeout(doCount, o.pace);
396         }
397
398         // Checks values for smart increment and creates debug text
399         function checkSmartValues(diff, cycles, inc, pace, time){
400                 var r = {result: true}, q;
401                 // Test conditions, all must pass to continue:
402                 // 1: Unrounded inc value needs to be at least 1
403                 r.cond1 = (diff / cycles >= 1) ? true : false;
404                 // 2: Don't want to overshoot the target number
405                 r.cond2 = (cycles * inc <= diff) ? true : false;
406                 // 3: Want to be within 10 of the target number
407                 r.cond3 = (Math.abs(cycles * inc - diff) <= 10) ? true : false;
408                 // 4: Total time should be within 100ms of target time.
409                 r.cond4 = (Math.abs(cycles * pace - time) <= 100) ? true : false;
410                 // 5: Calculated time should not be over target time
411                 r.cond5 = (cycles * pace <= time) ? true : false;
412
413                 // Keep track of 'good enough' values in case can't find best one within 100 loops
414                 if (r.cond1 && r.cond2 && r.cond4 && r.cond5){
415                         q = Math.abs(diff - (cycles * inc)) + Math.abs(cycles * pace - time);
416                         if (best.q === null) best.q = q;
417                         if (q <= best.q){
418                                 best.pace = pace;
419                                 best.inc = inc;
420                         }
421                 }
422
423                 for (var i = 1; i <= 5; i++){
424                         if (r['cond' + i] === false){
425                                 r.result = false;
426                         }
427                 }
428                 return r;
429         }
430
431         // http://stackoverflow.com/questions/18082/validate-numbers-in-javascript-isnumeric/1830844
432         function isNumber(n) {
433                 return !isNaN(parseFloat(n)) && isFinite(n);
434         }
435
436         function clearNext(){
437                 clearTimeout(nextCount);
438                 nextCount = null;
439         }
440
441         // Start it up
442         initialDigitCheck(o.value);
443 };