Retro Mechanical Counter In JavaScript
| AUTHOR: | Andrew Rubin |
|---|---|
| VIEWS TOTAL: | 527 views |
| OFFICIAL PAGE: | Go to website |
| LAST UPDATE: | August 31, 2020 |
| LICENSE: | MIT |
Preview:

Description:
A retro number counter that counts up to a specified number with a digit flipping effect just like a mechanical counter.
Built with vanilla JavaScript and CSS3 animations/transforms.
How to use it:
1. Create an empty container for the counter.
<div class="cool-element"></div>
2. Create an input field to accept the number you’d like to count up to.
<input class="number-input" type="number" value="123" /> <button class="number-button">Start The Counter</button>
3. The main JavaScript to enable the counter.
const element = document.querySelector(".cool-element"),
numberInput = document.querySelector(".number-input"),
numberSubmit = document.querySelector(".number-button");
const ROOT_CLASS_NAME = "digit-flipper";
class DigitFlipper {
constructor(element, options = {
number: 9,
iterationCount: 9 })
{
// First, some parameter sanitizing:
if (options.number > 9 || options.number < 0) return;
this.options = Object.assign({}, options);
if (!this.options.number) this.options.number = 9;
if (!this.options.iterationCount) this.options.iterationCount = 9;
// Adjusting the number of iterations,
// in case our numbers end up in the negatives:
if (this.options.number - this.options.iterationCount < 0) {
this.options.iterationCount = this.options.number;
}
this.element = element;
this.digitClassName = `${ROOT_CLASS_NAME}__digit`;
this.topClassName = `${this.digitClassName}-top`;
this.bottomClassName = `${this.digitClassName}-bottom`;
this.flipTopClass = `${this.digitClassName}--flip-top`;
this.flipBottomClass = `${this.digitClassName}--flip-bottom`;
this.flipDoneClass = `${this.digitClassName}--flip-done`;
this.DOMNodes = [];
this.flipDuration = parseFloat(
(window.getComputedStyle(document.documentElement).
getPropertyValue("--flip-duration") || "1s").
replace("s", ""));
this._init();
return this;
}
_init() {
this._populateDOM();
}
// creates DOM elements for each digit and all of its "iterations"
_populateDOM() {
let i = this.options.number - this.options.iterationCount;
for (i; i <= this.options.number; i++) {
const digit = document.createElement("span"),
digitTop = document.createElement("span"),
digitBottom = document.createElement("span"),
digitText = document.createTextNode(i);
digit.className = this.digitClassName;
digitTop.className = this.topClassName;
digitBottom.className = this.bottomClassName;
digitTop.appendChild(digitText);
digitBottom.appendChild(digitText.cloneNode());
digit.appendChild(digitTop);
digit.appendChild(digitBottom);
this.DOMNodes.push(digit);
this.element.insertAdjacentElement("afterbegin", digit);
}
}
// runs the animtion sequence for the digit
flip() {
this.DOMNodes.forEach((node, index) => {
const nextNode = this.DOMNodes[index + 1];
let delay = this.flipDuration * index * 1000;
// The flipBottomClass turns the bottom half
// down from it's inital state of 90deg
// The flipTopClass turns the top half
// down from it's inital state of 0deg
const t1 = setTimeout(() => {
node.classList.add(this.flipBottomClass);
clearTimeout(t1);
const t2 = setTimeout(() => {
if (nextNode) node.classList.add(this.flipTopClass);
clearTimeout(t2);
const t3 = setTimeout(() => {
node.style.zIndex = index + 1;
clearTimeout(t3);
}, this.flipDuration);
}, this.flipDuration);
}, delay);
});
}}
class FlipCounter {
constructor(element, value) {
if (typeof value !== "number") return;
this.element = element;
this.targetNumber = value;
this.targetDigits = [];
this.numDigits = this.targetNumber.toString().length;
this.DOMNodes = [];
this.flipperInstances = [];
// separate the digits of the value arg
for (let i = 0; i < this.numDigits; i++) {
this.targetDigits.push(this.targetNumber.toString()[i]);
}
this.populateDOM();
this.populateInstanceArray();
}
// creates wrapper elements for each digit
populateDOM() {
this.element.innerHTML = "";
let i = 0;
for (i; i < this.numDigits; i++) {
const container = document.createElement("span");
container.className = ROOT_CLASS_NAME;
this.element.appendChild(container);
this.DOMNodes.push(container);
}
}
// instantiate a DigitFlipper object for each digit
populateInstanceArray() {
this.DOMNodes.forEach((digit, index) => {
this.flipperInstances.push(
new DigitFlipper(digit, {
number: this.targetDigits[index],
iterationCount: 4 }));
});
}
// runs the animation, with a 200ms stagger
play() {
this.flipperInstances.forEach((instance, index) => {
let delay = index * 200;
setTimeout(() => instance.flip(), delay);
});
}}
// Handles the input field, for the demo
const onClick = () => {
let num = Number(numberInput.value);
if (num >= 0) {
let counter = new FlipCounter(element, num);
counter.play();
}
};
numberSubmit.addEventListener("click", onClick);
// kick off the initial one
const counter = new FlipCounter(element, Number(numberInput.value));
counter.play();4. The necessary CSS/CSS3 rules for the digit flipping effect.
.digit-flipper {
display: inline-block;
height: 0.98em;
font-size: 20vmin;
line-height: 1;
margin: 0 0.02em;
-webkit-perspective: 300px;
perspective: 300px;
position: relative;
width: 0.65em;
}
.digit-flipper__digit {
display: block;
height: 100%;
position: absolute;
text-align: center;
width: 100%;
}
.digit-flipper__digit-top,
.digit-flipper__digit-bottom {
color: black;
display: block;
height: 100%;
position: absolute;
-webkit-transform-origin: 50% 50%;
transform-origin: 50% 50%;
width: 100%;
}
.digit-flipper__digit-top {
background-color: #ffffff;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
-webkit-clip-path: inset(0 0 51% 0);
clip-path: inset(0 0 51% 0);
overflow: hidden;
-webkit-transform: rotateX(0deg);
transform: rotateX(0deg);
}
.digit-flipper__digit-bottom {
background-color: #d9d9d9;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
-webkit-clip-path: inset(51% 0 0 0);
clip-path: inset(51% 0 0 0);
-webkit-transform: rotateX(90deg);
transform: rotateX(90deg);
}
.digit-flipper__digit--flip-bottom .digit-flipper__digit-bottom {
-webkit-animation: flip-bottom .3s ease-in 0s 1 forwards;
animation: flip-bottom .3s ease-in 0s 1 forwards;
}
.digit-flipper__digit--flip-top .digit-flipper__digit-top {
-webkit-animation: flip-top .3s ease-in 0s 1 forwards;
animation: flip-top .3s ease-in 0s 1 forwards;
}
.digit-flipper__digit--flip-done .digit-flipper__digit-bottom {
-webkit-transform: rotateX(0deg);
transform: rotateX(0deg);
}
@-webkit-keyframes flip-top {
from {
-webkit-transform: rotateX(0deg);
transform: rotateX(0deg);
}
to {
-webkit-transform: rotateX(-90deg);
transform: rotateX(-90deg);
}
}
@keyframes flip-top {
from {
-webkit-transform: rotateX(0deg);
transform: rotateX(0deg);
}
to {
-webkit-transform: rotateX(-90deg);
transform: rotateX(-90deg);
}
}
@-webkit-keyframes flip-bottom {
from {
-webkit-transform: rotateX(90deg);
transform: rotateX(90deg);
}
to {
-webkit-transform: rotateX(0deg);
transform: rotateX(0deg);
}
}
@keyframes flip-bottom {
from {
-webkit-transform: rotateX(90deg);
transform: rotateX(90deg);
}
to {
-webkit-transform: rotateX(0deg);
transform: rotateX(0deg);
}
}
.cool-element {
background-color: #404040;
border-radius: 15px;
box-shadow: inset -2px -2px 10px 0px black, inset 4px 4px 10px 0px rgba(255, 255, 255, 0.4), 5px 5px 15px 0px rgba(0, 0, 0, 0.3);
margin: -40px 0 auto;
padding: 20px 18px 16px;
}
