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; }