Skip to content

[css-animations] Resolving dependencies in keyframes #5125

Closed
@andruud

Description

@andruud

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.

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions