Open kevinbrewster opened 3 years ago
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:
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.
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:
There is precedent for the usefulness of such triggers in the existence of:
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
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
eventI 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 = {});
};
Would it be helpful if I created separate proposals for AnimationEffectEvent
and/or Animation.played
?
Related Proposals
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
eventI think there is an argument to be made that
Animation.played
is analogous toAnimation.ready
and, as such, it makes sense to use a Promise. I suppose this would fall under2.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 anAnimation.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 toAnimationEffect
:
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/orAnimation.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!
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.
Animation.played
PromiseI want to be informed when these conditions happen (and I'm hoping both are exactly equivalent):
idle
to either playState running
or paused
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
.
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
});
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.
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.
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.
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.
Problem
Animation
to be played at a later time, there's no way to know when it's actually playing.delay
, there's no way to know when the animation effect actually starts.Proposal
Introduce two new promises on
Animation
:Animation.playing
: returns aPromise
which resolves once the animation has started playing.Animation.started
: returns aPromise
which resolves once thedelay
period is over and the effect has startedAlternatives
Animation.played
instead ofAnimation.playing
for consistency in tense withAnimation.finished
Animation.effectStarted
instead ofAnimation.started
for clarification