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-transitions] starting an animation and a transition with equal values yields some incompatible behavior between Safari, Chrome and Firefox #8701

Open
graouts opened this issue Apr 11, 2023 · 6 comments
Labels
css-transitions-1 Current Work

Comments

@graouts
Copy link
Contributor

graouts commented Apr 11, 2023

If I have an element specified as follows:

div {
    width: 0;
    height: 100px;
    background-color: black;
    transition: width 250ms linear;
}

… and later on set the following styles:

div.animated {
    width: 1000px;
    animation: grow 1s forwards linear;
}

@keyframes grow {
    from { width: 0 }
    to   { width: 1000px }
}

… then what is the expected behavior with regards to transition during the lifetime of the explicit CSS Animation started when the animated class is applied? I'm observing a different behavior in each of Chrome Canary, Firefox Nightly and Safari Technology Preview. In all three browsers a CSSAnimation is yielded going from width: 0px to width: 1000px as expected. However, the behavior differs greatly for the transition:

  1. Chrome Canary (114.0.5708.0): no transition is ever started (no transitionstart event and no CSSTransition object visible when calling getAnimations() right after applying the animated class).
  2. Firefox Nightly (114.0a1 (2023-04-11)): a transition going from width: 0px to width: 1000px is started as the animated class is applied and runs for 400ms until its natural completion.
  3. Safari Technology Preview (167): same behavior as Firefox when the animated class is applied, however the transition is canceled and restarted on each frame.

While this does not yield a visible difference because the CSS Animation overrides the CSS Transition, this is still an observable difference through events and getAnimations().

Here's the complete test (sadly GitHub does not allow attaching an HTML file):

<!DOCTYPE html>
<style>

div {
    width: 0;
    height: 100px;
    background-color: black;

    transition: width 250ms linear;
}

div.animated {
    width: 1000px;
    animation: grow 1s forwards linear;
}

@keyframes grow {
    from { width: 0 }
    to   { width: 1000px }
}

</style>

<div></div>

<script>

const div = document.querySelector("div");

const currentWidth = () => getComputedStyle(div).width;

const dumpAnimations = label => {
    const animations = document.getAnimations().map(animation => {
        const keyframes = animation.effect.getKeyframes();
        return `${animation.constructor.name} from ${keyframes[0].width} to ${keyframes[1].width}`;
    });
    console.log(`${label} width is ${currentWidth()} and getAnimations() yields [${animations.join(', ')}]`);
}

const dumpTransitionEvent = event => dumpAnimations(`After ${event.type} fired`);
div.addEventListener("transitionstart", dumpTransitionEvent);
div.addEventListener("transitioncancel", dumpTransitionEvent);
div.addEventListener("transitionend", dumpTransitionEvent);

requestAnimationFrame(() => {
    div.classList.add("animated");
    dumpAnimations("After starting animations");
    requestAnimationFrame(() => dumpAnimations("One frame after starting animations"));
    setTimeout(() => dumpAnimations("100ms after starting animations"), 100);
    setTimeout(() => dumpAnimations("500ms after starting animations"), 500);
});

</script>

Cc people I know to have worked or are working on animations in the affected browsers: @flackr, @BorisChiou and @birtles.

@graouts graouts added the css-transitions-1 Current Work label Apr 11, 2023
@graouts
Copy link
Contributor Author

graouts commented Apr 11, 2023

Here's the exact logging coming from those browsers with that sample:

Chrome
After starting animations width is 0px and getAnimations() yields [CSSAnimation from 0px to 1000px]
One frame after starting animations width is 8.32812px and getAnimations() yields [CSSAnimation from 0px to 1000px]
100ms after starting animations width is 99.9922px and getAnimations() yields [CSSAnimation from 0px to 1000px]
500ms after starting animations width is 499.977px and getAnimations() yields [CSSAnimation from 0px to 1000px]
Chrome
After starting animations width is 0px and getAnimations() yields [CSSTransition from 0px to 1000px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 6.23333px and getAnimations() yields [CSSTransition from 0px to 1000px, CSSAnimation from 0px to 1000px]
One frame after starting animations width is 6.23333px and getAnimations() yields [CSSTransition from 0px to 1000px, CSSAnimation from 0px to 1000px]
100ms after starting animations width is 97.9px and getAnimations() yields [CSSTransition from 0px to 1000px, CSSAnimation from 0px to 1000px]
After transitionend fired width is 256.233px and getAnimations() yields [CSSAnimation from 0px to 1000px]
500ms after starting animations width is 497.9px and getAnimations() yields [CSSAnimation from 0px to 1000px]
Safari
After starting animations width is 0px and getAnimations() yields [CSSTransition from 0px to 1000px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 0px and getAnimations() yields [CSSAnimation from 0px to 1000px]
One frame after starting animations width is 0px and getAnimations() yields [CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 13px and getAnimations() yields [CSSTransition from 0px to 13px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 13px and getAnimations() yields [CSSTransition from 0px to 13px, CSSAnimation from 0px to 1000px]
After transitionend fired width is 13px and getAnimations() yields [CSSTransition from 0px to 13px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 29px and getAnimations() yields [CSSTransition from 0.832px to 29px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 46px and getAnimations() yields [CSSTransition from 2.747424px to 46px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 46px and getAnimations() yields [CSSTransition from 2.747424px to 46px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 63px and getAnimations() yields [CSSTransition from 5.688599px to 63px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 63px and getAnimations() yields [CSSTransition from 5.688599px to 63px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 79px and getAnimations() yields [CSSTransition from 9.356529px to 79px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 79px and getAnimations() yields [CSSTransition from 9.356529px to 79px, CSSAnimation from 0px to 1000px]
100ms after starting animations width is 79px and getAnimations() yields [CSSTransition from 9.356529px to 79px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 96px and getAnimations() yields [CSSTransition from 14.092285px to 96px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 96px and getAnimations() yields [CSSTransition from 14.092285px to 96px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 113px and getAnimations() yields [CSSTransition from 19.66201px to 113px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 113px and getAnimations() yields [CSSTransition from 19.66201px to 113px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 129px and getAnimations() yields [CSSTransition from 25.635641px to 129px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 129px and getAnimations() yields [CSSTransition from 25.635641px to 129px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 146px and getAnimations() yields [CSSTransition from 32.664417px to 146px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 146px and getAnimations() yields [CSSTransition from 32.664417px to 146px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 163px and getAnimations() yields [CSSTransition from 40.371239px to 163px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 163px and getAnimations() yields [CSSTransition from 40.371239px to 163px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 179px and getAnimations() yields [CSSTransition from 48.219479px to 179px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 179px and getAnimations() yields [CSSTransition from 48.219479px to 179px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 196px and getAnimations() yields [CSSTransition from 57.112553px to 196px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 196px and getAnimations() yields [CSSTransition from 57.112553px to 196px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 213px and getAnimations() yields [CSSTransition from 66.5569px to 213px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 213px and getAnimations() yields [CSSTransition from 66.5569px to 213px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 229px and getAnimations() yields [CSSTransition from 75.92926px to 229px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 229px and getAnimations() yields [CSSTransition from 75.92926px to 229px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 256px and getAnimations() yields [CSSTransition from 92.460899px to 256px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 256px and getAnimations() yields [CSSTransition from 92.460899px to 256px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 263px and getAnimations() yields [CSSTransition from 97.039993px to 263px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 263px and getAnimations() yields [CSSTransition from 97.039993px to 263px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 279px and getAnimations() yields [CSSTransition from 107.66143px to 279px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 279px and getAnimations() yields [CSSTransition from 107.66143px to 279px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 296px and getAnimations() yields [CSSTransition from 119.312454px to 296px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 296px and getAnimations() yields [CSSTransition from 119.312454px to 296px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 313px and getAnimations() yields [CSSTransition from 131.327209px to 313px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 313px and getAnimations() yields [CSSTransition from 131.327209px to 313px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 329px and getAnimations() yields [CSSTransition from 142.954269px to 329px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 329px and getAnimations() yields [CSSTransition from 142.954269px to 329px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 346px and getAnimations() yields [CSSTransition from 155.605377px to 346px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 346px and getAnimations() yields [CSSTransition from 155.605377px to 346px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 363px and getAnimations() yields [CSSTransition from 168.552216px to 363px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 363px and getAnimations() yields [CSSTransition from 168.552216px to 363px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 379px and getAnimations() yields [CSSTransition from 180.996872px to 379px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 379px and getAnimations() yields [CSSTransition from 180.996872px to 379px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 396px and getAnimations() yields [CSSTransition from 194.46109px to 396px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 396px and getAnimations() yields [CSSTransition from 194.46109px to 396px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 413px and getAnimations() yields [CSSTransition from 208.165741px to 413px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 413px and getAnimations() yields [CSSTransition from 208.165741px to 413px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 429px and getAnimations() yields [CSSTransition from 221.275131px to 429px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 429px and getAnimations() yields [CSSTransition from 221.275131px to 429px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 446px and getAnimations() yields [CSSTransition from 235.400421px to 446px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 446px and getAnimations() yields [CSSTransition from 235.400421px to 446px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 463px and getAnimations() yields [CSSTransition from 249.721191px to 463px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 463px and getAnimations() yields [CSSTransition from 249.721191px to 463px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 479px and getAnimations() yields [CSSTransition from 263.371033px to 479px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 479px and getAnimations() yields [CSSTransition from 263.371033px to 479px, CSSAnimation from 0px to 1000px]
500ms after starting animations width is 479px and getAnimations() yields [CSSTransition from 263.371033px to 479px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 496px and getAnimations() yields [CSSTransition from 278.033813px to 496px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 496px and getAnimations() yields [CSSTransition from 278.033813px to 496px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 513px and getAnimations() yields [CSSTransition from 292.855499px to 513px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 513px and getAnimations() yields [CSSTransition from 292.855499px to 513px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 529px and getAnimations() yields [CSSTransition from 306.944733px to 529px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 529px and getAnimations() yields [CSSTransition from 306.944733px to 529px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 546px and getAnimations() yields [CSSTransition from 322.044495px to 546px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 546px and getAnimations() yields [CSSTransition from 322.044495px to 546px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 563px and getAnimations() yields [CSSTransition from 337.273468px to 563px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 563px and getAnimations() yields [CSSTransition from 337.273468px to 563px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 579px and getAnimations() yields [CSSTransition from 351.719971px to 579px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 579px and getAnimations() yields [CSSTransition from 351.719971px to 579px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 596px and getAnimations() yields [CSSTransition from 367.175018px to 596px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 596px and getAnimations() yields [CSSTransition from 367.175018px to 596px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 613px and getAnimations() yields [CSSTransition from 382.735107px to 613px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 613px and getAnimations() yields [CSSTransition from 382.735107px to 613px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 629px and getAnimations() yields [CSSTransition from 397.472046px to 629px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 629px and getAnimations() yields [CSSTransition from 397.472046px to 629px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 646px and getAnimations() yields [CSSTransition from 413.215942px to 646px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 646px and getAnimations() yields [CSSTransition from 413.215942px to 646px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 663px and getAnimations() yields [CSSTransition from 429.045258px to 663px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 663px and getAnimations() yields [CSSTransition from 429.045258px to 663px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 679px and getAnimations() yields [CSSTransition from 444.018372px to 679px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 679px and getAnimations() yields [CSSTransition from 444.018372px to 679px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 696px and getAnimations() yields [CSSTransition from 459.997131px to 696px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 696px and getAnimations() yields [CSSTransition from 459.997131px to 696px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 713px and getAnimations() yields [CSSTransition from 476.045319px to 713px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 713px and getAnimations() yields [CSSTransition from 476.045319px to 713px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 729px and getAnimations() yields [CSSTransition from 491.210419px to 729px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 729px and getAnimations() yields [CSSTransition from 491.210419px to 729px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 746px and getAnimations() yields [CSSTransition from 507.380096px to 746px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 746px and getAnimations() yields [CSSTransition from 507.380096px to 746px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 763px and getAnimations() yields [CSSTransition from 523.606262px to 763px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 763px and getAnimations() yields [CSSTransition from 523.606262px to 763px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 779px and getAnimations() yields [CSSTransition from 538.92749px to 779px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 779px and getAnimations() yields [CSSTransition from 538.92749px to 779px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 796px and getAnimations() yields [CSSTransition from 555.252441px to 796px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 796px and getAnimations() yields [CSSTransition from 555.252441px to 796px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 813px and getAnimations() yields [CSSTransition from 571.623291px to 813px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 813px and getAnimations() yields [CSSTransition from 571.623291px to 813px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 829px and getAnimations() yields [CSSTransition from 587.071411px to 829px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 829px and getAnimations() yields [CSSTransition from 587.071411px to 829px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 846px and getAnimations() yields [CSSTransition from 603.522583px to 846px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 846px and getAnimations() yields [CSSTransition from 603.522583px to 846px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 863px and getAnimations() yields [CSSTransition from 620.011047px to 863px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 863px and getAnimations() yields [CSSTransition from 620.011047px to 863px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 879px and getAnimations() yields [CSSTransition from 635.562317px to 879px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 879px and getAnimations() yields [CSSTransition from 635.562317px to 879px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 896px and getAnimations() yields [CSSTransition from 652.116089px to 896px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 896px and getAnimations() yields [CSSTransition from 652.116089px to 896px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 913px and getAnimations() yields [CSSTransition from 668.700195px to 913px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 913px and getAnimations() yields [CSSTransition from 668.700195px to 913px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 929px and getAnimations() yields [CSSTransition from 684.335388px to 929px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 929px and getAnimations() yields [CSSTransition from 684.335388px to 929px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 945px and getAnimations() yields [CSSTransition from 699.993896px to 945px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 945px and getAnimations() yields [CSSTransition from 699.993896px to 945px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 963px and getAnimations() yields [CSSTransition from 717.634338px to 963px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 963px and getAnimations() yields [CSSTransition from 717.634338px to 963px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 979px and getAnimations() yields [CSSTransition from 733.337769px to 979px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 979px and getAnimations() yields [CSSTransition from 733.337769px to 979px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 996px and getAnimations() yields [CSSTransition from 750.042786px to 996px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 996px and getAnimations() yields [CSSTransition from 750.042786px to 996px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 1000px and getAnimations() yields [CSSTransition from 766.767883px to 1000px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 1000px and getAnimations() yields [CSSTransition from 766.767883px to 1000px, CSSAnimation from 0px to 1000px]
After transitioncancel fired width is 1000px and getAnimations() yields [CSSTransition from 766.767883px to 1000px, CSSAnimation from 0px to 1000px]
After transitionstart fired width is 1000px and getAnimations() yields [CSSTransition from 766.767883px to 1000px, CSSAnimation from 0px to 1000px]
After transitionend fired width is 1000px and getAnimations() yields [CSSAnimation from 0px to 1000px]

@graouts
Copy link
Contributor Author

graouts commented Apr 11, 2023

Debugging WebKit, I can explain the Safari behavior. For each frame, we fall into the following case from the Starting of transitions section of the CSS specification:

If the element has a running transition for the property, there is a matching transition-property value, and the end value of the running transition is not equal to the value of the property in the after-change style, then:

… and then:

Otherwise, implementations must cancel the running transition and start a new transition whose:

It's not clear to me (yet) if that is correct.

@dbaron
Copy link
Member

dbaron commented Apr 11, 2023

Also cc @andruud (and noting that this is related to #6398 and #6688).

@flackr
Copy link
Contributor

flackr commented Apr 11, 2023

Strictly literally following the spec text for starting of transitions, I believe a transition is expected to start. In particular, the after-change style explicitly uses the computed values of the animation-* properties from the before-change style which don't have the new animation.

However, I don't think this is ideal behavior. If the animation were started slightly earlier than the underlying style change the transition wouldn't have started. Also, the canceling and restarting of transitions due to a running of an animation feels like a degenerate behavior resulting from this. Further, the transition is entirely covered up by the animation. I think if an animation will be active and replace the property value (i.e. with the default composite mode replace) that we shouldn't start a transition.

@birtles
Copy link
Contributor

birtles commented Apr 12, 2023

I haven't touched this code for a long time (@emilio @BorisChiou or @hiikezoe are probably more familiar with it) but for the discrepancy between Safari and Firefox, it looks like Firefox aborts updating transitions early if the before-change and after-change styles match:

https://searchfox.org/mozilla-central/rev/5f10809bf5559e6e988e4d1a58ce1338d391cc5f/servo/components/style/animation.rs#1038-1049

They will match in this case due to the definition of the before-change style:

Otherwise, define the before-change style as the computed values of all properties on the element as of the previous style change event, except with any styles derived from declarative animations such as CSS Transitions, CSS Animations ([CSS3-ANIMATIONS]), and SMIL Animations ([SMIL-ANIMATION], [SVG11]) updated to the current time.

That is, the before-change style is brought up to date with the after-change style.

However, the spec only skips cancelling/starting transitions for the "no change" case if there are no running transitions for the property so Firefox is wrong here.

(For what it's worth, a long time ago I wrote some other tests for various edge cases involving CSS animations and transitions here: https://bug1192592.bmoattachments.org/attachment.cgi?id=8843824)

@graouts
Copy link
Contributor Author

graouts commented Apr 12, 2023

For reference, this came up while debugging WebKit bug 255338.

webkit-commit-queue pushed a commit to graouts/WebKit that referenced this issue Apr 12, 2023
https://bugs.webkit.org/show_bug.cgi?id=255338
rdar://107532064

Reviewed by Dean Jackson.

When creating a new animation of any type (CSS Transition, CSS Animation, Web Animations API)
that is accelerated, we add it to the list of animations on GraphicsLayerCA regardless of its
composite order relative to other effects for the given target.

In the case of https://payto.com.au, a CSS Animation is applied to an element for the "transform"
property, and that property also yields a CSS Transition that is canceled and recreated on each
frame (whether this is the right behavior is discussed in w3c/csswg-drafts#8701
as Firefox, Chrome and Safari all have different behavior).

That perpetually-recreated CSS Transition is lower in the composite order, but since it's created
after the CSS Animation, due to how we create accelerated animations it would override the CSS Animation.

In this patch we change the behavior of DocumentTimeline::applyPendingAcceleratedAnimations() to
update the entire effect stack with which an effect pending application of an accelerated action
is associated. This guarantees the effect stack's order to be preserved.

We also ensure we remove any similarly-named animation before adding new animations in GraphicsLayerCA.

* LayoutTests/webanimations/accelerated-animation-addition-lower-in-effect-stack-expected.html: Added.
* LayoutTests/webanimations/accelerated-animation-addition-lower-in-effect-stack.html: Added.
* Source/WebCore/animation/DocumentTimeline.cpp:
(WebCore::DocumentTimeline::applyPendingAcceleratedAnimations):
* Source/WebCore/animation/KeyframeEffect.cpp:
(WebCore::KeyframeEffect::applyPendingAcceleratedActionsOrUpdateTimingProperties):
* Source/WebCore/animation/KeyframeEffect.h:
* Source/WebCore/animation/KeyframeEffectStack.cpp:
(WebCore::KeyframeEffectStack::applyPendingAcceleratedActions const):
* Source/WebCore/animation/KeyframeEffectStack.h:
* Source/WebCore/platform/graphics/ca/GraphicsLayerCA.cpp:
(WebCore::GraphicsLayerCA::createTransformAnimationsFromKeyframes):
(WebCore::GraphicsLayerCA::createFilterAnimationsFromKeyframes):

Canonical link: https://commits.webkit.org/262875@main
aperezdc pushed a commit to WebKit/WebKit that referenced this issue Apr 24, 2023
…gi?id=255338

    REGRESSION (260399@main): animations flicker on https://payto.com.au
    https://bugs.webkit.org/show_bug.cgi?id=255338
    rdar://107532064

    Reviewed by Dean Jackson.

    When creating a new animation of any type (CSS Transition, CSS Animation, Web Animations API)
    that is accelerated, we add it to the list of animations on GraphicsLayerCA regardless of its
    composite order relative to other effects for the given target.

    In the case of https://payto.com.au, a CSS Animation is applied to an element for the "transform"
    property, and that property also yields a CSS Transition that is canceled and recreated on each
    frame (whether this is the right behavior is discussed in w3c/csswg-drafts#8701
    as Firefox, Chrome and Safari all have different behavior).

    That perpetually-recreated CSS Transition is lower in the composite order, but since it's created
    after the CSS Animation, due to how we create accelerated animations it would override the CSS Animation.

    In this patch we change the behavior of DocumentTimeline::applyPendingAcceleratedAnimations() to
    update the entire effect stack with which an effect pending application of an accelerated action
    is associated. This guarantees the effect stack's order to be preserved.

    We also ensure we remove any similarly-named animation before adding new animations in GraphicsLayerCA.

    * LayoutTests/webanimations/accelerated-animation-addition-lower-in-effect-stack-expected.html: Added.
    * LayoutTests/webanimations/accelerated-animation-addition-lower-in-effect-stack.html: Added.
    * Source/WebCore/animation/DocumentTimeline.cpp:
    (WebCore::DocumentTimeline::applyPendingAcceleratedAnimations):
    * Source/WebCore/animation/KeyframeEffect.cpp:
    (WebCore::KeyframeEffect::applyPendingAcceleratedActionsOrUpdateTimingProperties):
    * Source/WebCore/animation/KeyframeEffect.h:
    * Source/WebCore/animation/KeyframeEffectStack.cpp:
    (WebCore::KeyframeEffectStack::applyPendingAcceleratedActions const):
    * Source/WebCore/animation/KeyframeEffectStack.h:
    * Source/WebCore/platform/graphics/ca/GraphicsLayerCA.cpp:
    (WebCore::GraphicsLayerCA::createTransformAnimationsFromKeyframes):
    (WebCore::GraphicsLayerCA::createFilterAnimationsFromKeyframes):

    Canonical link: https://commits.webkit.org/262875@main
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
css-transitions-1 Current Work
Projects
None yet
Development

No branches or pull requests

4 participants