Multi-field odometer with Rollover

Add the 'debugBackground' class to 'dupa2' to see the rollover zone in blue

by tprobinson

HTML

<div id="root"></div>
<button id="rand">rand</button>

CSS

#rand {
  margin-top: 50px;
}

#root {
  display: flex:
}

.field {
  height: 30px;
  width: 30px;
  border: 1px solid #000;
  overflow: hidden;
}

.field.smooth {
  transition: all 1s ease;
}

.field span {
  height: 30px;
  width: 30px;
  display: block;
  text-align: center;
  line-height: 30px;
}

.field.debugBackground {
  background: linear-gradient(to bottom, #ffffff 0%, #ffffff 50%, #207cca 51%, #207cca 100%);
}

.field[data-num="0"] {
  transform: translate(0, 0);
}

.field[data-num="1"] {
  transform: translate(0, -30px);
}

.field[data-num="2"] {
  transform: translate(0, -60px);
}

.field[data-num="3"] {
  transform: translate(0, -90px);
}

.field[data-num="4"] {
  transform: translate(0, -120px);
}

.field[data-num="5"] {
  transform: translate(0, -150px);
}

.field[data-num="6"] {
  transform: translate(0, -180px);
}

.field[data-num="7"] {
  transform: translate(0, -210px);
}

.field[data-num="8"] {
  transform: translate(0, -240px);
}

.field[data-num="9"] {
  transform: translate(0, -270px);
}

.field[data-num="10"] {
  transform: translate(0, -300px);
}

.field[data-num="11"] {
  transform: translate(0, -330px);
}

.field[data-num="12"] {
  transform: translate(0, -360px);
}

.field[data-num="13"] {
  transform: translate(0, -390px);
}

.field[data-num="14"] {
  transform: translate(0, -420px);
}

.field[data-num="15"] {
  transform: translate(0, -450px);
}

.field[data-num="16"] {
  transform: translate(0, -480px);
}

.field[data-num="17"] {
  transform: translate(0, -510px);
}

.field[data-num="18"] {
  transform: translate(0, -540px);
}

.field[data-num="19"] {
  transform: translate(0, -570px);
}

JavaScript

// Generate this ahead of time
const fieldInnerHtml = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(x => `<span>${x}</span>`)

class RolloverField {
  constructor() {
    this.animationInProgress = null
    this.element = document.createElement('div')
    this.element.addClass('field')
    this.element.dataset.num = 0
    this.element.innerHtml = fieldInnerHtml
  }

  animate(num) {
    // Add the smooth class and set the number to let it roll.
    this.element.classList.add('smooth')
    this.element.dataset.num = num

    // In 1000 ms, remove the smooth class
    animationInProgress = window.setTimeout(() => {
      this.element.classList.remove('smooth')
      this.animationInProgress = null
    }, 1000)
  }

  setNum(num) {
    let oldNum = Number.parseInt(this.element.dataset.num)
    if (oldNum === undefined || oldNum === null) {
      oldNum = 0
    }

    // If an animation is already in progress, cancel it
    if (this.animationInProgress) {
      window.clearTimeout(this.animationInProgress)
      this.element.classList.remove('smooth')
      this.animationInProgress = null
    }

    // If the new number is before our old number
    // we have to force a roll forwards
    if (newNum < oldNum) {
      newNum += 10
    }

    if (oldNum > 9) {
      // The dial was already rolled over. We need to
      // snap the dial back before rolling again.
      // Wait for a frame so we can snap the dial back
      this.element.dataset.num = oldNum - 10
      this.element.classList.remove('smooth')

      window.requestAnimationFrame(() => {
        // Wait for one frame to let the snapback happen
        window.requestAnimationFrame(() => {
          // Then roll forward
          this.animate(newNum)
        })
      })

      return
    }

    // Roll the dial
    this.animate(newNum)
  }
}

// Generate all our HTML
const root = document.getElementById("root");
console.log(root)
const elArray = []
while (elArray.length < 9) {
  const newEl =...