[csswg-drafts] [web-animations-1] Alternative to FillAnimation: replace events (#3689)

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

== [web-animations-1] Alternative to FillAnimation: replace events ==
In #3210 and #2054 I have been working on a proposal to allow filling animations to not leak memory. I have [prepared spec text](https://github.com/w3c/csswg-drafts/compare/master...birtles:fill-animation) for that and implemented (but not yet landed) it in Firefox but it is quite complex. This is an alternative approach that might be simpler.

Summary
---------

Proposal: automatically cancel any completely replaced filling script animations and fire a 'replace' event that includes their animated style so the author can manually set that on the element if they wish to preserve the animation's effect:

e.g. instead of

```js
left.addEventListener('click', () => {
  target.animate(
    { transform: 'translateX(-80px)', composite: 'add' },
    { duration: 1000, easing: 'ease', fill: 'forwards' }
  );
});
```
[(Live example)](https://codepen.io/birtles/pen/jJbNmo)

The author would be required to write:

```js
left.addEventListener('click', () => {
  const animation = target.animate(
    { transform: 'translateX(-80px)', composite: 'add' },
    { duration: 1000, easing: 'ease', fill: 'forwards' }
  );
  animation.onreplace = evt => {
    target.style.transform = evt.computedStyle.transform;
  };
});
```

When additive animations are not in use, the authors would typically be free to ignore the event unless they planned to cancel the overriding animation.

Proposal
--------

Define a forever-filling animation as:

*   The existence of the animation is _not_ prescribed by markup.
    That is, it is _not_ a CSS animation with an owning element, nor a CSS transition with an owning element.
*   The animation's play state is finished.
*   The animation is associated with a monotonically increasing timeline.
*   The animation has an associated target effect.
*   The target effect associated with the animation is in effect (i.e. it has the appropriate fill mode).

Any time a forever-filling animation finishes, run the following steps:

1.  Look up the effect stack for the target element of the animation that finished.
    (Once we introduce `GroupEffect`s, this will involve visiting all target elements.)

2.  Let _replaced animations_ be an empty set.

3.  Proceeding from the bottom of the effect stack[1], for each forever-filling animation, _animation_, where _all_ the target properties (once resolved to physical longhand properties) are also specified by one or more forever-filling animations that have a _greater_ composite order:

    1.  Calculate the computed value of each of _animation_'s target effect's target properties at its current position in the effect stack.

    2.  Add (_animation_, _computed values_) to _replaced animations_.

4.  For each pair (_animation_, _computed values_) in _replaced animations_:

    1.  Cancel _animation_ (but don't dispatch a 'cancel' event? See below).

    2.  Create an `AnimationReplaceEvent`, _replaceEvent_.

        (I'm not sure if we should re-use `AnimationPlaybackEvent` or not here.)

    3.  Set _replaceEvent_’s `type` attribute to 'replace'.

    4.  Set _replaceEvent_’s `computedStyle` attribute to _computed values_.

        (For implementations that implement CSS Typed OM, there is also
        a `computedStyleMap` attribute.)

    5.  Set _replaceEvent_’s `timelineTime` attribute to the current time of the timeline with which _animation_ is associated. If _animation_ is not associated with a timeline, or the timeline is inactive, let timelineTime be null.

    6.  If _animation_ has a document for timing, then append _replaceEvent_ to its document for timing's pending animation event queue along with its target, animation. For the scheduled event time, use the result of converting the most recently finished overriding animation’s target effect end to an origin-relative time.

        Otherwise, queue a task to dispatch _replaceEvent_ at animation. The task source for this task is the DOM manipulation task source.

[1] Obviously no one would actually implement this as proceeding from the bottom of the stack. You'd almost certainly want to proceed from the top and go down.  The point here is just to ensure that the replaced animations and hence the replace events are dispatched in order from lower in stack to higher.

Since replace events are dispatched in composite order, script can blindly just set the computed style on the target element and wait for subsequent events to override it.

Given that replace events are typically queued as part of updating timing and run before updating the rendering, there should be no visual glitch as a result of updating style in this manner.

In order to generate the replace events an additional style flush will typically be required to compute the correct computed style.  Implementations might want to avoid doing this when there are no replace event listeners registered. Such an optimization could possibly produce Web-observable changes with regards to dispatching transitions so the behavior with regard to style change events might need to be specified.

IDL
---

```webidl
partial interface Animation {
    attribute EventHandler onreplace;
}

[Exposed=Window,
 Constructor (DOMString type, optional AnimationReplaceEvent eventInitDict)]
interface AnimationReplaceEvent : Event {
    readonly attribute double? timelineTime;
    readonly attribute CSSStyleDeclaration computedStyle;
    readonly attribute StylePropertyMapReadOnly computedStyleMap;
};
dictionary AnimationReplaceEventInit : EventInit {
    double? timelineTime = null;
};
```

Relationship to finish events
-----------------------------

Finish events sort before replace events. It is the finishing of a higher-priority animation that triggers the replacing.

For an animation that is replaced as soon as it finishes, the finish event still fires first since logically it finishes before it is replaced.

Relationship to the `finished` Promise
--------------------------------------

Since finishing happens before replacing, the `finished` promise will be resolved and hence will not be rejected when an animation is subsequently canceled by being replaced.

Relationship to cancel events
-----------------------------

I suspect cancel events should _not_ be fired even though we effectively cancel these animations. Dispatching cancel events will likely confuse authors and might break existing content. Rather, we should consider replacing a special kind of canceling; neutering perhaps.

Side effects
------------

Replace events give the author the computed animated style for the effect that is being canceled so that they can apply it directly. However, since the returned value will be comprised of computed values for longhand (and probably physical-only) properties, any context-sensitive values (`em` units, CSS variables(?) etc.) will be lost. Furthermore, since CSS cannot yet represent additive values in static contexts, any additive behavior will also be lost.

These side effects should at least be somewhat apparent from the naming (`computedStyle`) and steps taken to achieve the effect.

Compatibility
-------------

Currently only effects that fully replace the underlying value are shipped so the only situation where this proposal would produce an observable change is if an overriding forever-filling animation is canceled while there is an underlying forever-filling animation.  Under this proposal, the underlying animation would have been canceled so the result would differ.

I don't expect this is common but usage of the cancel() method is [surprisingly high (3% of page loads!)](https://www.chromestatus.com/metrics/feature/timeline/popularity/699)

Issues
------

Ideally it would be good to offer a way for authors to opt-out of the replacing behavior.

For example, if, in the replace event callback, the author could re-instate the animation to its filling state, they could avoid the side effects mentioned above. This might produce a situation that effectively leaks memory, but it would be at the author's explicit direction much as if they created new Elements ad infinitum (and unlike the case where they fire off filling animations without necessarily being aware the animations will never disappear).

I don't have any idea for what a suitable API for reinstating such an Animation would be, however.

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

Received on Thursday, 28 February 2019 03:43:52 UTC