Skip to content

[web-animations-2] Add Animation.started and Animation.playing promises #5871

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
kevinbrewster opened this issue Jan 13, 2021 · 8 comments
Labels
web-animations-2 Current Work

Comments

@kevinbrewster
Copy link

kevinbrewster commented Jan 13, 2021

Problem

  1. If you manually construct an Animation to be played at a later time, there's no way to know when it's actually playing.
  2. If the animation has a delay, there's no way to know when the animation effect actually starts.
let animation = new Animation(new KeyframeEffect(element, { opacity: 1 }, { delay: 1000, duration: 2000 }));

animation.ready.then(_ => {
    console.log("animation is ready"); // this is logged immediately
});
window.setTimeout(_ => {
	animation.play()
}, 1500);

// When will animation play?
// When will the delay period end?

Proposal

Introduce two new promises on Animation:

  1. Animation.playing: returns a Promise which resolves once the animation has started playing.
  2. Animation.started: returns a Promise which resolves once the delay period is over and the effect has started
let animation = new Animation(new KeyframeEffect(element, { opacity: 1 }, { delay: 1000, duration: 2000 }));

animation.ready.then(_ => {
    console.log("animation is ready"); // @ time = 0
});
animation.playing.then(_ => {
    console.log("animation is playing"); // @ time = 1500
});
animation.started.then(_ => {
    console.log("animation effect started"); // @ time = 2500
});
animation.finished.then(_ => {
    console.log("animation effect started"); // @ time = 4500
});

window.setTimeout(_ => {
	animation.play()
}, 1500);

Alternatives

  1. Animation.played instead of Animation.playing for consistency in tense with Animation.finished
  2. Animation.effectStarted instead of Animation.started for clarification
@birtles
Copy link
Contributor

birtles commented Jan 14, 2021

Thanks for filing this issue. I wonder if you could elaborate on the use cases for this?

I'm not sure that Promises are the right fit for this use case. The Writing Promise-Using Specifications document gives some guidance about when to use Promises vs events.

In this case a start event can happen many times because:

  • The playback rate can change back and forth meaning we enter the active interval from either end
  • The current time can be seeked back and forth
  • When we introduce nested effects, child effects can re-enter their active interval many times due to repetition or easing on an ancestor (perhaps even several thousand times per second in the case of a bounce easing effect).

So it would seem like events would be a better fit for this.

Regarding the third point above, it might also make sense to specify that such events only apply to the delay defined on the root effect, but then I believe authors will also want events on child effects and I don't know how we can fix that without introducing performance issues.

I believe we've seen similar proposals before so it might be worth searching for them and seeing what the concerns were then.

@birtles birtles changed the title Web Animations Proposal: Add Animation.started and Animation.playing promises [web-animations-1] Web Animations Proposal: Add Animation.started and Animation.playing promises Jan 14, 2021
@kevinbrewster
Copy link
Author

Related Proposals

  1. [web-animations-2] animation (or effect) start and iteration events #4461
  2. [scroll-animations] Should animation events fire every time active range is left / reentered? #4324

Motivation & Use Cases

Often, an author needs to synchronize arbitrary code with a web animation. For example, during a few key points in an animation lifecycle, one may need to manipulate the DOM, or initiate network requests, or conditionally trigger other animations, among other things. These key lifecycle points are:

  1. When the animation is finished
  2. When the animation is played
  3. When an animation effect begins (i.e. after duration has expired)
  4. When an animation effect iterates

There is precedent for the usefulness of such triggers in the existence of:

CSS Animation Events:

  • animationstart
  • animationend
  • animationiteration
  • animationcancel

Authors can already be informed when an animation is finished (case #1 above) via the Animation.finished promise but it's currently impossible to be informed about cases 2, 3, and 4

Promise vs Event

The unhandled cases 2, 3, and 4 from above can be separated into two groups:

Animation

"2. When the animation is played" could be handled in one of two ways:

  • Animation.played Promise
    --or--
  • animationplay event

I think there is an argument to be made that Animation.played is analogous to Animation.ready and, as such, it makes sense to use a Promise. I suppose this would fall under 2.3. More general state transitions in the Writing Promise-Using Specifications document.

Side note: even after reading the spec a few times and experimenting with the Animation.ready promise, I'm still unclear on it's use-case or why a Promise was used versus and event. Perhaps an exploration of that reasoning could help guide a decision for an Animation.played Promise.

AnimationEffect

In issue 4461, You wrote:

If we were to add iteration events they would most naturally become a property of the effects...Initially these events were not added because of performance concerns...since then we've taken a different approach to the requirements for dispatching events for CSS animations that limits events to 1~2 per animation frame. I think that would address some of the performance concerns we previously had

I agree and therefore propose that "3. When an animation effect begins" and "4. When an animation effect iterates" be handled by the following events:

AnimationEffectEvent

  • effectstart
    The effectstart event occurs at the start of the effect. If there is a delay then this event will fire once the delay period has expired.

  • effectiteration
    The effectiteration event occurs at the end of each iteration of an effect, except when an effectend event would fire at the same time.

  • effectend
    The effectend event occurs when the effect finishes.

An uneducated guess of what AnimationEffectEvent could look like:

interface AnimationEffectEvent : Event {
  readonly attribute CSSOMString animationEffectName;
  readonly attribute double elapsedTime;
};

This would require adding a new name attribute to AnimationEffect:

interface AnimationEffect {
    attribute CSSOMString name; /// <--- new
    EffectTiming          getTiming();
    ComputedEffectTiming  getComputedTiming();
    undefined             updateTiming(optional OptionalEffectTiming timing = {});
};

Next Steps

Would it be helpful if I created separate proposals for AnimationEffectEvent and/or Animation.played ?

@birtles birtles added the web-animations-2 Current Work label Jan 15, 2021
@birtles birtles changed the title [web-animations-1] Web Animations Proposal: Add Animation.started and Animation.playing promises [web-animations-2] Web Animations Proposal: Add Animation.started and Animation.playing promises Jan 15, 2021
@birtles birtles changed the title [web-animations-2] Web Animations Proposal: Add Animation.started and Animation.playing promises [web-animations-2] Add Animation.started and Animation.playing promises Jan 15, 2021
@birtles
Copy link
Contributor

birtles commented Jan 15, 2021

Related Proposals

  1. [web-animations-2] animation (or effect) start and iteration events #4461

  2. [scroll-animations] Should animation events fire every time active range is left / reentered? #4324

Thank you! That looks right.

Motivation & Use Cases

Often, an author needs to synchronize arbitrary code with a web animation. For example, during a few key points in an animation lifecycle, one may need to manipulate the DOM, or initiate network requests, or conditionally trigger other animations, among other things.

Thank you. I was hoping there might be more specific use cases but the precedent of CSS animation events is compelling enough that I think this makes sense.

"2. When the animation is played" could be handled in one of two ways:

  • Animation.played Promise
    --or--
  • animationplay event

I think there is an argument to be made that Animation.played is analogous to Animation.ready and, as such, it makes sense to use a Promise. I suppose this would fall under 2.3. More general state transitions in the Writing Promise-Using Specifications document.

Side note: even after reading the spec a few times and experimenting with the Animation.ready promise, I'm still unclear on it's use-case or why a Promise was used versus and event. Perhaps an exploration of that reasoning could help guide a decision for an Animation.played Promise.

The Animation.ready promise fits into the "One-and-done operations" category, i.e. asynchronous operations. Calling play() and pause() triggers an asynchronous operation to setup the playback / pausing by synchronizing with the state of animations running on other processes/threads. The ready Promise resolves when that async operation has completed.

Initially the expectation was that user agents might take different amounts of time to setup animations but in practice all user agents resolve the promise on the next animation frame unless the animation is already playing or unless it is associated with an inactive timeline.

As for use cases, it lets you know when it is safe to read the startTime (in the casing of playing) or currentTime (in the case of pausing), so that, for example, you can synchronize other animations with the newly played/paused animation.

I'm still not clear on the use cases for the played promise, however. When do you ever have an animation that you don't initially play() on, where the caller of play() is sufficiently distant from the observer of played state that you can't call it directly, and where events would not be ergonomical?

As I understand it, the examples for "More general state transitions" in that document refer to async operations that differ from this case. Also it seems like the warnings about over-using Promises apply to this case, at least as I understand it.

AnimationEffect

In issue #4461, You wrote:

If we were to add iteration events they would most naturally become a property of the effects...Initially these events were not added because of performance concerns...since then we've taken a different approach to the requirements for dispatching events for CSS animations that limits events to 1~2 per animation frame. I think that would address some of the performance concerns we previously had

Thanks, that's great. (I've moved this discussion to web-animations-2 in keeping with the discussion there.)

Yes, it does sound like we could resolve the performance issues I had in mind.

An uneducated guess of what AnimationEffectEvent could look like: [...]

That all seems reasonable.

This would require adding a new name attribute to AnimationEffect:

I wonder if that would be necessary if we make the target of the event the AnimationEffect itself? (Since only KeyframeEffect's have a (pseudo-)element target and even that is optional.)

Next Steps

Would it be helpful if I created separate proposals for AnimationEffectEvent and/or Animation.played ?

I'm personally still unclear about Animation.played but I can see AnimationEffectEvent as being worth specifying if you have time to work on it!

@kevinbrewster
Copy link
Author

kevinbrewster commented Jan 15, 2021

EDIT: I cleaned up a few things since yesterday

Thanks for taking the time to explain all that!

I'll create a separate proposal for AnimationEffectEvent next week and use this page to further discuss the potential merits of an Animation.played promise.

The Animation.ready promise fits into the "One-and-done operations" category, i.e. asynchronous operations.

Hmm. I interpreted this category as explicitly referring to methods that "return a promise", not properties, but perhaps that's a distinction without a difference.

Anyway, I think I found a better way to articulate what I want.

Better Explanation of Animation.played Promise

I want to be informed when these conditions happen (and I'm hoping both are exactly equivalent):

  • an animation goes from playState idle to either playState running or paused
  • an animation's currentTime becomes not null

In other words, I see three state categories:

  • idle
  • playing(Bool isRunning) (encompasses both running and paused playStates)
  • finished
	`played`  promise resolved
	  | |
	   V	

           |  ---- playing -----  |  
[idle]     | [running] / [paused] | [finished]
	   | -------------------  |
	   |    [effect]          |
	   |      [effect]        |
	   |             [effect] |

I want to know when the animation has entered this playing state category. This would be a one-time event (i.e. would not happen with every play() or pause().

By the way, I'm not sure played is even the best name for this, as it implies it's directly related to the play() method. Perhaps you can suggest a better name?

The only advantage of a promise over an event is so authors can still be informed if animation is auto-played.
In other words, if there was a play event instead of a promise, you could never be notified when using element.animate().

Consider these three examples where the console should log "played" each time.

let animation = new Animation(...);
animation.played.then( console.log("played") );
animation.play();
let animation = new Animation(...)
animation.play();
animation.played.then( console.log("played") )
let animation = element.animate()
animation.played.then( console.log("played") )

Obviously the third example is practically pointless, but I'm imagining a situation where animations are dynamically created, sometimes being played immediately and sometimes not, and we want a consistent way to be notified.

Next consider this example of a trivial "conditional sequence". In other words, when one animation finishes it may or may not trigger another animation. Imagine there is some timeline UI shown on the page for users to visualize animations on the timeline. Whenever an animation enters the playing state category, I want to append a DIV to the timeline UI element to represent that animation.

There's certainly work-arounds (e.g. lifting animation1.play() to a separate function that can handle the extra DOM work as well as trigger the animation) but these work-arounds quickly become cumbersome as the code grows in complexity.

let box = document.querySelector("#box");

let animation1 = new Animation(new KeyframeEffect(box, 
   [{ opacity: 1 }, { opacity: 0 }], 
   { duration: 300, delay: 1000 }
))
animation1.played.then(_ => {
   console.log("animation1 played")
   // todo: add some kind of visual element to the timelineUI
});
animation1.finished.then(_ => {
   console.log("animation1 finished") 
});


let animation2 = new Animation(new KeyframeEffect(box, 
   [{ transform: "translateX(0)" }, { transform: "translateX(100px)" }], 
   { duration: 600, delay: 100 }
))
animation2.played.then(_ => {
   console.log("animation2 played")
   // todo: add some kind of visual element to the timelineUI
});
animation2.finished.then(_ => {
   console.log("animation2 finished") 
});


let animation3 = new Animation(new KeyframeEffect(box, 
   [{ transform: "rotate(0)" }, { transform: "rotate(90deg)" }], 
   { duration: 800, delay: 400 }
))
animation3.played.then(_ => {
   console.log("animation3 played");
   // todo: add some kind of visual element to the timelineUI
});
animation3.finished.then(_ => {
   console.log("animation3 finished")

   if(Math.random() > 0.5) {
      animation1.play();
   } else {
      animation2.play();
   } 
});

// kick-off the first animation
animation3.play();

For a more concrete example, I'm currently creating a dynamic slideshow using animations. I'm creating all of my animations ahead of time and effectively doing a bunch of SequenceAnimations and GroupAnimatons but sometimes more complex (i.e. with conditions / different pathways for the sequence part). The spacebar and arrow keys trigger certain animation groups/sequences, and the exact sequence is not predetermined ahead of time.

It would greatly simplify my life to have custom code run whenever an animation is first played, so I can adjust the DOM as needed (or even just logging for debug purposes). Sometimes the actual effects have a significant delay so I don't want to synchronize with the effect - I'm strictly interested in when play() is actually called).

From a theoretical standpoint, the API allows Animation instances to be created ahead of time and remain in an idle state indefinitely. It stands to reason that the API should either auto-play every animation, or provide a mechanism to signal the transition from idle to running / paused.

Alternative

Alternatively, you could alter the spec to add a third condition for an animation to be considered ready:

An animation is ready at the first moment where all of the following conditions are true:

  • the user agent has completed any setup required to begin the playback of the animation’s associated effect including rendering the first frame of any keyframe effect.
  • the animation is associated with a timeline that is not inactive.
  • the animation is NOT in an idle state [i.e. play() or pause() has been called]. // << NEW

That would mean that the current ready promise would initially be unresolved.

In the below code, I think it's intuitive that the animation should not be considered "ready" until play() or pause() has been called and the animation is out of the idle state.

let animation = new Animation(new KeyframeEffect(box, 
   [{ opacity: 1 }, { opacity: 0 }], 
   { duration: 300, delay: 1000 }
))
animation.ready.then(_ => {
   console.log("ready"); // should NOT be called
});

@birtles
Copy link
Contributor

birtles commented Jan 18, 2021

Thank you for taking the time to explain this use case. I really appreciate it. I've read it through and I think I understand the scenario.

Firstly, I think you'd want to define that such a promise is replaced when cancel() is called. That method causes the currentTime to become null so I think that matches the usage you outlined.

That said, I'm still not sure that this pattern is common enough to warrant adding to the platform. Like you said, you could accomplish your particular case by wrapping the play() / pause() / animate() methods to add the desired behavior.

For the timeline UI case, I think it might be better to pursue a more generic animation mutation observer API. That's what Firefox uses internally for its animation DevTools and I'm told Chrome has something similar so if we could standardize the behavior of this interface we could potentially expose it to Web content. It's more work to specify, but likely to be useful to more applications.

Of course, if you can convince other parties that the played Promise is generally useful, I'm happy to see it added.

@kevinbrewster
Copy link
Author

Thanks for your response.

What are your thoughts on my "Alternative" section on my last comment?

i.e. what is the rationale for having the current ready promise be initially resolved? If the current ready promise was initially un-resolved, this would solve my problem as well.

@birtles
Copy link
Contributor

birtles commented Jan 19, 2021

What are your thoughts on my "Alternative" section on my last comment?

i.e. what is the rationale for having the current ready promise be initially resolved? If the current ready promise was initially un-resolved, this would solve my problem as well.

The ready promise represents the state of an asynchronous operation. When it resolves, it tells you that it is safe to read the currentTime (for pausing) and startTime (for playing) values. Having it be unresolved initially breaks that model. There is no operation pending and it is safe to read currentTime / startTime so I don't think it should be unresolved at that point. I'd also be a little concerned about changing that behavior now that it is shipping in all browsers.

@kevinbrewster
Copy link
Author

Right. I guess I'm still struggling to understand how a resolved promise can represent both the completion of an asynchronous operation and also represent no asynchronous operation having been run at all. Perhaps I should have said: current ready promise should initially be null, as no asynchronous operation has been run, so there is no state to represent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
web-animations-2 Current Work
Projects
None yet
Development

No branches or pull requests

2 participants