[csswg-drafts] [css-cascade] Additive CSS

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

== [css-cascade] Additive CSS ==
# Additive CSS

The following material has been prepared for discussion at the August 2017 CSS F2F in Paris, France.

## Outline

The goal of this discussion is to:

1. Explain additive animation and why it was added to Web Animations
2. Propose a general-purpose approach to combining CSS property values that is useful in static contexts too
3. Decide if it is suitable for to ship additive animation in Web Animations and CSS Animations 2 knowing that an overlapping general-purpose feature may arrive in the near future

## Part 1: Additive animation

This section describes what additive animation is, how it works, and why it is part of Web Animations.

### What is additive animation?

Typically when two independent animations target the same property on the same element, one animation will clobber the other.

This can be either because of the nature of the CSS cascade, e.g.

```css
.spin {
  animation: spin 2s infinite;
}
.swell {
  animation: swell 1s alternate 2;
}
@keyframes spin  { to { transform: rotate(1turn) } }
@keyframes swell { to { transform: scale(2) } }

```
[(Demo)](https://jsfiddle.net/p9445uq4/)

Or simply because only one animation is ever applied to a given property, e.g.

```css
div {
  animation: spin 2s infinite, swell 1s alternate 2;
}
@keyframes spin  {
  from { transform: rotate(0turn) }
  to { transform: rotate(1turn) }
}
@keyframes swell {
  from { transform: scale(1) }
  to { transform: scale(2) }
}
```
[(Demo)](https://jsfiddle.net/pxux4L7b/1/)

Additive animation, however, allows the two independent animations to be combined together.

For example, using the Web Animations API we can express the above as:

```js
elem.animate({ transform: 'rotate(1turn)' },
             { duration: 2000, iterations: Infinity });
elem.animate({ transform: 'scale(2)', composite: 'add' },
             { duration: 1000, iterations: 2, direction: 'alternate' });
```
[(Demo - requires Firefox Nightly or Chrome Canary)](https://jsfiddle.net/qwn32qem/)

CSS Animations 2 proposes to expose this using the [`animation-composite`](https://drafts.csswg.org/css-animations-2/#animation-composition) property.

e.g.

```css
div {
  animation: spin 2s infinite, swell 1s alternate 2;
}
@keyframes spin  { to { transform: rotate(1turn) } }
@keyframes swell { to { transform: scale(2); animation-composite: add } }
```

> Note that this only solves the second of the two problem cases above. To solve the case where the CSS cascade results in only one animation appearing in the computed value of `animation-name` we need the more general additive approach described in part 2 of this proposal.

### How does it work?

Firstly, animations that target the same property are arranged in order. This order is important because:

* for some types of properties, the addition operation might not be commutative (e.g. addition of transform lists involves concatenating lists which is equivalent to matrix multiplication which is not commutative), and
* if animation A is additive, but animation B is not, what should be the result? The answer is it depends on which is higher in the stack.

Web animations defines this as an [effect stack](https://w3c.github.io/web-animations/#combining-effects). For animations created using the API the order is roughly equal to the order in which the animations are generated but for CSS animations, it is based on the order of the animations listed in the `animation-name` property.

At the bottom of the stack is the unanimated value, called the _underlying value_. It is possible for a single animation to be combined with the unanimated value using this same mechanism, e.g.

```js
elem.style.transform = 'translate(100px)';
// The following animation represents an offset from the initial translation
elem.animate({ transform: 'translate(100px, 100px)', composite: 'add' },
             { duration: 1000, iterations: 2, direction: 'alternate' });
```
[(Demo - requires Firefox Nightly or Chrome Canary)](https://jsfiddle.net/6pk5ns1p/)

The specific procedures for adding each property needs to be defined just as we define the specific procedures for interpolating each property. The intention is that both the interpolation procedures and addition procedures for common types will be defined in CSS Values and Units and then specifications
that define new property types can defined further procedures.

One technical detail is that in some cases there are multiple notions of addition. For example, for a filter list we can define `blur(2px) + blur(5px)` as either `blur(2px) blur(5px)` or `blur(7px)`.

As it turns out SVG uses *both* these modes. When two independent animations are added together it does list concatenation, i.e. `blur(2px) blur(5px)` from above. When an animation is defined to build on itself on each iteration it adds the function components, i.e.  `blur(7px)` from above.

(Technically SVG only makes this distinction for transform animations and it doesn't actually do list concatenation, but matrix post-multiplication which is functionally equivalent.)

Web Animations calls these two modes `add` and `accumulate` respectively, and collectively calls these [composite operations](https://w3c.github.io/web-animations/#effect-composition).

### Why does Web Animations have additive animation?

The original brief for Web Animations was that it should represent the common base for CSS Animations, CSS Transitions, and SMIL/SVG animation. SMIL/SVG animation has additive animation so therefore Web Animations has additive animation.

### Is it needed?

The most common usage of additive animation is for combining transform animations. A number of these use cases can be addressed using the [individual transform properties](https://drafts.csswg.org/css-transforms-2/#individual-transforms) from CSS Transforms 2. However, this does not cover all use cases such as:

* animating translateX and translateY independently (as required by libraries such as GreenSock Animation)
* animating the same transform component independently (e.g. the translate example above)
* animating other properties such as `filter` (e.g. animating the blur on an element that already has a sepia effect applied), or even `opacity` (e.g. animating the opacity to +50% of its current value).

The other alternative to additive animation is to simply add more and more `<div>` elements to represent each possible independently timed animation component. This approach does not scale well to dynamic animations where a potentially unbounded number of animations might be generated dynamically and having to adding elements for this is, in this author's opinion, one of the more frustrating parts of authoring CSS at the moment.

## Part 2: Static CSS and addition

As it turns out, the notion of combining CSS property values is useful beyond just animations.

### Use cases

e.g. truly generic classes

```css
.nostalgic {
  filter: sepia(60%) !add;
}
.blurry {
  filter: blur(5px) !add;
}
```
```html
<div class="nostalgic"></div>
<div class="blurry"></div>
<div class="nostalgic blurry"></div> <!-- No problem -->
```

e.g. modifying underlying values

```css
.translucent {
  opacity: -50% !add;
}
```

e.g. combining animations defined by different rules that target the same property

```css
.spin {
  animation: spin 2s infinite !add;
  animation-composite: add;
}
.swell {
  animation: swell 1s alternate 2 !add;
  animation-composite: add;
}
```

### Proposal: `!add` annotation

I believe both David Baron and Tab Atkins have at some point considered the idea of an `!add` annotation for this.

Presumably for static CSS the CSS cascade (that is selector specificity etc.) would provide the ordering for composition.

### Issues that need consideration

Some issues that would need to be considered as part of such a proposal:

* Is `!add` enough? Do we need both the `add` and `accumulate` modes described above?
* Are there other modes we need?
  * e.g. multiplication to produce 75% of the current opacity value. Or should that be achieved by extending `calc()` to be able to work with the underlying value?
* Is there some formulation that would allow overriding specific elements of a list so that the following works? (Or should we just tweak specificity to get the correct result?)
```css
button {
  filter: blur(2px);
}
button:focus {
  /* This should override the blur item but not the grayscale item */
  filter: blur(0px);
}
button:disabled {
  filter: grayscale(50%) !add;
}
```
* Can we relax the range checking used when an `!add` annotation is in place so that is is possible to write `opacity: -50% !add`?
  (We already do this internally for SMIL and I suppose we want to do this for `animation-composite` anyway.)

## Part 3: Shipping additive animations

The purpose of raising this proposal is to consider the implications of shipping additive animations now and later introducing a more generalized mechanism.

How would the additive features specified in part 1 change if we also eventually do part 2?

Example 1: Setting the composite mode in a single keyframe

```js
// Current API (part 1)
elem.animate([ { transform: 'translate(100px)' },
               { transform: 'translate(200px)', composite: 'add' ], 2000);

// Possible future API (part 2)
elem.animate([ { transform: 'translate(100px)' },
               { transform: 'translate(200px) !add' ], 2000);

// What if both are combined? (part 1 & part 2)
elem.animate([ { transform: 'translate(100px)' },
               { transform: 'translate(200px) !add',
                 opacity: 0.5,
                 composite: 'accumulate' ],
             2000);
// Presumably we would define a rule that says that if we have an !add
// annotation then that overrides any composite member such that in the above
// 'transform' uses the 'add' composite mode, and 'opacity' uses the
// 'accumulate' composite mode.
// As such, I think we can harmonize these two approaches.
```

Example 2: Setting the composite mode across all keyframes

```js
// Current API (part 1)
elem.animate({ transform: [ 'translate(100px)', 'translate(200px)' ] },
             { composite: 'add', duration: 2000 });

// Possible future API (part 2)
elem.animate({ transform: [ 'translate(100px) !add',
                            'translate(200px) !add' ] },
             2000);

// What if both are combined? (part 1 & part 2)
elem.animate({ transform: [ 'translate(100px)', 'translate(200px) !add' ] },
             { composite: 'accumulate', duration: 2000 });
// Again, presumably we say that !add overrides any composite member such
// that the first value uses 'accumulate' and the second value uses 'add'.
// As such, there does not appear to be any conflict here.
```

I think these same principles could equally apply to the CSS syntax if we choose to ship that too. For example, taking Example 1 and translating it into the proposed CSS syntax from CSS Animations 2:

```css
/* Current API (part 1) */
div {
  animation: move 2s;
}
@keyframes move {
  from { transform: translate(100px); }
  to   { transform: translate(200px); animation-composite: add; }
}

/* Possible future API (part 2) */
div {
  animation: move 2s;
}
@keyframes move {
  from { transform: translate(100px) }
  to   { transform: translate(200px) !add }
}

/* What if both are combined? (part 1 & part 2) */
div {
  animation: move 2s;
}
@keyframes move {
  from { transform: translate(100px) }
  to   { transform: translate(200px) !add;
         opacity: 0.5;
         animation-composite: accumulate }
}
/* As above, the 'transform' value would use 'add' while the 'opacity' value
   would use accumulate. */
```

It seems to me that there is a relatively straightforward path for harmonizing the approach in part 1 if we also do part 2 and that, in fact, the features introduced in part 1 might be useful in combination with part 2 as a way of bulk-setting the composite operation.

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

Received on Wednesday, 12 July 2017 05:41:45 UTC