Retro Mechanical Counter In JavaScript

Category: Javascript | August 31, 2020
Author: Andrew Rubin
Views Total: 273 views
Official Page: Go to website
Last Update: August 31, 2020
License: MIT

Preview:

Retro Mechanical Counter In JavaScript

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

You Might Be Interested In:


Leave a Reply