Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSS transition start value is not computed correctly #7816

Closed
paulrouget opened this issue Oct 1, 2015 · 24 comments
Closed

CSS transition start value is not computed correctly #7816

paulrouget opened this issue Oct 1, 2015 · 24 comments

Comments

@paulrouget
Copy link
Contributor

In the example below, the transform:translateX value is incremented every 2 seconds. A CSS transition animates the div. But during the animation, the start value is not the value set via the style attribute, but the initial value set in the CSS rule.

<style>
  div {
    position: absolute;
    top: 10px;
    left: 10px;
    width: 100px;
    height: 100px;
    background: red;
    transform: translateX(-100px);
    transition: transform 1500ms linear;
  }
</style>

<body>
  <div></div>
</body>

<script>
  var i = 0;
  setInterval(() => {
    var transform = `transform:translateX(${i * 50}px)`;
    document.querySelector('div').setAttribute('style', transform);
    i = (i + 1) % 4;
  }, 2000);
</script>

EDIT

Here is an example that highlight all the problems I found while working on this issue:

<style>
  div {
    position: absolute;
    top: 10px;
    left: 10px;
    width: 100px;
    height: 100px;
    background: red;
    transition: all 2000ms linear;
    opacity: 0.1;
  }

  div.foo {
    width: 200px;
    opacity: 1;
    transform: translateY(200px);
  }
</style>

<div></div>

<script>
  var div = document.querySelector('div');
  setTimeout(() => div.classList.toggle('foo'), 1000);
  setTimeout(() => div.classList.toggle('foo'), 4000);
  setTimeout(() => div.classList.toggle('foo'), 5000);
</script>
@paulrouget
Copy link
Contributor Author

Maybe related to #7781.

@jdm
Copy link
Member

jdm commented Oct 1, 2015

Presumably we're getting incorrect values in http://mxr.mozilla.org/servo/source/components/layout/animation.rs#22 . The trick is figuring out why that's happening.

@paulrouget
Copy link
Contributor Author

The new style is reset to old style in start_transitions_if_applicable:

property_animation.update(new_style, 0.0);

then it's supposed to be set to the final style in cascade_node_pseudo_element:

animation.property_animation.update(&mut *Arc::make_mut(style), 1.0);

But cascade_node is not called once the animation is over.

@paulrouget
Copy link
Contributor Author

@pcwalton why is this_style reset to its initial value if there's a transition?

@paulrouget
Copy link
Contributor Author

Can animations be interrupted or updated? Doesn't appear to work. Did it ever work or it's not implemented yet?

@pcwalton
Copy link
Contributor

pcwalton commented Oct 8, 2015

@paulrouget I'm confused by what "reset to its initial value" means here—could you elaborate?

It's not safe to call cascade_node_pseudo_element during transitions because they run off the main thread.

@paulrouget
Copy link
Contributor Author

@paulrouget I'm confused by what "reset to its initial value" means here—could you elaborate?

Sure. Keep in mind that I'm really not familiar with this code, so feel free to tell me if I'm totally off track :)

I'm looking at when animation::start_transitions_if_applicable is called (https://mxr.mozilla.org/servo/source/components/layout/css/matching.rs#483):

// Trigger transitions if necessary. This will reset `this_style` back to its old value if
// it did trigger a transition.

… and in animation::start_transitions_if_applicable (https://mxr.mozilla.org/servo/source/components/layout/animation.rs#24):

// Set the property to the initial value.
property_animation.update(new_style, 0.0);

So in cascade_node_pseudo_element, we do not update the style until the animation is finished. The final update happens here: https://mxr.mozilla.org/servo/source/components/layout/css/matching.rs#449

Without digging too much, I just assumed that it meant that when a DOM element goes from transition:scale(1) to transition:scale(2), we keep the computed style at transition:scale(1) until the transition is done, then update to transition:scale(2).

By "reset to its initial value", I meant that we do not switch to transition:scale(2) immediately.

Assuming I assumed right:

Like you said, we can't update it during the transition, so it's done at the end. But:

  1. Why is it done at the end and not before the transition start? It doesn't impact the transition itself. And what about getComputedStyle() in the middle of a transition?
  2. Even if we want to do it at the end, the above-mentionned code is not called at the end of the transition (and really, that's the only thing I think we need to fix for this bug).
  3. If a transition on the same property happens in the middle of the transition, where would we find the current computed value to compute the new animation?

@paulrouget
Copy link
Contributor Author

Hmm, there's something wrong in my reasoning. The computed style (as seen from JS), is correctly updated during the transition and, of course, layout is properly updated overtime. Edit: nope. See next comment.

@paulrouget
Copy link
Contributor Author

The computed style (as seen from JS), is correctly updated during the transition and, of course, layout is properly updated overtime.

Not all of them! opacity style (getComputedStyle().opacity) is not updated. width is.

  div {
    position: absolute;
    top: 10px;
    left: 10px;
    width: 100px;
    height: 100px;
    background: red;
    transition: all 1000ms linear;
    opacity: 0.1;
  }

  div.foo {
    width: 200px;
    opacity: 1;
    transform: translateY(200px);
  }

3 properties are being animated.

From div to div.foo, on screen, everything is animated correctly. The computed style (as seen from JS) of width is properly updated. Opacity is not (stays at 0.1). Transform can't be printed (empty string), so I can't tell, but I guess it behaves like the opacity property.

From div.foo to div, on screen, transform and opacity are properly reset to their initial value (none and 0.1), but without the transition (because Servo animates from the very initial state, skipping the div.foo values). And the width property stays at 200px. As for the computed style, it is stuck at the div.foo values for the 3 properties.

Opacity and transform are properties that do not require reflow. They have that in common. So I guess that explain why they behave differently (something like OMTA in Gecko).

So here, I see 3 issues, and I don't know how much they are related:

  • computed style (as seen from JS) of opacity and transforms are never updated
  • width of the div (on screen and computed style as seen from JS) is not updated on the 2nd animation
  • For 2 animations chaining (state A -> B -> C), on the 2nd animation (B -> C), the opacity and transform values at t0 come from A, not from B

@paulrouget
Copy link
Contributor Author

Commenting out this line:

https://mxr.mozilla.org/servo/source/components/layout/animation.rs#33

solves all the issues mentioned above.

But I'm not sure to understand why it's here in the first place.

There's a very short and barely noticeable flickering at the beginning of the animation where the width of the div jumps to its final value, and then, it's properly animated. I guess this is what the reset to 0.0 is for. So then, what would be missing is to call cascade_node_pseudo_element once the animation is over I guess.

@paulrouget
Copy link
Contributor Author

@pcwalton: can you please explain why this line is necessary: https://mxr.mozilla.org/servo/source/components/layout/animation.rs#33

Can you confirm that cascade_node_pseudo_element should be called at the end of the animation to trigger this code: https://mxr.mozilla.org/servo/source/components/layout/css/matching.rs#449 - if so, how can this be done?

@paulrouget
Copy link
Contributor Author

I guess we could do that on Msg::ChangeRunningAnimationsState.

@pcwalton
Copy link
Contributor

You can't call cascade_node_pseudo_element during animations. That's for nodes, not for fragments.

@pcwalton
Copy link
Contributor

To be more specific, there is no access to the DOM during animations. It's forbidden, because JS could be running concurrently during a transition (which is the whole point of OMT animations).

@pcwalton
Copy link
Contributor

That line is trying to set the animation to its original start value, but I guess the start value for the animation isn't properly taking the cascade into account. We will need to fix the code that initializes the animation. You can't access the DOM during animations, so this has to be all set up properly in advance.

@paulrouget
Copy link
Contributor Author

We should keep a list of finished animations, then at the next cascade, we can update the property value. Also, if animations are not finished, we should cancel them and update the style according to the current time.

@pcwalton does this require some big changes or it's just a matter of piping the right things together? In other words, do you think I can fix that or it's better if you take care of it?

@metajack
Copy link
Contributor

metajack commented Nov 2, 2015

Pinging @pcwalton

This is blocking browser.html progress.

@pcwalton
Copy link
Contributor

pcwalton commented Nov 3, 2015

@paulrouget I don't think it'll be too tough—a medium-sized change, perhaps?

@paulrouget
Copy link
Contributor Author

In cascade_node_pseudo_element, at the beginning of the function, when we go through the animations and update the style to its progress=1.0 value, we should not assume the animation is over and update it to the its real progress value.

Something like replacing:

animation.property_animation.update(&mut *Arc::make_mut(style), 1.0);

with:

let now = clock_ticks::precise_time_s();
let mut progress = (now - animation.start_time) / animation.duration();
if progress > 1.0 {
  progress = 1.0
}
if progress > 0.0 {
  animation.property_animation.update(&mut *Arc::make_mut(style), progress);
}

This appears to work.

We should also cancel the animation at this point (not sure how yet).

Now, we also need to handle finished animations here, but finished animations are not accessible at this point. So in components/layout/animation.rs#update_animation_state, I'd suggest that we fill a finished_animations array (the same way we have a running_animations array), that we would flush in cascade_node_pseudo_element after we updated the style to its progress=1.0 value.

@paulrouget
Copy link
Contributor Author

@pcwalton could you take a quick look at this commit: b8379dd

I just want to know if that looks like the proper approach.

I still need to cancel animations in cascade_node_pseudo_element and flush the finished_animations array.

@pcwalton
Copy link
Contributor

pcwalton commented Nov 4, 2015

Seems reasonable to me.

@paulrouget
Copy link
Contributor Author

Well, I think this is beyond what I can do. Things are getting hairy when trying to cancel the animation on a property that is being modified in cascade_node. Maybe someone else can take over. Summary of what (I think) should be done:

  1. save a list of finished animation, then, at the next cascade, the property value can be updated (at progress=1.0) (see a quick & dirty implementation here: b8379dd). The finished animation list should be flushed once the new property value has been computed.
  2. on each cascade, update the value of the animated properties (also addressed in b8379dd)
  3. cancel animations on properties that have changed. Maybe we need a sender/receiver for canceled animations. update_animation_state would check if there's any canceled animations and update the running animation list.

@metajack
Copy link
Contributor

@pcwalton agreed to take this

@pcwalton
Copy link
Contributor

I have a fix for this locally. Testing performance now.

pcwalton added a commit to pcwalton/servo that referenced this issue Nov 25, 2015
animations complete or are interrupted.

This adds a new pair of reader-writer locks. I measured the performance
of style recalculation on Wikipedia and the overhead of the locks was
not measurable.

Closes servo#7816.
bors-servo pushed a commit that referenced this issue Nov 25, 2015
Write animated values into the `ComputedValues` structures when animations complete or are interrupted.

This adds a new pair of reader-writer locks. I measured the performance
of style recalculation on Wikipedia and the overhead of the locks was
not measurable.

Closes #7816.

cc @paulrouget

r? @glennw

<!-- Reviewable:start -->
[<img src="https://reviewable.io/review_button.png" height=40 alt="Review on Reviewable"/>](https://reviewable.io/reviews/servo/servo/8670)
<!-- Reviewable:end -->
paulrouget pushed a commit to paulrouget/servo that referenced this issue Nov 25, 2015
animations complete or are interrupted.

This adds a new pair of reader-writer locks. I measured the performance
of style recalculation on Wikipedia and the overhead of the locks was
not measurable.

Closes servo#7816.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants