[csswg-drafts] [css-animations] Resolving dependencies in keyframes (#5125)

andruud has just created a new issue for https://github.com/w3c/csswg-drafts:

== [css-animations] Resolving dependencies in keyframes ==
This came (again) up during @kevers-google's in-progress implementation of getKeyframes in Chrome. Since getKeyframes directly exposes computed keyframes, it forces us to deal with how/when dependencies in the keyframe are resolved in more detail.

We should in my opinion take a "broad" approach of thinking about all dependencies, and not specify detailed behavior per case. For example, `1em` has a dependency on `font-size`, and `var(--x)` has a dependency on `--x`. In terms of dependencies, `1em` and `var(--x)` are very similar, so for consistency, these cases should be treated the same way.

Browsers generally don't agree on how interpolation actually happens when complicated dependencies are involved. I won't enumerate all differences, but instead highlight this (well-known) example:

@keyframes test {
  from { font-size: 2px; width: 10em; }
  to { font-size: 4px; width: 20em; }

div {
  font-size: 10px;
  animation: test 10s -5s linear paused; /* t=0.5 */

The computed value of `width` on `div` for the different browsers:

 * Firefox: `150px`. (1)
 * Safari: `50px`. (2)
 * Chrome: `45px`. (3)

@kevers-google, @flackr and myself have had several discussion about this recently, and three different ways of dealing with it surfaced in those discussions:

## 1. Resolving against the pre-animated style

In this model, the `em` units would resolve against the would-be computed value of `font-size` without animation effects, i.e. `10px` in the first example. The interpolation for `width` would take place in absolute values, between `100px` and `200px`. For `var()` references, they would behave similarly: the value would be substituted using the pre-animated computed value of the referenced custom property.

I think this solution is a nice one in terms of technical complexity, but I'm not sure if it matches author expectation. 

This appears to be what Firefox is doing (for `em` units).

## 2. Resolving against local values in the keyframe

In this approach, each keyframe is basically a list of declarations that's added to the cascade, and we derive computed values from that. In approximated spec language:

 * Let _base cascade_ be the cascade as it would be without animation/transition affects added.
 * For each specified keyframe:
   * Let _keyframe cascade_ be a copy of the base cascade
   * Add the declarations of the keyframe to the keyframe cascade, at the animations level.
   * Produce a _computed keyframe_ by substituting each value in the specified keyframe with the corresponding computed value from the keyframe cascade.
 * Interpolation then happens between computed keyframes.

In the first example, `em` units would then resolve against the `font-size` specified locally in the keyframe (if present). The interpolation of `width` would be between `20px` and `80px`.

This approach would match my initial expectation as an author, I think. But not sure how practical it would be to implement this. Although Safari has the apparent behavior of this model already, at least for the example I gave.

Note that an interesting side-effect of this suggestion is that !important declarations from the base style can end up in the computed keyframes. 

## 3. Resolving against the (animated) computed value

This is what Chrome is currently doing for `em` units (but not for `var()`). In the example, `width` interpolates in `em`-space from `10em` to `20em`, at the same time as `font-size` interpolates from `2px` to `4px`. The `em` unit is resolved computed-value time (as apposed to before the effect value is added to the cascade), against the animated `font-size` at that time. So Chrome produces `45px` in the first example via `15em * 3px = 45px`.

For `var()` it would mean that, for this example:

@keyframes test {
  from { --a: 10px; --b: 30px: width: var(--a); }
    to { --a: 20px; --b: 40px: width: var(--b); }

div {
  animation: test 1s;

The animation would add a value to the cascade that can be illustrated by: `calc(var(--a) + t * (var(--b) - var(--a)))`. In other words, like for `em` resolution, the `var()` resolution would be delayed until computed-value time. (Again, Chrome does not currently do this, but it would be the consistent thing to do in this model).

Not sure what I think about this behavior. On the one hand it produces unnecessarily complex animation behavior, but on the other hand it means the concept of "computed values in keyframes" can be mostly avoided, which has a certain simplicity.

Note that getKeyframes in this model should in my opinion not return the keyframes as if "all property values are replaced with their computed values" as the spec currently suggest, as this would misrepresent the endpoints of the interpolation.


So at this point I was hoping for some feedback regarding which approach is better, whether or not the author really cares about this at all, how compatible each solution is w.r.t. web-animations, and whether or not there are other approaches that should be considered. Thanks.

cc @birtles @dbaron

PS: I realize this topic has been discussed many times before, and I apologize for not digging up all prior discussion in advance. (Please add links). At the same time, I hope we can avoid treating prior resolutions as automatically holy and immutable, since they apparently failed (so far) to effectuate any real consensus in browser behavior.

Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/5125 using your GitHub account

Received on Thursday, 28 May 2020 11:00:21 UTC