├── CHANGELOG ├── digits.css ├── README.md ├── digits-default.css ├── digits-white.css └── digits.js /CHANGELOG: -------------------------------------------------------------------------------- 1 | 2 | Upcoming Versions 3 | ----------------- 4 | 5 | - More skins 6 | 7 | - Fire events on specific ranges 8 | 9 | - Use the Page Visiblity API 10 | 11 | - Responsive 12 | 13 | - IE Support + Safari 5 + Others 14 | 15 | 16 | 17 | 0.9.1 (December 6, 2012) 18 | ----------------------- 19 | 20 | - Fixed initial delay -- timer may be off by a few milliseconds 21 | 22 | - Cleanup 23 | 24 | 25 | 26 | 0.9 (November 20, 2012) 27 | ----------------------- 28 | 29 | - Countdown and Statistics mode 30 | -------------------------------------------------------------------------------- /digits.css: -------------------------------------------------------------------------------- 1 | 2 | /* CORE CSS AND ANIMATIONS 3 | -------------------------------------------------*/ 4 | 5 | .digits, 6 | .digits .digit { 7 | display: inline-block; 8 | } 9 | 10 | .digits .top-half-wrapper, 11 | .digits .bottom-half-wrapper { 12 | position: relative; 13 | } 14 | 15 | .digits .top-half, 16 | .digits .bottom-half { 17 | position: absolute; 18 | overflow: hidden; 19 | top: 0; 20 | left: 0; 21 | } 22 | 23 | .digits .top-half { 24 | -webkit-transform-origin: 50% 100%; 25 | -moz-transform-origin: 50% 100%; 26 | 27 | -webkit-transform: perspective(300) rotateX(0deg); 28 | -moz-transform: rotateX(0deg); 29 | } 30 | 31 | .digits .bottom-half { 32 | line-height: 0; 33 | 34 | -webkit-transform-origin: 50% 0%; 35 | -moz-transform-origin: 50% 0%; 36 | 37 | -webkit-transform: perspective(300) rotateX(90deg); 38 | -moz-transform: rotateX(90deg); 39 | } 40 | 41 | .digits .no-animation { 42 | -webkit-transition: none !important; 43 | -moz-transition: none !important; 44 | transition: none !important; 45 | } 46 | 47 | .digits .show { 48 | z-index: 10; 49 | 50 | -webkit-transform: perspective(300) rotateX(0deg); 51 | -moz-transform: rotateX(0deg); 52 | } 53 | 54 | .digits .roll-over { 55 | -webkit-transform: perspective(300) rotateX(-90deg); 56 | -moz-transform: rotateX(-90deg); 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Digits 3 | ==== 4 | 5 | [Demo](http://www.kennethcachia.com/digits) 6 | 7 | Includes 8 | --- 9 | 10 | **Core Files** 11 | 12 | 13 | 14 | 15 | 16 | **Skins** 17 | 18 | Choose one from the following skins: 19 | 20 | 21 | 22 | 23 | 24 | Mode 1: Countdown 25 | --- 26 | 27 | Countdown to a specific date. 28 | 29 | new Digits({ 30 | wrapper: '#wrapper', 31 | mode: 'countdown', 32 | to: 'January 1 2013 00:00:00', 33 | labels: true, 34 | ready: function () { 35 | alert('Happy New Year!'); 36 | } 37 | }); 38 | 39 | **wrapper**: class/ID of (empty) element where digits are displayed 40 | 41 | **to**: countdown to this date 42 | 43 | **labels**: show/hide labels (Days, Hours, Minutes, Seconds) 44 | 45 | **ready**: function to be executed when counter reaches zero 46 | 47 | Mode 2: Statistics 48 | --- 49 | 50 | Start off with an initial value. Can also be changed at runtime. 51 | 52 | var stats = new Digits({ 53 | wrapper: '#wrapper', 54 | mode: 'statistics', 55 | value: 199 56 | }); 57 | 58 | **wrapper**: class/ID of (empty) element where digits are displayed 59 | 60 | **value**: initial value 61 | 62 | 63 | Value can be changed at runtime using: 64 | 65 | stats.changeValue(240); 66 | 67 | -------------------------------------------------------------------------------- /digits-default.css: -------------------------------------------------------------------------------- 1 | 2 | /* DEFAULT SKIN 3 | -------------------------------------------------*/ 4 | 5 | .digits .top-half-wrapper, 6 | .digits .bottom-half-wrapper { 7 | width: 100px; 8 | height: 50px; 9 | } 10 | 11 | .digits .top-half, 12 | .digits .bottom-half { 13 | width: 100px; 14 | height: 50px; 15 | text-shadow: 0px 1px 0px #000; 16 | text-align: center; 17 | font-family: helvetica, sans; 18 | font-weight: bold; 19 | font-size: 80px; 20 | } 21 | 22 | .digits .top-half { 23 | color: #fff; 24 | } 25 | 26 | .digits .bottom-half { 27 | color: #f1f1f1; 28 | } 29 | 30 | .digits .top-half { 31 | background: #525252; 32 | border-radius: 10px 10px 0px 0px; 33 | line-height: 100px; 34 | } 35 | 36 | .digits .bottom-half { 37 | background: #333; 38 | border-radius: 0px 0px 10px 10px ; 39 | } 40 | 41 | 42 | /* COUNTDOWN MODE 43 | -------------------------------------------------*/ 44 | 45 | .digits.countdown .digit:not(:last-child) { 46 | margin-right: 10px; 47 | } 48 | 49 | .digits.countdown .digit.second-2 { 50 | margin-right: 0; 51 | } 52 | 53 | .digits.countdown .digit.second-1, 54 | .digits.countdown .digit.minute-1, 55 | .digits.countdown .digit.hour-1 { 56 | margin-left: 50px; 57 | } 58 | 59 | 60 | /* STATISTICS MODE 61 | -------------------------------------------------*/ 62 | 63 | .digits.statistics .digit:not(:last-of-type) { 64 | margin-right: 5px; 65 | } 66 | 67 | 68 | /* LABELS 69 | -------------------------------------------------*/ 70 | 71 | .digits .labels { 72 | text-align: right; 73 | color: #111; 74 | } 75 | 76 | .digits .label { 77 | display: inline-block; 78 | width: 210px; 79 | text-align: center; 80 | font-family: helvetica, sans; 81 | font-size: 11px; 82 | letter-spacing: 1px; 83 | font-weight: bold; 84 | text-transform: uppercase; 85 | margin: 20px 0; 86 | } 87 | 88 | .digits .label.hours, 89 | .digits .label.minutes, 90 | .digits .label.seconds { 91 | margin-left: 60px; 92 | } 93 | 94 | .digits .label.days:after { 95 | content: "Days"; 96 | } 97 | 98 | .digits .label.hours:after { 99 | content: "Hours"; 100 | } 101 | 102 | .digits .label.minutes:after { 103 | content: "Minutes"; 104 | } 105 | 106 | .digits .label.seconds:after { 107 | content: "Seconds"; 108 | } 109 | -------------------------------------------------------------------------------- /digits-white.css: -------------------------------------------------------------------------------- 1 | 2 | /* DEFAULT SKIN 3 | -------------------------------------------------*/ 4 | 5 | .digits .top-half-wrapper, 6 | .digits .bottom-half-wrapper { 7 | width: 100px; 8 | height: 50px; 9 | } 10 | 11 | .digits .top-half, 12 | .digits .bottom-half { 13 | width: 100px; 14 | height: 50px; 15 | text-shadow: 0px 1px 0px #fafafa; 16 | text-align: center; 17 | font-family: helvetica, sans; 18 | font-weight: bold; 19 | font-size: 80px; 20 | } 21 | 22 | .digits .top-half { 23 | color: #333; 24 | } 25 | 26 | .digits .bottom-half { 27 | color: #222; 28 | } 29 | 30 | .digits .top-half { 31 | background: #fff; 32 | border-radius: 10px 10px 0px 0px; 33 | line-height: 100px; 34 | } 35 | 36 | .digits .bottom-half { 37 | background: #e5e5e5; 38 | border-radius: 0px 0px 10px 10px ; 39 | } 40 | 41 | 42 | /* COUNTDOWN MODE 43 | -------------------------------------------------*/ 44 | 45 | .digits.countdown .digit:not(:last-child) { 46 | margin-right: 10px; 47 | } 48 | 49 | .digits.countdown .digit.second-2 { 50 | margin-right: 0; 51 | } 52 | 53 | .digits.countdown .digit.second-1, 54 | .digits.countdown .digit.minute-1, 55 | .digits.countdown .digit.hour-1 { 56 | margin-left: 50px; 57 | } 58 | 59 | 60 | /* STATISTICS MODE 61 | -------------------------------------------------*/ 62 | 63 | .digits.statistics .digit:not(:last-of-type) { 64 | margin-right: 5px; 65 | } 66 | 67 | 68 | /* LABELS 69 | -------------------------------------------------*/ 70 | 71 | .digits .labels { 72 | text-align: right; 73 | color: #111; 74 | } 75 | 76 | .digits .label { 77 | display: inline-block; 78 | width: 210px; 79 | text-align: center; 80 | font-family: helvetica, sans; 81 | font-size: 11px; 82 | letter-spacing: 1px; 83 | font-weight: bold; 84 | text-transform: uppercase; 85 | margin: 20px 0; 86 | } 87 | 88 | .digits .label.hours, 89 | .digits .label.minutes, 90 | .digits .label.seconds { 91 | margin-left: 60px; 92 | } 93 | 94 | .digits .label.days:after { 95 | content: "Days"; 96 | } 97 | 98 | .digits .label.hours:after { 99 | content: "Hours"; 100 | } 101 | 102 | .digits .label.minutes:after { 103 | content: "Minutes"; 104 | } 105 | 106 | .digits .label.seconds:after { 107 | content: "Seconds"; 108 | } 109 | -------------------------------------------------------------------------------- /digits.js: -------------------------------------------------------------------------------- 1 | 2 | // Basic JS Operations 3 | var JS = { 4 | createElement: function (params) { 5 | var el = document.createElement(params.type), 6 | i; 7 | 8 | if (params.classes) { 9 | for (i = 0; i < params.classes.length; i += 1) { 10 | el.classList.add(params.classes[i]); 11 | } 12 | } 13 | 14 | if (params.styles) { 15 | for (i = 0; i < params.styles.length; i += 1) { 16 | el.style[params.styles[i][0]] = params.styles[i][1]; 17 | } 18 | } 19 | 20 | if (params.parent) { 21 | params.parent.appendChild(el); 22 | } 23 | 24 | return el; 25 | }, 26 | 27 | class: function (params) { 28 | var els = params.parent.querySelectorAll(params.selector), 29 | e; 30 | 31 | for (e = 0; e < els.length; e += 1) { 32 | if (params.mode === 'remove') { 33 | els[e].classList.remove(params.className); 34 | } else if (params.mode === 'add') { 35 | els[e].classList.add(params.className); 36 | } 37 | } 38 | } 39 | }; 40 | 41 | 42 | // Individual Digit 43 | var Digit = function (params) { 44 | var current, 45 | topHalfWrapper, 46 | bottomHalfWrapper, 47 | digitWrapper, 48 | ripple, 49 | interval; 50 | 51 | function createDigit(no) { 52 | var topHalf, 53 | bottomHalf; 54 | 55 | topHalf = JS.createElement({ 56 | parent: topHalfWrapper, 57 | type: 'div', 58 | classes: ['no-' + no, 'top-half'], 59 | styles: [ 60 | ['webkitTransition', 'all ' + params.animationDelay + ' linear'], 61 | ['MozTransition', 'all ' + params.animationDelay + ' linear'] 62 | ] 63 | }); 64 | 65 | bottomHalf = JS.createElement({ 66 | parent: bottomHalfWrapper, 67 | type: 'div', 68 | classes: ['no-' + no, 'bottom-half'], 69 | styles: [ 70 | ['zIndex', 10 - no], 71 | ['webkitTransition', 'all ' + params.animationDelay + ' linear'], 72 | ['webkitTransitionDelay', params.animationDelay], 73 | ['MozTransition', 'all ' + params.animationDelay + ' linear'], 74 | ['MozTransitionDelay', params.animationDelay] 75 | ] 76 | }); 77 | 78 | bottomHalf.innerHTML = no; 79 | topHalf.innerHTML = no; 80 | 81 | if (no === params.start) { 82 | topHalf.classList.add('show'); 83 | bottomHalf.classList.add('show'); 84 | } 85 | } 86 | 87 | function getMax() { 88 | var max; 89 | 90 | if (params.group && ripple) { 91 | max = ripple.isMax() ? params.max : 9; 92 | } else { 93 | max = params.max; 94 | } 95 | 96 | return max; 97 | } 98 | 99 | function cleanFrame(f, type) { 100 | if (f > getMax()) { 101 | f = 0; 102 | } 103 | 104 | JS.class({ parent: digitWrapper, selector: type + '.no-' + f, className: 'no-animation', mode: 'add' }); 105 | JS.class({ parent: digitWrapper, selector: type + '.no-' + f, className: 'show', mode: 'remove' }); 106 | JS.class({ parent: digitWrapper, selector: type + '.no-' + f, className: 'roll-over', mode: 'remove' }); 107 | } 108 | 109 | function bottomHalfZeroZindex(i) { 110 | if (i === 0) { 111 | digitWrapper.querySelector('.bottom-half.no-' + getMax()).style.zIndex = 999; 112 | } 113 | } 114 | 115 | function tickTock() { 116 | if (current === 0) { 117 | digitWrapper.querySelector('.bottom-half.no-' + getMax()).style.zIndex = 1; 118 | } 119 | 120 | // Reset Top Half 121 | setTimeout(function () { 122 | cleanFrame(current + 1, '.top-half'); 123 | }, parseFloat(params.animationDelay) * 1000); 124 | 125 | // Reset bottom half 126 | setTimeout(function () { 127 | cleanFrame(current + 1, '.bottom-half'); 128 | bottomHalfZeroZindex(current); 129 | }, parseFloat(params.animationDelay) * 1000 * 2); 130 | 131 | // Prepare for next animation 132 | setTimeout(function () { 133 | JS.class({ parent: digitWrapper, selector: '.top-half', className: 'no-animation', mode: 'remove' }); 134 | JS.class({ parent: digitWrapper, selector:'.bottom-half', className: 'no-animation', mode: 'remove' }); 135 | }, parseFloat(params.animationDelay) * 1000 * 3); 136 | 137 | // Animate top half 138 | JS.class({ parent: digitWrapper, selector: '.top-half.no-' + current, className: 'roll-over', mode: 'add' }); 139 | 140 | // Affect nearby digits 141 | if (ripple && current === 0) { 142 | ripple.flip(); 143 | } 144 | 145 | // Next cycle 146 | current = current === 0 ? getMax() : --current; 147 | 148 | // Finish animation 149 | JS.class({ parent: digitWrapper, selector: '.no-' + current, className: 'show', mode: 'add' }); 150 | 151 | // Stop Timer 152 | if (params.play && ripple && current === 0) { 153 | 154 | if (ripple.isReady()) { 155 | params.ready(); 156 | clearInterval(interval); 157 | } 158 | } 159 | } 160 | 161 | function build() { 162 | var d; 163 | 164 | if (params) { 165 | params.max = params.max || 9; 166 | params.delay = params.delay || 1000; 167 | params.first = params.first || false; 168 | params.start = params.start >= 0 ? params.start : params.max; 169 | params.group = params.group || false; 170 | params.play = params.play || false; 171 | params.animationDelay = params.animationDelay || '.20s'; 172 | params.name = params.name || []; 173 | params.offset = params.offset || 0; 174 | 175 | params.name.push('digit'); 176 | current = params.start; 177 | 178 | } else { 179 | throw new Error('Params not Defined'); 180 | } 181 | 182 | digitWrapper = JS.createElement({ type: 'div', classes: params.name }); 183 | topHalfWrapper = JS.createElement({ parent: digitWrapper, type: 'div', classes: ['top-half-wrapper'] }); 184 | bottomHalfWrapper = JS.createElement({ parent: digitWrapper, type: 'div', classes: ['bottom-half-wrapper'] }); 185 | 186 | for (d = 0; d <= 9; d += 1) { 187 | createDigit(d); 188 | } 189 | 190 | if (params.first && params.wrapper.childNodes[0]) { 191 | params.wrapper.insertBefore(digitWrapper, params.wrapper.childNodes[0]); 192 | } else { 193 | params.wrapper.appendChild(digitWrapper); 194 | } 195 | 196 | bottomHalfZeroZindex(params.start); 197 | } 198 | 199 | this.isMax = function () { 200 | return current === params.max; 201 | } 202 | 203 | this.isZero = function () { 204 | return current === 0 ? true : false; 205 | } 206 | 207 | this.isReady = function () { 208 | return ripple ? this.isZero() && ripple.isReady() : this.isZero(); 209 | } 210 | 211 | this.affect = function (digit) { 212 | ripple = digit; 213 | } 214 | 215 | this.flip = function (forceValue) { 216 | if (forceValue || forceValue === 0) { 217 | 218 | interval = setInterval(function() { 219 | if (current != forceValue) { 220 | tickTock(); 221 | } else { 222 | clearInterval(interval); 223 | } 224 | }, params.delay); 225 | 226 | } else { 227 | tickTock(); 228 | } 229 | } 230 | 231 | function animate() { 232 | if (params.play) { 233 | setTimeout(function () { 234 | interval = setInterval(function () { 235 | tickTock(); 236 | }, params.delay); 237 | }, params.offset); 238 | } 239 | } 240 | 241 | build(); 242 | animate(); 243 | } 244 | 245 | 246 | // Main Function 247 | var Digits = function (params) { 248 | var digits = [], 249 | classes = { 250 | main: 'digits', 251 | countdown: 'countdown', 252 | statistics: 'statistics' 253 | }; 254 | 255 | this.changeValue = function(newValue) { 256 | var p = digits.length - 1; 257 | newValue = newValue && newValue.toString() || '0'; 258 | 259 | // Set new value 260 | for (var d = newValue.length - 1; d >= 0; d--) { 261 | if (digits[p]) { 262 | digits[p].flip(newValue[d]); 263 | } else { 264 | digits = digits.reverse(); 265 | digits.push(new Digit({ name: ['statistic', 'statistic-' + (d + 1)], first: true, wrapper: params.wrapper, start: parseInt(newValue[d]), delay: 250, animationDelay: '.08s' })); 266 | digits = digits.reverse(); 267 | } 268 | 269 | p--; 270 | } 271 | 272 | // Clear unwanted digits 273 | for (var d = 0; d <= digits.length - newValue.length - 1; d++) { 274 | digits[d].flip(0); 275 | } 276 | } 277 | 278 | function statistics() { 279 | if (!params.value) { 280 | throw Error('Missng value parameter'); 281 | } 282 | 283 | params.value = params.value.toString(); 284 | 285 | for (var d = 0; d< params.value.length; d++) { 286 | digits.push(new Digit({ name: ['statistic', 'statistic-' + (d + 1)], wrapper: params.wrapper, start: parseInt(params.value[d]), delay: 250, animationDelay: '.08s' })); 287 | } 288 | } 289 | 290 | function countdown() { 291 | var days, 292 | hours, 293 | minutes, 294 | seconds, 295 | labels, 296 | offset, 297 | diff; 298 | 299 | if (!params.to) { 300 | throw Error('Missing to parameter'); 301 | } 302 | 303 | // Time difference 304 | diff = (new Date(params.to) - new Date()); 305 | 306 | if (diff > 0) { 307 | 308 | // TODO: IMPROVE 309 | days = Math.floor(diff / 1000 / 60 / 60 / 24).toString(); 310 | hours = Math.floor((diff / 1000 / 60 / 60) - (days * 24)).toString(); 311 | minutes = Math.floor(((diff / 1000 / 60 / 60) - (days * 24) - hours) * 60).toString(); 312 | seconds = Math.floor(((((diff / 1000 / 60 / 60) - (days * 24) - hours) * 60) - minutes) * 60).toString(); 313 | 314 | // Fix initial out-of-sync delay 315 | offset = diff % 1000; 316 | 317 | // Add Leading zeros 318 | hours = hours.length === 1 ? '0' + hours : hours; 319 | minutes = minutes.length === 1 ? '0' + minutes : minutes; 320 | seconds = seconds.length === 1 ? '0' + seconds : seconds; 321 | 322 | } else { 323 | days = hours = minutes = seconds = '00'; 324 | } 325 | 326 | // Days 327 | for (var d = 0; d < days.length; d++) { 328 | digits.push(new Digit({ name: ['day', 'day-' + (d + 1)], ready: params.ready, wrapper: params.wrapper, start: parseInt(days[d]) })); 329 | } 330 | 331 | // Hours 332 | digits.push(new Digit({ name: ['hour', 'hour-1'], ready: params.ready, wrapper: params.wrapper, start: parseInt(hours[0]), max: 2 })); 333 | digits.push(new Digit({ name: ['hour', 'hour-2'], ready: params.ready, wrapper: params.wrapper, start: parseInt(hours[1]), max: 3, group: true })); 334 | 335 | // Minutes 336 | digits.push(new Digit({ name: ['minute', 'minute-1'], ready: params.ready, wrapper: params.wrapper, start: parseInt(minutes[0]), max: 5 })); 337 | digits.push(new Digit({ name: ['minute', 'minute-2'], ready: params.ready, wrapper: params.wrapper, start: parseInt(minutes[1]) })); 338 | 339 | // Seconds 340 | digits.push(new Digit({ name: ['second', 'second-1'], ready: params.ready, wrapper: params.wrapper, start: parseInt(seconds[0]), max: 5 })); 341 | digits.push(new Digit({ name: ['second', 'second-2'], ready: params.ready, wrapper: params.wrapper, start: parseInt(seconds[1]), play: diff > 0 ? true : false, offset: offset })); 342 | 343 | // Fire ready event when countdown is not required 344 | if (diff <= 0) { 345 | params.ready(); 346 | } 347 | 348 | // Add labels 349 | if (params.labels) { 350 | labels = JS.createElement({ type: 'div', parent: params.wrapper, classes: ['labels'] }); 351 | 352 | JS.createElement({ parent: labels, classes: ['label', 'days'], type: 'span' }) 353 | JS.createElement({ parent: labels, classes: ['label', 'hours'], type: 'span' }) 354 | JS.createElement({ parent: labels, classes: ['label', 'minutes'], type: 'span' }) 355 | JS.createElement({ parent: labels, classes: ['label', 'seconds'], type: 'span' }) 356 | } 357 | 358 | // Link digits 359 | for (var d = digits.length - 1; d > 0; d--) { 360 | digits[d].affect(digits[d-1]); 361 | } 362 | } 363 | 364 | function init() { 365 | // Check params 366 | if (params) { 367 | params.wrapper = params.wrapper && document.querySelector(params.wrapper); 368 | params.mode = params.mode || 'countdown'; 369 | params.labels = params.labels || false; 370 | 371 | if (!params.wrapper) { 372 | throw Error('Missing parameters'); 373 | } else { 374 | params.wrapper.innerHTML = ''; 375 | params.wrapper = JS.createElement({ parent: params.wrapper, type: 'div', classes: [classes.main] }); 376 | } 377 | } else { 378 | throw Error('Params not defined'); 379 | } 380 | 381 | // Main Class 382 | params.wrapper.classList.add(classes.main); 383 | 384 | // Plugin mode 385 | switch (params.mode) { 386 | case 'countdown': 387 | params.wrapper.classList.add(classes.countdown); 388 | countdown(); 389 | break; 390 | 391 | case 'statistics': 392 | params.wrapper.classList.add(classes.statistics); 393 | statistics(); 394 | break; 395 | 396 | default: 397 | break; 398 | } 399 | } 400 | 401 | init(); 402 | } 403 | --------------------------------------------------------------------------------