I'm trying to display some sort of timeline and my goal is to get it smoothly refreshed.
I managed to get something better than absolute positionning using css transform property but I'm not very happy because there is some flickering (especially when the background is dark).
var background = document.querySelector('#background');
var position = document.querySelector('#position');
var transform = document.querySelector('#transform');
var backgroundColor, borderColor
background.addEventListener('change', e => {
backgroundColor = e.target.checked ? '#333333' : 'white'
position.style.backgroundColor = backgroundColor
transform.style.backgroundColor = backgroundColor
});
let current = 0
let step = 30
for (let i = 0; i < 300; i++) {
for (let j = 0; j < 2; j++) {
var element = document.createElement('div')
element.style.position = 'absolute'
element.style.height = '50px'
element.style.width = step + 'px'
if(j) {
element.style.left = current + 'px'
} else {
element.style.left = 0 + 'px'
element.style.transform = 'translateX(' + current + 'px)'
}
element.style['border-left'] = '1px gray solid'
if (j)
position.appendChild(element)
else
transform.appendChild(element)
}
current += step
}
setInterval(refresh, 50);
let init = 0
function refresh() {
init -= 0.2
let current = init
for (var i = 0; i < position.children.length; i++) {
var c = position.children[i];
c.style.left = current + 'px'
current += step
}
current = init
for (var i = 0; i < transform.children.length; i++) {
var c = transform.children[i];
c.style.transform = 'translateX(' + current + 'px)'
current += step
}
}
<html>
<body>
<div>
<input type="checkbox" id="background" label="Dark">
<label>Dark</label>
</div>
<span>Using css position<span>
<div id="position" style="width:100%;height:50px;margin-bottom:1em;"></div>
<span>Using css transform<span>
<div id="transform" style="width:100%;height:50px;"></div>
</body>
</html>
Is there a better way to do this?
The reason the animation is flickering is that it's being translated very slowly at 20fps (1000ms/50
). To make it smoother, simply add the refresh rate to 60 times per second and increase the translation value per frame.
You're using a 300 divs to create the timeline animation and animating 300 divs at once. This can be a little more expensive. To streamline your animation, you can simply create enough amount of divs to fit the whole container plus one. Then, you simply need to translate until the leftmost div disappears before replaying the animation. It creates an illusion of a continuous animation when it actually isn't. Another more performant way is to only animate the container (i.e. wrapper) of the divs.
If you can alter either position
or transform
to create an animation, always opt for transform
. Try reading the the link here in CSS Tricks to get a rough idea why.
Furthermore, you can use either requestAnimationFrame
or setTimeout
for this animation. There are, obviously, other methods too. Here's quite a handy description from SO about the difference between the two: requestAnimationFrame and setInterval.
Using JS
Finally, here's a working example using both setInterval
and requestAnimationFrame
:
window.onload = (() => {
var background = document.querySelector('#background')
var interval = document.querySelector('#setInterval')
var intervalFast = document.querySelector('#setIntervalFast')
var raq = document.querySelector('#raq')
var raqFast = document.querySelector('#raqFast')
var backgroundColor, borderColor
background.addEventListener('change', e => {
backgroundColor = e.target.checked ? '#333333' : 'white'
interval.style.backgroundColor = backgroundColor
intervalFast.style.backgroundColor = backgroundColor
raq.style.backgroundColor = backgroundColor
raqFast.style.backgroundColor = backgroundColor
})
let intervalDocFrag = document.createDocumentFragment()
let intervalFastDocFrag = document.createDocumentFragment()
let raqDocFrag = document.createDocumentFragment()
let raqFastDocFrag = document.createDocumentFragment()
let current = 0
let step = 30
let divNeeded = Math.ceil(interval.getBoundingClientRect().width / 30) + 1 // Calculating how many divs are needed to fit one container + 1; 30 is the width of the div (29px + 1px of left border)
for (let i = 0; i < divNeeded; i++) {
for (let j = 0; j < 4; j++) {
var element = document.createElement('div')
element.style.position = 'absolute'
element.style.height = '50px'
element.style.width = step + 'px'
element.style.left = current + 'px'
element.style['border-left'] = '1px gray solid'
if (j === 0)
intervalDocFrag.appendChild(element)
else if (j === 1)
raqDocFrag.appendChild(element)
else if (j === 2)
intervalFastDocFrag.appendChild(element)
else if (j === 3)
raqFastDocFrag.appendChild(element)
}
current += step
}
interval.appendChild(intervalDocFrag)
intervalFast.appendChild(intervalFastDocFrag)
raq.appendChild(raqDocFrag)
raqFast.appendChild(raqFastDocFrag)
let intervalTranslateSlowValue = 0
function intervalSlowAnimation() {
if (Math.floor(intervalTranslateSlowValue) === -30) {
intervalTranslateSlowValue = 0 // Resetting animation to create an endless timeline animating illusion
} else {
intervalTranslateSlowValue -= 0.064 // Gotten from 0.2 * 16 / 50
}
for (let child of interval.children) {
child.style.transform = `translateX(${intervalTranslateSlowValue}px)`
}
}
let intervalTranslateFastValue = 0
function intervalFastAnimation() {
if (Math.floor(intervalTranslateFastValue) === -30) {
intervalTranslateFastValue = 0
} else {
intervalTranslateFastValue -= 0.2
}
for (let child of intervalFast.children) {
child.style.transform = `translateX(${intervalTranslateFastValue}px)`
}
}
function raqSlowAnimate(timeElapsed) {
let translateValue = -1 * ((timeElapsed / (1000/60) * 0.064) % 30)
for (let child of raq.children) {
child.style.transform = `translateX(${translateValue}px)`
}
window.requestAnimationFrame(raqSlowAnimate)
}
function raqFastAnimate(timeElapsed) {
let translateValue = -1 * ((timeElapsed / (1000/60) * 0.2) % 30)
for (let child of raqFast.children) {
child.style.transform = `translateX(${translateValue}px)`
}
window.requestAnimationFrame(raqFastAnimate)
}
window.setInterval(intervalSlowAnimation, 1000/60)
window.setInterval(intervalFastAnimation, 1000/60)
window.requestAnimationFrame(raqSlowAnimate)
window.requestAnimationFrame(raqFastAnimate)
})
* {
box-sizing: border-box;
}
#setInterval,
#setIntervalFast,
#raq,
#raqFast {
position: relative;
width: 100%;
height: 50px;
margin-bottom: 1em;
overflow: hidden;
}
<div>
<input type="checkbox" id="background" label="Dark">
<label>Dark</label>
</div>
<span>Using setInterval at [1000/60]ms; translating .2px per 50ms (.064px per 16ms)</span>
<div id="setInterval"></div>
<span>Using requestAnimationFrame; translating .2px per 50ms (.064px per 16ms)</span>
<div id="raq"></div>
<span>Using setInterval at [1000/60]ms; translating .2px per 16ms</span>
<div id="setIntervalFast"></div>
<span>Using requestAnimationFrame; translating .2px per 16ms</span>
<div id="raqFast"></div>
Using CSS Animation (easier)
You can also use CSS animation to easily create the animation above:
window.onload = (() => {
var background = document.querySelector('#background')
var css = document.querySelector('#cssMethod')
var cssFast = document.querySelector('#cssMethodFast')
var backgroundColor, borderColor
background.addEventListener('change', e => {
backgroundColor = e.target.checked ? '#333333' : 'white'
css.style.backgroundColor = backgroundColor
cssFast.style.backgroundColor = backgroundColor
})
let cssDocFrag = document.createDocumentFragment()
let cssFastDocFrag = document.createDocumentFragment()
let current = 0
let step = 30
let divNeeded = Math.ceil(css.getBoundingClientRect().width / 30) + 1
for (let i = 0; i < divNeeded; i++) {
for (let j = 0; j < 2; j++) {
var element = document.createElement('div')
element.style.position = 'absolute'
element.style.height = '50px'
element.style.width = step + 'px'
element.style.left = current + 'px'
element.style['border-left'] = '1px gray solid'
if (j == 0) css.appendChild(element)
else if (j == 1) cssFast.appendChild(element)
}
current += step
}
css.appendChild(cssDocFrag)
cssFast.appendChild(cssFastDocFrag)
})
* {
box-sizing: border-box;
}
#cssMethod,
#cssMethodFast {
position: relative;
width: 100%;
height: 50px;
margin-bottom: 1em;
overflow: hidden;
}
#cssMethod div {
animation: 6s linear translation 0s infinite;
}
#cssMethodFast div {
animation: 2.4s linear translation 0s infinite;
}
@keyframes translation {
from { transform: translateX(-0px); }
to { transform: translateX(-30px); }
}
<div>
<input type="checkbox" id="background" label="Dark">
<label>Dark</label>
</div>
<span>Using CSS animation; translating .2px per 50ms</span>
<div id="cssMethod"></div>
<span>Using CSS animation; translating .625px per 50ms</span>
<div id="cssMethodFast"></div>