Timing issues when animating with CSS3 Transitions
I have been playing around a lot lately with motion graphics created using HTML5 and / or CSS3. One of my favorite new features is CSS Transitions, which makes it super simple to animate element properties between two states.
However, I ran into a gotcha the other day, and wanted to make a quick blog post in case anyone else runs into it in the future. Basically, if you change a property that a CSS Transition is monitoring in the same script loop that you add the element to the DOM, the CSS Transition will not take affect. Instead, the element will be drawn with the new properties, and will not animate to those properties.
Here is an example that shows the issue, as well as how to fix it.
First the relevant styles for the CSS Transition:
.box { /*...*/ -webkit-transition: all 0.7s ease; -moz-transition: all 0.7s ease; -o-transition: all 0.7s ease; transition: all 0.7s ease; }
Notice how when you click the example the cirlce just appears on the right, instead of appearing on the left, and then animating to the right as it should.
Here is the relevant code that creates the div and updates it position:
function onMouseClick() { if(box) { //remove existing box from dom document.body.removeChild(box); } //create the div box = document.createElement("div"); box.className = "box"; //add to dom document.body.appendChild(box); //position in middle / left of screen box.style.left = "10px"; box.style.top = (window.innerHeight / 2) + "px"; //set position we want div to animate to (using the CSS Transition) //NOTE : This will not animate, but will instead just be moved / drawn //in the final position //need to delay this call by 20 ms box.style.left = (window.innerWidth - 100) + "px"; }
The solution, is to delay changing the properties that you want to animate by a short time interval. I have found that for most browsers a delay of as little as one second is enough to allow the CSS transition to take effect. However, for Firefox, the first time an element is added, I had to add a delay of 20ms in order to ensure the element animated correctly. However, subsequent animations could use a 1 ms delay. I am not sure if the delay interval is browser specific, or is related to how fast the users CPU is.
Setting the delay can be done using setTimeout.
Here is the update example, and code:
function onMouseClick() { if(box) { //remove existing div from dom document.body.removeChild(box); } //create the div box = document.createElement("div"); box.className = "box"; //add to dom document.body.appendChild(box); //position in middle / left of screen box.style.left = "10px"; box.style.top = (window.innerHeight / 2) + "px"; //delay updating position so it will animate correctly setTimeout(moveBox, 20, box); } function moveBox(box) { box.style.left = (window.innerWidth - 100) + "px"; }
The only change, is that we moved the code to update the divs position to its own function. This allows us to then use setTimeout to delay the code from being called for 1 ms. By doing this, the div will be added to the dom, and positioned in the correct initial position, and then animate to its new position.
Here is another example that uses setTimeout to animate newly created DIVs.
Post in the comments if you have any questions or suggestions.
Any chance you could hook into the DOMNodeInserted event instead of relying on a fixed time delay?
Sho Kuwamoto
20 Jul 11 at 1:10 pm edit_comment_link(__('Edit', 'sandbox'), ' ', ''); ?>
Yeah. You should be able to do that (or listen for the next animation frame event). I did the delay because the code is a bit easier.
Ill do a quick test though with the event and post the results.
mikechambers
20 Jul 11 at 1:29 pm edit_comment_link(__('Edit', 'sandbox'), ' ', ''); ?>
ok. I put together a quick test with the DOMNodeInsertedIntoDocument event. The event is fired. However, the weird thing is that, the div’s position isnt updated at all:
Basically, in this case, the DIV will stay in its original position, and not the position it should have animated to.
I might be doing something wrong here, and am going to keep playing around with it.
mikechambers
20 Jul 11 at 1:45 pm edit_comment_link(__('Edit', 'sandbox'), ' ', ''); ?>
a value of 0 in the setTimeout does not work ?
I guess it’s a mater of reflow / repaint : what happens if instead of :
You do something in the middle, like
jpvincent
21 Jul 11 at 3:11 am edit_comment_link(__('Edit', 'sandbox'), ' ', ''); ?>
Try creating a style declaration in js, then creating the dom node to match your styling’s selector, then adding the animation class style. That should trigger the appropriate rendering steps without the need for setTIMEOUT.
Using a set of three closures would work as well, one for setup, one for initial styling, and one for applying the animation. From your first, call the second with setTimeout (myInitialStyling, 0); And the same for the second to the third. This will set the functions to run the next time the VM is free, which should occur after the reflow triggered by node insertion, then by styling, etc.
Jack Viers
21 Jul 11 at 10:50 pm edit_comment_link(__('Edit', 'sandbox'), ' ', ''); ?>
Once again, this is absolutely normal considering the reflow/repaint mechanism that all browsers use.
If you modify twice a specific style property before a reflow happens, only the second modification will be taken into account.
No need to use a setTimeout to fix that, you can just *trigger* a reflow between the two style modification:
elem.style.top = “10px”;
window.getComputedStyle(elem).getPropertyValue(“top”);
elem.style.top = “100px”;
Lr
louisremi
22 Jul 11 at 4:06 am edit_comment_link(__('Edit', 'sandbox'), ' ', ''); ?>
@louisremi
Once again?
Isnt triggering an additional reflow relatively expensive?
mike
mikechambers
22 Jul 11 at 10:03 am edit_comment_link(__('Edit', 'sandbox'), ' ', ''); ?>
in regards to the detection of finished rendering of inserted DOM elements:
we found out the best approach would be to reset a style property of the node before triggering the css transform -
domObject.style.display = document.defaultView.getComputedStyle(domObject)['display'];
that helps without the need to care about timers etc
as
12 Dec 11 at 1:11 am edit_comment_link(__('Edit', 'sandbox'), ' ', ''); ?>