Description
This came up (again) 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.
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.