Read more in the blog post: http://viget.com/inspire/make-a-flippin-3d-countdown-with-css-and-javascript
A Pen by Doug Avery on CodePen.
Read more in the blog post: http://viget.com/inspire/make-a-flippin-3d-countdown-with-css-and-javascript
A Pen by Doug Avery on CodePen.
| <div class="count"></div> | |
| <script id="count-template" type="text/template"> | |
| <span class="current top <%= currentSize %>"><%= time %></span> | |
| <span class="next top <%= nextSize %>"><%= nextTime %></span> | |
| <span class="current bottom <%= currentSize %>"><%= time %></span> | |
| <span class="next bottom <%= nextSize %>"><%= nextTime %></span> | |
| </script> |
| // underscore loaded | |
| Countdown = function() { | |
| _(this).bindAll('update', 'executeAnimation', 'finishAnimation'); | |
| this.setVars.apply(this, arguments); | |
| this.update(); | |
| }; | |
| Countdown.prototype = { | |
| duration: 1000, | |
| setVars: function(time, el, template) { | |
| this.max = time; | |
| this.time = time; | |
| this.el = el; | |
| this.template = _(template.innerHTML).template(); | |
| this.delta = -1; | |
| }, | |
| update: function() { | |
| this.checkTime(); | |
| this.setSizes(); | |
| this.setupAnimation(); | |
| _(this.executeAnimation).delay(20); | |
| _(this.finishAnimation).delay(this.duration * 0.9); | |
| _(this.update).delay(this.duration); | |
| }, | |
| checkTime: function() { | |
| this.time += this.delta; | |
| if (this.time === 0) this.delta = 1; | |
| if (this.time === this.max) this.delta = -1; | |
| this.delta === 1 ? this.toggleDirection('up', 'down') : this.toggleDirection('down', 'up'); | |
| this.nextTime = this.time + this.delta; | |
| }, | |
| toggleDirection: function(add, remove) { | |
| this.el.classList.add(add); | |
| this.el.classList.remove(remove); | |
| }, | |
| setSizes: function() { | |
| this.currentSize = this.getSize(this.time); | |
| this.nextSize = this.getSize(this.nextTime); | |
| }, | |
| getSize: function(time) { | |
| return time > 9 ? 'small' : ''; | |
| }, | |
| setupAnimation: function() { | |
| this.el.innerHTML = this.template(this); | |
| this.el.classList.remove('changed'); | |
| }, | |
| executeAnimation: function() { | |
| this.el.classList.add('changing'); | |
| }, | |
| finishAnimation: function() { | |
| this.el.classList.add('changed'); | |
| this.el.classList.remove('changing'); | |
| } | |
| }; | |
| new Countdown( | |
| 12, | |
| document.querySelector('.count'), | |
| document.querySelector('#count-template') | |
| ); |
| @import "compass/css3" | |
| // animation vars | |
| $duration: 0.35s | |
| $bounce: cubic-bezier(0.375, 1.495, 0.610, 0.780) | |
| // dimensions | |
| $height: 300px | |
| $width: 200px | |
| .count | |
| box-shadow: 0 10px 5px -5px rgba(#000, 0.2) | |
| height: $height | |
| left: 50% | |
| line-height: $height | |
| margin: -($height / 2) 0 0 -($width / 2) | |
| +perspective(500px) | |
| position: absolute | |
| text-align: center | |
| top: 50% | |
| +translateZ(0) | |
| width: $width | |
| // the basic "card" | |
| // there are four of these: top current, top next, bottom current, and bottom next | |
| span | |
| background: #202020 | |
| color: #f8f8f8 | |
| display: block | |
| font-size: 250px | |
| left: 0 | |
| position: absolute | |
| top: 0 | |
| text-shadow: 0 1px 0 (#000 + 40), 0 2px 0 (#000 + 30), 0 3px 0 (#000 + 20), 0 4px 0 (#000 + 10), 0 5px 0 #000, 0 0 10px rgba(#000, 0.8) | |
| +transform-origin(0, 150px, 0) | |
| width: 100% | |
| // the dividing line in the center | |
| &:before | |
| border-bottom: 2px solid #000 | |
| content: '' | |
| left: 0 | |
| position: absolute | |
| width: 100% | |
| // a shadow fill that adds some convexity on the card surfaces | |
| &:after | |
| box-shadow: inset 0 0 60px rgba(#000, 0.35) | |
| content: '' | |
| height: 100% | |
| left: 0 | |
| position: absolute | |
| top: 0 | |
| width: 100% | |
| // two-digit numbers get the 'small' class | |
| .small | |
| font-size: 175px | |
| .top | |
| // top card sit above the bottom ones, so if we give them the same | |
| // border radius they'll create some crunchiness. | |
| // instead, go one pixel smaller | |
| border-top-left-radius: 11px | |
| border-top-right-radius: 11px | |
| // creating a light shine on the top of the card | |
| box-shadow: inset 0 2px rgba(#000, 0.9), inset 0 3px 0 rgba(#fff, 0.4) | |
| // top cards are only 50% height, and overflow-hidden | |
| // so they only show the top of their number | |
| height: 50% | |
| overflow: hidden | |
| &:before | |
| bottom: 0 | |
| &:after | |
| // top card needs to get darker as it curves downward | |
| +background(linear-gradient(rgba(#000, 0), rgba(#000, 0.15))) | |
| border-top-left-radius: 11px | |
| border-top-right-radius: 11px | |
| .bottom | |
| // bottom cards are 100% height, but their top half is hidden by "top" cards | |
| // this was the best way I could think of to show the bottom cards in half, but | |
| // there's probably another way using display: table-cell and vertical-align. | |
| // ew. | |
| border-radius: 10px | |
| height: 100% | |
| &:before | |
| top: 50% | |
| &:after | |
| border-radius: 10px | |
| +background(linear-gradient(rgba(#fff, 0.1), rgba(#fff, 0.1) 50%, rgba(#fff, 0))) | |
| // styles that only apply when counting "down" | |
| &.down | |
| .top | |
| // use a higher number than the bottoms to prevent crunchy border radiuses | |
| border-top-left-radius: 11px | |
| border-top-right-radius: 11px | |
| height: 50% | |
| &.current | |
| // required to prevent safari bug: https://bugs.webkit.org/show_bug.cgi?id=61824 | |
| +transform-style(flat) | |
| z-index: 3 | |
| &.next | |
| // when counting down, the next top card is rotated towards the user (and invisible) | |
| +transform(rotate3d(1, 0, 0, -90deg)) | |
| z-index: 4 | |
| .bottom | |
| border-radius: 10px | |
| &.current | |
| z-index: 2 | |
| &.next | |
| z-index: 1 | |
| &.changing | |
| .bottom.current | |
| box-shadow: 0 75px 5px -20px rgba(#000, 0.3) | |
| +transform(rotate3d(1, 0, 0, 90deg)) | |
| // the current bottom card rotates up to hide itself, and reveal the next one | |
| +transition(transform $duration ease-in, box-shadow $duration ease-in) | |
| &.changing, | |
| &.changed | |
| .top.next | |
| // and the next top card rotates into view (after $duration) | |
| +transition(transform $duration ease-out $duration) | |
| +transform(none) | |
| &.up | |
| .top | |
| height: 50% | |
| &.current | |
| z-index: 4 | |
| &.next | |
| z-index: 3 | |
| .bottom | |
| &.current | |
| z-index: 1 | |
| &.next | |
| box-shadow: 0 75px 5px -20px rgba(#000, 0.3) | |
| // when counting "up", the next bottom card begins pointed at the user... | |
| +transform(rotate3d(1, 0, 0, 90deg)) | |
| z-index: 2 | |
| &.changing | |
| .top.current | |
| // and the current top card does the rotating | |
| +transform(rotate3d(1, 0, 0, -90deg)) | |
| // when the card is "dropping" it should be faster | |
| +transition(transform $duration * 0.75 ease-in, box-shadow $duration * 0.75 ease-in) | |
| &.changing, | |
| &.changed | |
| .bottom.next | |
| box-shadow: 0 0 0 0 rgba(#000, 0) | |
| // add a little bounce at the moment the card finishes falling | |
| +transition(box-shadow $duration / 2 $bounce $duration, transform $duration $bounce $duration) | |
| +transform(rotate3d(1, 0, 0, 0)) | |
| &.changed | |
| .top.current, | |
| .bottom.current | |
| display: none | |
| // presentation styles | |
| @import url(http://fonts.googleapis.com/css?family=Oswald) | |
| html, | |
| body | |
| height: 100% | |
| width: 100% | |
| body | |
| background: #202020 url(http://cl.ly/image/040I101f1i0I/planes.jpg) 50% 50% | |
| background-origin: 50% 50% | |
| +background-size(cover) | |
| font-family: 'Oswald' |