How to translate element to act like a odometer
You can use two sets of numbers and a little bit of extra javascript to achieve this effect.
If the new number is less than the current number, use a second set of numbers (digits 0-9) that are farther down. As the css animation transitions from the first set of numbers to the second, it will appear as if the odometer is "rolling over".
When the animation completes, switch back to the first set of numbers without animating (no transition class).
I've made a working example based off of your original jsfiddle.
NOTE: This makes use of the .classList
property of DOM elements, and the tranistionend
event. You may have to add vendor prefixes (i.e. webkitTransitionEnd
) and implement your own version of .classList
, depending on what browsers you need to support.
document.getElementById("rand").addEventListener("click", randomize);
document.getElementById("cipa").addEventListener("transitionend", transitionEnd);
function randomize() {
setNumber(Math.floor(Math.random() * 9));
}
function setNumber(newNumber) {
let dupa = document.getElementById("cipa");
// assumes dupa.dataset.num always be a valid int
let selected = parseInt(dupa.dataset.num);
if (newNumber === selected) return; // if same as existing, don't do anything
// if the new number is less than the old number
// use the second set of numbers to avoid moving "backwards"
if (newNumber < selected) dupa.classList.add("rolledover");
// animate to the new position
dupa.classList.add("transitioning");
dupa.dataset.num = "" + newNumber;
}
function transitionEnd() {
let dupa = document.getElementById("cipa");
// don't animate
dupa.classList.remove("transitioning");
dupa.classList.remove("rolledover");
}
#rand {
margin-top: 50px;
}
.dupa1 {
height: 30px;
width: 30px;
border: 1px solid #000;
overflow: hidden;
}
.dupa2.transitioning {
transition: all 1s ease;
}
.dupa2 span {
height: 30px;
width: 30px;
display: block;
text-align: center;
line-height: 30px;
}
.dupa2[data-num="0"] {
transform: translate(0, 0);
}
.dupa2[data-num="1"] {
transform: translate(0, -30px);
}
.dupa2[data-num="2"] {
transform: translate(0, -60px);
}
.dupa2[data-num="3"] {
transform: translate(0, -90px);
}
.dupa2[data-num="4"] {
transform: translate(0, -120px);
}
.dupa2[data-num="5"] {
transform: translate(0, -150px);
}
.dupa2[data-num="6"] {
transform: translate(0, -180px);
}
.dupa2[data-num="7"] {
transform: translate(0, -210px);
}
.dupa2[data-num="8"] {
transform: translate(0, -240px);
}
.dupa2[data-num="9"] {
transform: translate(0, -270px);
}
.rolledover.dupa2[data-num="0"] {
transform: translate(0, -300px);
}
.rolledover.dupa2[data-num="1"] {
transform: translate(0, -330px);
}
.rolledover.dupa2[data-num="2"] {
transform: translate(0, -360px);
}
.rolledover.dupa2[data-num="3"] {
transform: translate(0, -390px);
}
.rolledover.dupa2[data-num="4"] {
transform: translate(0, -420px);
}
.rolledover.dupa2[data-num="5"] {
transform: translate(0, -450px);
}
.rolledover.dupa2[data-num="6"] {
transform: translate(0, -480px);
}
.rolledover.dupa2[data-num="7"] {
transform: translate(0, -510px);
}
.rolledover.dupa2[data-num="8"] {
transform: translate(0, -540px);
}
.rolledover.dupa2[data-num="9"] {
transform: translate(0, -570px);
}
<div class="dupa1">
<div class="dupa2" id="cipa" data-num="0">
<span>0</span>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
<span>0</span>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
</div>
</div>
<button id="rand">rand</button>
As @codyThompsonDev said, a rollover area is the best way to implement this. Something I think he missed though, is what happens when you go from a rollover number to a non-rollover number.
For example, let's say that the odometer randomly tries to roll to 4
, then 3
, then 1
. The first time, it can roll to 4 no problem. The second time, it has to roll to "13", in the rollover zone. But then, it tries to roll to "11" which is also in the rollover zone, causing it to roll backwards.
To achieve this effect under those circumstances, you must snap the dial back out of the rollover zone, then roll forward again. I would implement this using window.requestAnimationFrame()
.
I've made a fiddle to demonstrate this: https://jsfiddle.net/tprobinson/8k125fmz/67/
Add the debugBackground
class to dupa2
to see the rollover effect visually.
I would recommend generating the CSS classes with a preprocessor like Sass, as writing them by hand can be error prone as well.
document.getElementById("rand").addEventListener("click", randomize);
const debug = document.getElementById("debug");
const dupa = document.getElementById("cipa");
let animationInProgress = null
function setDebug(num) {
debug.textContent = 'Number is really: ' + num
}
function animateOdometer(newNum) {
// Add the smooth class and set the number to let it roll.
dupa.classList.add('smooth')
setDebug(newNum)
dupa.dataset.num = newNum
// In 1000 ms, remove the smooth class
animationInProgress = window.setTimeout(() => {
dupa.classList.remove('smooth')
animationInProgress = null
}, 1000)
}
function randomize() {
let oldNum = Number.parseInt(dupa.dataset.num)
if (oldNum === undefined || oldNum === null) {
oldNum = 0
}
let newNum = Math.floor(Math.random() * 9) + 0;
// If an animation is already in progress, cancel it
if (animationInProgress) {
window.clearTimeout(animationInProgress)
dupa.classList.remove('smooth')
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
dupa.dataset.num = oldNum - 10
setDebug(oldNum - 10)
dupa.classList.remove('smooth')
window.requestAnimationFrame(() => {
// Wait for one frame to let the snapback happen
window.requestAnimationFrame(() => {
// Then roll forward
animateOdometer(newNum)
})
})
return
}
// Roll the dial
animateOdometer(newNum)
}
#rand,
#debug {
margin-top: 50px;
}
.dupa1 {
height: 30px;
width: 30px;
border: 1px solid #000;
overflow: hidden;
}
.dupa2.smooth {
transition: all 1s ease;
}
.dupa2 span {
height: 30px;
width: 30px;
display: block;
text-align: center;
line-height: 30px;
}
.dupa2.debugBackground {
background: linear-gradient(to bottom, #ffffff 0%, #ffffff 50%, #207cca 51%, #207cca 100%);
}
.dupa2[data-num="0"] {
transform: translate(0, 0);
}
.dupa2[data-num="1"] {
transform: translate(0, -30px);
}
.dupa2[data-num="2"] {
transform: translate(0, -60px);
}
.dupa2[data-num="3"] {
transform: translate(0, -90px);
}
.dupa2[data-num="4"] {
transform: translate(0, -120px);
}
.dupa2[data-num="5"] {
transform: translate(0, -150px);
}
.dupa2[data-num="6"] {
transform: translate(0, -180px);
}
.dupa2[data-num="7"] {
transform: translate(0, -210px);
}
.dupa2[data-num="8"] {
transform: translate(0, -240px);
}
.dupa2[data-num="9"] {
transform: translate(0, -270px);
}
.dupa2[data-num="10"] {
transform: translate(0, -300px);
}
.dupa2[data-num="11"] {
transform: translate(0, -330px);
}
.dupa2[data-num="12"] {
transform: translate(0, -360px);
}
.dupa2[data-num="13"] {
transform: translate(0, -390px);
}
.dupa2[data-num="14"] {
transform: translate(0, -420px);
}
.dupa2[data-num="15"] {
transform: translate(0, -450px);
}
.dupa2[data-num="16"] {
transform: translate(0, -480px);
}
.dupa2[data-num="17"] {
transform: translate(0, -510px);
}
.dupa2[data-num="18"] {
transform: translate(0, -540px);
}
.dupa2[data-num="19"] {
transform: translate(0, -570px);
}
<div class="dupa1">
<div class="dupa2" id="cipa" data-num="0">
<span>0</span>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
<span>0</span>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
</div>
</div>
<div id="debug">
Number is really: 0
</div>
<button id="rand">rand</button>