2

Currently I am working on an animation for a website which involves two elements having their position changed over a period of time and usually reset to their initial position. Only one element will be visible at a time and everything ought to run as smoothly as possible.

Before you ask, a CSS-only solution is not possible as it is dynamically generated and must be synchronised. For the sake of this question, I will be using a very simplified version which simply consists of a box moving to the right. I shall be referring only to this latter example unless explicitly stated for the remainder of this question to keep things simple.

Anyway, the movement is handled by the CSS transition property being set so that the browser can do the heavy lifting for that. This transition must then be done away with in order to reset the element's position in an instant. The obvious way of doing so would be to do just that then reapply transition when it needs to get moving again, which is also right away. However, this isn't working. Not quite. I'll explain.

Take a look at the JavaScript at the end of this question or in the linked JSFiddle and you can see that is what I'm doing, but setTimeout is adding a delay of 25ms in between. The reason for this is (and it's probably best you try this yourself) if there is either no delay (which is what I want) or a very short delay, the element will either intermittently or continually stay in place, which isn't the desired effect. The higher the delay, the more likely it is to work, although in my actual animation this causes a minor jitter because the animation works in two parts and is not designed to have a delay.

This does seem like the sort of thing that could be a browser bug but I've tested this on Chrome, Firefox 52 and the current version of Firefox, all with similar results. I'm not sure where to go from here as I have been unable to find this issue reported anywhere or any solutions/workarounds. It would be much appreciated if someone could find a way to get this reliably working as intended. :)


Here is the JSFiddle page with an example of what I mean.

The markup and code is also pasted here:

var box = document.getElementById("box");
//Reduce this value or set it to 0 (I
//want rid of the timeout altogether)
//and it will only function correctly
//intermittently.
var delay = 25;

setInterval(function() {
  box.style.transition = "none";
  box.style.left = "1em";

  setTimeout(function() {
    box.style.transition = "1s linear";
    box.style.left = "11em";
  }, delay);
}, 1000);
#box {
  width: 5em;
  height: 5em;
  background-color: cyan;
  position: absolute;
  top: 1em;
  left: 1em;
}
<div id="box"></div>
Obsidian Age
  • 41,205
  • 10
  • 48
  • 71
spacer GIF
  • 626
  • 5
  • 19

2 Answers2

3

Force the DOM to recalculate itself before setting a new transition after reset. This can be achieved for example by reading the offset of the box, something like this:

var box = document.getElementById("box");

setInterval(function(){
      box.style.transition = "none";
      box.style.left = "1em";
      let x = box.offsetLeft; // Reading a positioning value forces DOM to recalculate all the positions after changes
      box.style.transition = "1s linear";
      box.style.left = "11em";  
    }, 1000);
body {
  background-color: rgba(0,0,0,0);
}

#box {
  width: 5em;
  height: 5em;
  background-color: cyan;
  position: absolute;
  top: 1em;
  left: 1em;
}
<div id="box"></div>

See also a working demo at jsFiddle.

Normally the DOM is not updated when you set its properties until the script will be finished. Then the DOM is recalculated and rendered. However, if you read a DOM property after changing it, it forces a recalculation immediately.

What happens without the timeout (and property reading) is, that the style.left value is first changed to 1em, and then immediately to 11em. Transition takes place after the script will be fihished, and sees the last set value (11em). But if you read a position value between the changes, transition has a fresh value to go with.

Teemu
  • 22,918
  • 7
  • 53
  • 106
  • That simple solution seems to work really well. Thanks for editing your answer to add an explanation as to why it works! – spacer GIF Mar 14 '18 at 20:50
2

Instead of making the transition behave as an animation, use animation, it will do a much better job, most importantly performance-wise and one don't need a timer to watch it.

With the animation events one can synchronize the animation any way suited, including fire of a timer to restart or alter it.

Either with some parts being setup with CSS

var box = document.getElementById("box");
box.style.left = "11em";    // start

box.addEventListener("animationend", animation_ended, false);

function animation_ended (e) {
  if (e.type == 'animationend') {
    this.style.left = "1em";
  }
}
#box {
  width: 5em;
  height: 5em;
  background-color: cyan;
  position: absolute;
  top: 1em;
  left: 1em;
  animation: move_me 1s linear 4;
}
@keyframes move_me {
  0% { left: 1em; }
}
<div id="box"></div>

Or completely script based

var prop = 'left', value1 = '1em', value2 = '11em'; 

var s = document.createElement('style');
s.type = 'text/css';
s.innerHTML = '@keyframes move_me {0% { ' + prop + ':' + value1 +' }}';
document.getElementsByTagName('head')[0].appendChild(s);

var box = document.getElementById("box");
box.style.animation = 'move_me 1s linear 4';
box.style.left = value2;      // start

box.addEventListener("animationend", animation_ended, false);

function animation_ended (e) {
  if (e.type == 'animationend') {
    this.style.left = value1;
  }
}
#box {
  width: 5em;
  height: 5em;
  background-color: cyan;
  position: absolute;
  top: 1em;
  left: 1em;
}
<div id="box"></div>
Asons
  • 84,923
  • 12
  • 110
  • 165
  • It definitely looks better. – Teemu Mar 14 '18 at 20:22
  • @Teemu How is this a CSS only solution? ... The style is set with script – Asons Mar 14 '18 at 20:23
  • @Teemu I updated my answer to be more clear how-to. – Asons Mar 14 '18 at 20:33
  • I don't think I'll be switching to using `animation` for this, although it certainly is very powerful and worth using for some things. – spacer GIF Mar 14 '18 at 20:53
  • @spacerGIF To force a DOM redraw as needed for `transition` alone is worth it, performance-wise, since that will affect the overall user experience. Furthermore, this is what `animation` is for, `transition` is not. – Asons Mar 14 '18 at 20:56
  • @spacerGIF And you can set everything up and allow for the `animation` to do what it does best, instead of filling up the environment with timers. – Asons Mar 14 '18 at 21:01
  • @LGSon How would I keep two animations synchronised using your method? I'm using `setInterval` to randomly generate the content in the animation, which must also be synchronised. In this case, moving things using `transition` and JavaScript seems to be the way to ago about things, although obviously it is not the only solution. – spacer GIF Mar 14 '18 at 21:06
  • @spacerGIF You create and start up two animations, give the 2nd a delay matching the 1st duration, and they will synchronize themselves a lot better than you can with `setInterval`. – Asons Mar 14 '18 at 21:13
  • @spacerGIF Bottom line, if you have the experience which is better, make your own pick, though there is a reason why you need to do tricks like DOM redraw to make `transition` work. `transition` is best for interactive effects, like hover etc. and `animation` for time based effects. – Asons Mar 14 '18 at 21:17
  • @LGson Two animations would likely drift out of sync over time. `setInterval`, even though it won't work as an exact measurement of time or anything, won't have any drifting between the two. – spacer GIF Mar 14 '18 at 21:23
  • @spacerGIF I've been using several more animations for a digital signage solution and never had drifting issues, but we can drop this discussion now, I was simply trying to share my experience. – Asons Mar 14 '18 at 21:39
  • @LGSon Well maybe I should look into using `animation` instead of `transition` for this. Between your solution and Teemu's I'm getting no difference in performance so to the end user it will make no difference as to which method is used and for myself, adding one line (two in my actual animation) is the simpler solution. Don't be offended by my choice of solution - both work equally well. But yes, I don't really want to argue over this. – spacer GIF Mar 14 '18 at 21:42
  • @spacerGIF I'm not offended :) ... but when I see someone cut corners, I can be persistent in making them consider a second option, at least when I've been there myself. – Asons Mar 14 '18 at 21:47
  • 1
    My snippet doesn't redraw, it only calculates. Also, transitioned elements are out of the textflow, recalculating the position is a light-weight task to browsers, as well as rendering the page after the script execution. – Teemu Mar 14 '18 at 21:49
  • @Teemu and spacer GIF, what occurs is a style change, which you can read about here: https://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes ... and here: https://gist.github.com/paulirish/5d52fb081b3570c81e3a – Asons Mar 15 '18 at 05:38
  • Yes, what I've tried to explain in my answer is pretty much the same as what Paul Irish writes. Recalculation being a bottleneck in OP's particular case is arguable, since all the elements gone through a style change are out of the textflow. The important part is, that the recalc happens only when reading values after change, _not when setting_ them. The recalc will be done anyway when the script is finished (providing there are changes to the DOM), and this process is optimized for the elements set out of the textflow so that the entire layout doesn't need to be calculated or even rerendered. – Teemu Mar 15 '18 at 06:19
  • @spacerGIF I updated both samples with an iteration count and an event listener, which will fire when the animation is done. This will make the whole setup event driven, which is the preferred way in opposite of a polling one, and show how e.g. one animation can sync with another. Hopefully my answer is good and useful enough to get an upvote. – Asons Mar 15 '18 at 09:37
  • @LGSon Okay, okay, you've marked the question as duplicate. Take your upvote and stop bickering about why this is the best way to do it. – spacer GIF Mar 15 '18 at 10:58
  • @spacerGIF I'm spending my spare time to help you (and all the other users who find this post) by adding more code to show how to take advantage of the `animation` feature, and you call that _"bickering"_?. And the dupe is not a bad thing, it is a good thing, where users get more posts linked, which is one of the great things with this site. – Asons Mar 15 '18 at 11:10
  • @spacerGIF And if the pure reason for your upvote was my _"bickering"_, please remove it. – Asons Mar 15 '18 at 11:11
  • @LGSon Okay. About the bickering, I was referring to yourself in relation to Teemu's answer. I do appreciate the help, as will people who stumble upon this question, but in this case I have chosen not to use `animation`, although it is definitely a useful option, especially for more complex things. And yes, the answers linked in the duplicate thing are actually quite helpful - sorry, I was a bit quick on that. – spacer GIF Mar 15 '18 at 14:07
  • 1
    @spacerGIF All that is perfectly fine :) – Asons Mar 15 '18 at 14:31