Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Event 'transitionend' is called too early, it delays rendering

What I'm doing and what's wrong

When I click on a button, a slider shows up. (here is an example of what it looks like, do not pay attention to this code)

The slider shows via an animation. When the animation is finished I should include an HTML page I've loaded from the server. I need to apply the HTML in the slider after the animation otherwise the animation stops (the DOM is recalculated).

My algorithm

  1. Start the request to get the HTML to display inside the slider
  2. Start the animation
  3. Wait the data to be ready and the transition to be finished

    Why? If I apply the HTML during the animation, it stops the animation while the new HTML is added to the DOM. So I wait for both to end before step 4.

  4. Apply the HTML inside the slider

Here is the shortened code:

// Start loading data & animate transition
var count = 0;
var data = null;

++count;
$.get(url, function (res) {
    data = res;
    cbSlider();
});

// Animation starts here

++count;
$(document).on('transitionend', '#' + sliderId, function () {
    $(document).off('transitionend', '#' + sliderId);
    cbSlider()
});

function cbSlider() {
    --count;
    // This condition is only correct when both GET request and animation are finished
    if (count == 0) {
        // Attempt to enforce the frame to finish (doesn't work)
        window.requestAnimationFrame(() => { return });

        $('#' + sliderId + ' .slider-content').html(data);
    }
}

The detailed issue

transitionend is called too early. It makes the last animated frame a lot too long (477.2ms) and the last frame is not rendered at transitionend event.

Google Chrome Timeline

From the Google documentation, I can tell you that the Paint and Composite step of the Pixel Pipeline is called after the Event(transitionend):

Pixel Pipe Line

Maybe I'm overthinking this.

How should I handle this kind of animations?

How can I wait the animation to be fully finished and rendered?

like image 572
Elfayer Avatar asked Aug 26 '16 13:08

Elfayer


People also ask

What is Transitionend?

The transitionend event occurs when a CSS transition has completed. Note: If the transition is removed before completion, e.g. if the CSS transition-property property is removed, the transitionend event will not fire. For more information about CSS Transitions, see our tutorial on CSS3 Transitions.

What is Transitionend in Javascript?

The transitionend event is fired when a CSS transition has completed. In the case where a transition is removed before completion, such as if the transition-property is removed or display is set to none , then the event will not be generated.


1 Answers

I'm not sure why transitionend is fired before the last frame has rendered, but in this (very crude) test it seems that a setTimeout does help...

The first example shows how the html calculation and injection happens too early. The second example wraps the long running method in a setTimeout and doesn't seem to trigger any interuption in the animation.

Example 1: reproduction of your problem

var ended = 0;
var cb = function() {
  ended += 1;

  if (ended == 2) {
    $(".animated").html(createLongHTMLString());
  }
}

$(".load").click(function() {
  $(".animated").addClass("loading");
  $(".animated").on("transitionend", cb);
  setTimeout(cb, 100);
});

function createLongHTMLString() {
  var str = "";
  for (var i = 0; i < 100000; i += 1) {
    str += "<em>Test </em>";
  }
  return str;
};
.animated,
.target {
  width: 100px;
  height: 100px;
  position: absolute;
  text-align: center;
  line-height: 100px;
  overflow: hidden;
}
.target,
.animated.loading {
  transform: translateX(300%);
}
.animated {
  background: green;
  z-index: 1;
  transition: transform .2s linear;
}
.target {
  background: red;
  z-index: 0;
}
.wrapper {
  height: 100px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div class="wrapper">
  <div class="animated">Loading</div>
  <div class="target"></div>
</div>

<button class="load">load</button>

Example 2: in which a setTimeout seems to fix it

With a setTimeout around the html injection code.

var ended = 0;
var cb = function() {
  ended += 1;

  if (ended == 2) {
    setTimeout(function() {
      $(".animated").html(createLongHTMLString());
    });
  }
}

$(".load").click(function() {
  $(".animated").addClass("loading");
  $(".animated").on("transitionend", cb);
  setTimeout(cb, 100);
});

function createLongHTMLString() {
  var str = "";
  for (var i = 0; i < 100000; i += 1) {
    str += "<em>Test </em>";
  }
  return str;
};
.animated,
.target {
  width: 100px;
  height: 100px;
  position: absolute;
  text-align: center;
  line-height: 100px;
  overflow: hidden;
}
.target,
.animated.loading {
  transform: translateX(300%);
}
.animated {
  background: green;
  z-index: 1;
  transition: transform .2s linear;
}
.target {
  background: red;
  z-index: 0;
}
.wrapper {
  height: 100px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div class="wrapper">
  <div class="animated">Loading</div>
  <div class="target"></div>
</div>

<button class="load">load</button>
like image 174
user3297291 Avatar answered Oct 20 '22 23:10

user3297291