w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.35k stars 641 forks source link

[css-animations][css-transitions] Isolate animation side-effects? #6398

Open andruud opened 3 years ago

andruud commented 3 years ago

(Using "animations" loosely to refer to any of CSS/Web Animations/Transitions/):

There seems to be an underlying idea in the specifications that effects caused by animations should themselves not trigger new animations. We solve this by making things "not animatable", "animation-tainted", etc. With container queries this problem becomes a bit worse, since the animated size of some element can cause the size of a sibling container (for example) to re-evaluate its container query, hence applying a totally different style to its descendants, including new animation- properties. Probably we need some adjustment to the specs to define what should happen when such animation properties appear (or change) because of "animated re-evaluation" of container queries.

The css-transitions spec describes some alternate-reality-styles (before-change style and after-change style) which is used to determine from/to values. This does give a behavior for container queries, but the before/after-change styles now implicitly become before/after-change layouts. The before-change style is particularly painful, since we would need to do layout in a world where existing animations are updated to the current time, producing different animation effects, potentially causing a change in container query evaluation results, arbitrarily modifying the "previous" style and layout. This sounds pretty complicated to me, and I thought we should explore alternatives. And even if we want to keep css-transitions as is, we probably do need some update for css-animations.

What if we isolate all side-effects that happen because of stuff that's animating, such that it can't trigger new animations:

Not sure how web-compatible this is, or if this loses us any crucial aspect of the current specified behavior. On the other hand, the interop situation seems pretty poor in this area, and no browser seems to implement the current before/after change style correctly, so there might be wiggle-room.

At the moment I don't have any other great ideas for how to deal with animations and container queries.

birtles commented 3 years ago

What if we isolate all side-effects that happen because of stuff that's animating, such that it can't trigger new animations:

  • Let the base style be the style as it would be without any animation effects applied (of any kind), inheriting from the base style of the parent.

I've certainly come across a few cases where having an API to get the un-animated style would be very useful so introducing this concept at a spec level might be a useful step in that direction.

  • Let the before-change style be the base style as of the previous style change event, inheriting from the before-change style of the parent. (I.e. just remember the previous base style).

I'm not sure about this. It's been a while since I've touched this code but from memory the following clause from the CSS Transitions spec is quite important:

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.

Without that, animations end up triggering transitions.

Not sure how web-compatible this is, or if this loses us any crucial aspect of the current specified behavior. On the other hand, the interop situation seems pretty poor in this area, and no browser seems to implement the current before/after change style correctly, so there might be wiggle-room.

I think Firefox should be correct regarding the before/after change styles part including inheritance--the only area where I'm aware it's wrong is with regards to context-sensitive properties like font-size.

andruud commented 3 years ago

Thanks for having a look @birtles.

I'm not sure about this. It's been a while since I've touched this code but from memory the following clause from the CSS Transitions spec is quite important: [...]

Note that I'm redefining the before-change style to be the before-change base style, and the same for the after-change style. The base style does not include any animation/transition effects of any kind, hence the current time of existing animations is not relevant for the purposes of triggering transitions.

I think Firefox should be correct regarding the before/after change styles part including inheritance--the only area where I'm aware it's wrong is with regards to context-sensitive properties like font-size.

I don't doubt that the fundamentals are correct in Firefox. For example I see that all levels are correctly transitioning at the same time in this example. (Not the case in Chrome).

But more interesting are the parts that would be affected by this change, e.g. "complex" inherited interaction between ongoing animations and transitions, as in this example, where no browser gets it right. With this proposal, there would be no transition on #inner at all, since its before/after-change (base) styles are unaffected by animations on #outer, and therefore the before/after-change styles are equal.

Not sure what the full implications of this change would be, and if any losses would be worth it if we gain container queries.

birtles commented 3 years ago

Note that I'm redefining the before-change style to be the before-change base style, and the same for the after-change style. The base style does not include any animation/transition effects of any kind, hence the current time of existing animations is not relevant for the purposes of triggering transitions.

Oh, ok, that makes sense.

But more interesting are the parts that would be affected by this change, e.g. "complex" inherited interaction between ongoing animations and transitions, as in this example, where no browser gets it right. With this proposal, there would be no transition on #inner at all, since its before/after-change (base) styles are unaffected by animations on #outer, and therefore the before/after-change styles are equal.

I think Firefox's CSS handling here is correct but there is a bug due to running background-color animations on the compositor. If you disable the gfx.omta.background-color pref you will see the correct result.

(Quite a while ago I wrote a few tests covering some of these cases but never finished reworking the CSS transitions WPT tests enough that I could add them there: https://bug1192592.bmoattachments.org/attachment.cgi?id=8843824)

andruud commented 3 years ago

disable the gfx.omta.background-color pref you will see the correct result

Ah, I should have chosen the property to animate more carefully. Yes, that looks correct indeed, nice.

flackr commented 3 years ago
  • CSS transitions are triggered using the before-change style/after-change style. (Taking into account the current effect value for re-targeting purposes. The spec already covers that, though).

The starting of transitions will be sort of how it works today based on step 4 of starting transitions in css-transitions-1. I.e. if the end value (which is equivalent to your suggested before-change style) is the same, no new transition is started.

One notable difference with the proposal is that I think setting a property equal to its current animation value normally would have no effect but with your proposed change would start a transition. We could probably carve out an exception for this given that we need to compute the current value to start from anyways.

To make sure I understand the current effect value, if you interrupt a transition like the following:

e.style.transition = 'all 1s linear';
e.style.left = '100px';
setTimeout(() => {
  // getComputedStyle(e).left == '50px'
  e.style.left = '200px';
}, 500);

We would still compute the current effect value from the replaced transition (e.g. left: 50px)?

The animation-/transition- properties are ignored on the animated style, hence if they appear/disappear/update because of container queries re-evaluating, then nothing happens. ("Global animation-tainting").

Does this mean the entire descendant style which matches the container query would be "animated style" rather than base style? E.g. the following wouldn't start an animation on #b?

<style>
@keyframes grow {
  0% { width: 100px;
  100% { width: 1000px;
}
#a {
  animation: grow 5s;
  contain: size;
}

@container (min-width: 500px){
  #b {
    animation: grow 5s;
  }
}
<div id="a">
  <div id="b">
  </div>
</div>
andruud commented 3 years ago

One notable difference with the proposal is that I think setting a property equal to its current animation value normally would have no effect but with your proposed change would start a transition. We could probably carve out an exception for this given that we need to compute the current value to start from anyways.

Not sure if by "current animation value" you mean only effects from CSS/Web-Animations, only effects from transitions, or effects from all of them.

For transitions happening on the same element, it looks like the transition would be canceled in both the current and proposed behavior.

For CSS/Web animations happening on the same element, I think you're right. And yes I suppose we could check the current value for any animations and special-case on that, if necessary.

For transitions/css/web-animations effects inherited from ancestors, I don't think we can do tricks like that. It would just start/update transitions as if no transitions/animations take place in the document.

We would still compute the current effect value from the replaced transition (e.g. left: 50px)?

Yes.

Does this mean the entire descendant style which matches the container query would be "animated style" rather than base style? E.g. the following wouldn't start an animation on #b?

That's right. Animations/transitions are only interpreted on the before/after (base) styles, so if #a's width is less than 500px in the after-change style, #b does not get the animation property applied in the after-change style either.

I was originally going to also say: #b will still ultimately end up with a value getComputedStyle(b).animationName == 'grow', but this is considered a side-effect of #a's animation effect, and therefore, to satisy animations-do-not-trigger-animations, an animation can't be triggered.

However, it probably makes more sense to specify that the computed values of animation- and transition- properties on the animated style are forced to the corresponding values on the after-change style. That way gCS() matches reality to a greater degree.

andruud commented 3 years ago

animation- and transition- properties on the animated style are forced to the corresponding values on the after-change style

Actually, I think it makes sense to do that for all properties which are "not animatable". Otherwise animations can affect writing-mode etc via @container, and writing-mode is supposed to affect how animations resolve.

flackr commented 2 years ago

It's worth considering the complexity in knowing the style without animations means that we would need to perform two layouts. One layout without animations to know which container queries should apply to the base style, and the second for the container queries which end up applying to the animated style.

andruud commented 2 years ago

When there's no "style change event" (i.e. only animations progressing), we also don't need recalculate the base style/layout, since it didn't change. But yes, otherwise that's true.

However, the currently specified definition of "before-change style" already requires us to do a separate style pass (which we currently ignore in Blink) for each style change event. Then container queries comes along and makes it worse by mixing style and layout, so now we also must potentially also do layout twice, using the old style information but with animation effects updated to current time.

flackr commented 2 years ago

Overall this makes sense. I think this is pretty consistent with how you cannot affect animations on descendants using variables due to the animation-tainted rules, though we should revisit those since I think instead of tainting the variables they should use the after-style values.

I remain somewhat concerned about the potential performance cost of multiple layouts, and how easy it will be to understand when some container query animations do not run because the container query was triggered as a result of animation. The latter is already somewhat of an issue with animation-tainted variables and should be addressable with good tooling.

One notable difference with the proposal is that I think setting a property equal to its current animation value normally would have no effect but with your proposed change would start a transition. We could probably carve out an exception for this given that we need to compute the current value to start from anyways.

Not sure if by "current animation value" you mean only effects from CSS/Web-Animations, only effects from transitions, or effects from all of them.

For transitions happening on the same element, it looks like the transition would be canceled in both the current and proposed behavior.

Ah yes, it seems the starting of transitions is already attempting to do something like the proposal here where we look at the non-animated values. I checked and it looks like the reverse-adjusting duration behavior should also work correctly.

For CSS/Web animations happening on the same element, I think you're right. And yes I suppose we could check the current value for any animations and special-case on that, if necessary.

Normally, transitions would not see style changes on elements with active animations because the animation has a higher composite order than the underlying style. I think we'll just need to have a special case here to not start transitions on an element that is currently animating.

However, it probably makes more sense to specify that the computed values of animation- and transition- properties on the animated style are forced to the corresponding values on the after-change style. That way gCS() matches reality to a greater degree.

I agree, the getComputedStyle of "not animateable" properties should be their after-change style.

dbaron commented 2 years ago

@andruud and I were just chatting about this proposal, and I wanted to write a few notes about one issue that we discussed. It seems like one of the undesirable effects of this proposal is what happens when there are animations or transitions triggered by a container query change.

To make this somewhat concrete, suppose we have a component that has container queries that give it substantially different styles at (width < 300px) and (width >= 300px). Then let's consider the 4 cases that result from crossing the following pairs of scenarios (which are intentionally using simple cases of transitions and animations rather than what might perhaps be more realistic ones):

With the current proposal above, I think the results are as follows (and I think these results are undesirable):

I think the expectation is probably that container-query-dependent animations and transitions inside the container should start when the container query starts to apply to layout and to non-animation styles. It's possible this proposal could be modified to produce that result; part of such a modification would include having a style change event inside of the container when the container query evaluation changes, though other changes would be needed as well.

andruud commented 2 years ago

I've implemented a prototype of container-size-change-is-style-change-event, and so far it appears to be a win-win: it gives much more desirable web-facing behavior, and it's also much easier and less expensive to implement (avoids having to know "two layouts" as @flackr was worried about).

Taking this into account, how about this revised proposal:

At first I was worried that this would be a problem for container units, since they act as mini-container queries that dynamically respond to any change in container size. With this proposal, if you define a transition on something that uses container units, and also animate the size of that container, then it would trigger a transition on each frame. However, I think this is expected and non-fatal:

So to me the container units situation looks acceptable.

flackr commented 2 years ago

Overall this sounds great! I'm glad that we can behavior that makes sense to developers

How are container units defined? I would have thought that they would be similar to percentages, viewport units or calc where the computed value is a calc that does not itself change when the input variables change. Unfortunately, I can see that this is not the case at least for vw from the demo I put together to test this. It seems there is plenty of precedent for it starting transitions.

dbaron commented 2 years ago

I think it sounds good to me as well, except that:

  • Let the base style be the style as it would be without any animation effects, inheriting from the base style of the parent.

probably needs to be specified differently, since I think in the revised proposal it does include some animation effects (those resulting from container queries that change in animations, or maybe all container queries), and something will need to define exactly which animation effects are included and which aren't.

andruud commented 2 years ago

How are container units defined? I would have thought that they would be similar to percentages, viewport units or calc where the computed value is a calc that does not itself change when the input variables change.

The only thing that "survives" to used-value time, is %. Everything else, including viewport units and container units are resolved to px at computed-value time, or sooner.

But yes, specifying that container units are resolved used-value time would probably be the way to "fix" the problem, if we were to define this as a problem.

in the revised proposal it does include some animation effects

Yeah, agree. Should be more specific than "animation effects". Perhaps:

And then "animation effects" can still sneak its way into the base, since an animating container can affect the author/user origin of descendants.