-
Notifications
You must be signed in to change notification settings - Fork 719
[css-animations-2][web-animations-2] How should AnimationTrigger work? #12119
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
Comments
With option 1, can you elaborate on how animation.cancel() works? If I cancel before it's triggered? While it's playing? Does it stop the animation from triggering? If so, what is this state and how do I later say that I want it to be triggerable again? When does a trigger become active for a programmatically constructed animation? As soon as the trigger is assigned? For 1iii, element.animate should not immediately start playing the animation, it should only result in a trigger that is armed. For option 2i and 2ii, I think that pause() should just pause the animation in whatever state its in. If it hasn't triggered yet then it wouldn't trigger even if the condition is met until you call play. I'm leaning towards option 2 where the trigger being active is semi-synonymous with the animation being in a playing state, the trigger being an extension of the playing lifecycle of the animation (e.g. like being in the before phase) which feels easier to reason about. I imagine also that a trigger @ydaniv FYI |
Oh Great question. I think animation.cancel() should have the effects of removing the animation's visual effect and disabling the trigger. For ways to re-enable a trigger, it seems like we'd either need to:
Hmm, I suppose we could think about it this way but I would have thought a developer calling element.animate signals that they want it to start playing and that this was why, despite animate returning an animation (which a developer could then call play on), animate just directly calls play?
That sounds reasonable but what about code that could have been relying on creating a waapi animation and immediately calling pause in order to keep the animation paused at the initial keyframe before calling play at some other point in time? Would pause still cause the animation to take effect without enabling the trigger under your proposal? |
I suggest we discuss each issue on the dedicated issue thread so that we can track and resolve these properly. These questions are discussed in #11914 and I suggested a solution for
This SGTM, but then we may want to propose a change that WAAPI animations are created with a default trigger with
I think with the above suggestions we can reach that point where they actually do.
So far I proposed (also in the comment above), as @DavMila also suggested, that calling
In this case then, what is the base-case for a
I think you mean the animation stays relevant as we proposed in #11971. |
My concern with this approach is that in isolation it's easy to make decisions that don't fit a consistent higher level model. This is why I suggested we have a meta issue to agree on the fundamental model of how triggers fit into the animation ecosystem. |
It might be helpful to agree on a set of expectations (prioritized in case some of them are not self consistent) and work through the details of how well each model explains them / requires fewer special cases. E.g. here's some ideas to start: Of course, existing animations should work as they do today, i.e.
Setting up an animation with a trigger that is not yet active (either via CSS* or element.animate* or the animation constructor and calling play) should result in:
* CSS animations and Element.animate implicitly "play" their animations If you create an idle animation with a trigger but don't play it (e.g. Animation constructor), just like with regular animations, it will:
If you call pause on an animation:
If you explicitly call play on an animation (this one may be the crux of the differences), but I argue that it should be consistent with implicit play (from CSS or element.animate):
If you call cancel() on an animation
If you assign a trigger to a finished animation
|
Thanks @flackr. The expectations sound mostly reasonable to me. I definitely agree with trying to make Web Animations and CSS animations behave consistently at a basic level. Since we're talking about the high-level architecture, I think it makes sense that triggers operate at the playback level and should ideally not need to touch the animation effect timing. However, taking this a step further, I wonder if it is possible to have triggers be entirely independent of animations such that they exist as external mechanisms that simply call If we want to distinguish between certain cases where the to-be-triggered animation fills backwards or not (e.g. distinguishing between |
Agreed, I've been thinking of a trigger as being similar to an animation whose start time (which is not part of the effect timing) is in the future.
I understand the rationale behind this model, and I think this represents the current direction (option 1) of how animation trigger works. I think we'll have to carefully work through the expectations to avoid any surprises though, as currently we have a very confusing state around cancelation. To this end, it seems like we'd need to update the procedure to play an animation such that if the animation is currently idle and if it has a trigger, it pauses the animation (i.e. "arms" the trigger, though see my later response). Then a second play could start the animation right away if we think this makes sense. I think though that where this gets confusing is what happens when you call pause()? Does this not have any effect if the trigger hasn't happened yet and leave the trigger able to play the animation? Does pausing an animation that has started playing due to the trigger leave it in a state where it will continue playing if the trigger condition happens again?
Calling pause() would result in an active effect even if it was not |
This matches how I had been thinking about it and I think it might be possible with a proposal I've just uploaded. Regarding some of the expectations put forward by @flackr Certainly, existing animations should work as before and I think gating AnimationTrigger behind
It seems to me that the expectation that
On creating but not playing an animation, I think if you don’t play but you call |
Thanks. I had a read and I think that's pretty close to what I was imagining. However, I wonder if
I don't follow why that clarification is needed. If |
I am currently imagining that Animations and AnimationTriggers are not 1-to-1. I think we want to be able to set
In my proposal, when we call Edit: It occurred to me that under my proposal, an |
The way I see AnimationTriggers (and how they're currently speced) is a separate object, external to animations, that you attach onto Animations, and it uses the existing Animations' procedures that control playback (playing/reversing/pausing). They don't set anything on the animations, they just trigger the same procedures triggered when I think we have agreement on how they should work with existing features, as described above. I think we also agree that AnimationTriggers' domain is effects on Animations' playback. In regard to @DavMila's proposal, I don't think we should be adding imperative API to triggers. IMHO this over complicates the API. But this proposal does build on some features of Triggers that I think we can agree on:
Since I envisioned Triggers as a separate entity, it seems weird to me that calling But the main question boils down to whether the playback methods produce the same effects as they currently do? Or do they delegate it to the triggers? I think we also have an agreement on the following behaviors:
The questions that remain open are the following behaviors:
I think after we agree on expected behavior we can move forward to clarify the technical details. |
This comment has been minimized.
This comment has been minimized.
The CSS Working Group just discussed The full IRC log of that discussion |
Another use case to consider is developer tools where you have a button that pauses all animations and resumes them (as chrome dev tools has). I would expect that if I do the following: const paused = document.getAnimations().filter(anim => anim.playState == 'playing');
paused.forEach(anim.pause()); This would pause all of the animations in their current state, and then if I later call: paused.forEach(anim => anim.play()); I'd expect it to restore the animations to the state they were in prior to pausing. With play arming the trigger, I think this code just works, however if it's a separate API to arm the triggers then developers will have to carefully handle that in some way. |
Thanks for bringing this up! This might be the key difference between the models boiling down to a question: Is it more surprising that after I have set up a trigger, when I explicitly call I'm inclined to think that because Also, there might be a bit of a parallel to the relationship between Overall, it seems to me like a bit of a severe limitation that an author cannot play an animation because they have set up a trigger whose condition has not yet been met? |
Though to be clear, even though it wouldn't progress through the animation, the call to play arming the trigger would also change the animation play state, result in the |
Yes, I think we're on the same page here. These 2 expectations I listed above as things we agree on:
I'm fine with the "arming the trigger" approach, as long as we agree on behavior and APIs.
I agree that having Regarding constructing an Animation with a trigger using WAAPI, since the animation starts in |
I've gone through the AnimationTrigger doc and with the help of some explanations from @DavMila I think it makes sense. However, I'm pretty sure I'm still missing some important understanding of how triggers work. I wonder if anyone would be kind enough to explain to me what wouldn't work in the following very simplistic adaptation of the proposal:
Sorry for the delay and for where I've surely overlooked important behaviors. I'm just trying to wrap my head around this and see if there's a simpler, less-coupled approach that lets us avoid adding more boolean state parameters to the already-complicated Web Animations model. |
I think all your suggestions are good and I'd be happy with moving forward this way. I'll add a few details.
Yes. I believe this works just fine conceptually for AnimationTriggers. The slight difference with the way that PR is currently written is that AnimationTrigger will want to be in the before phase for a forward-playing animation when the local time is at the before-active boundary time. But I guess those are just details and using a/this flag is conceptually fine... just thought I'd mention the detail in case there is anything about that that is unexpected (which I doubt).
This seems good to me. We can track any relevant state within the AnimationTrigger rather than the Animation. In line with your suggestions in the doc, we can scrap Animation.trigger and just have AnimationTrigger.arm(Animation) and the AnimationTrigger will be aware of which animations its associated with.
This seems good to me.
Agreed. I've realized we can move all state tracking to the trigger. I'm okay with not even having that convenience. At the very least, if it's something that authors think they'd find useful and want, we can always add it. Maybe we can also think about an AnimationTrigger.getAnimations() API?.. and perhaps a document.getAnimationTriggers() API?
Sounds good to me. With option 1 we really don't need a default trigger for explicitly constructed Animations. However I think CSS animations should still get triggers by default. Like we're currently trying to specify, animation-trigger defaults to a trigger that would play the animation immediately - so no change to existing animations - and custom
Yes. Further, explicit calls to
No worries, your input has been very helpful! I think we're converging on what the API should look like. |
Yup, this sounds good and matches my thinking as well, to add at a spec level a start point exclusive paused state.
Scroll animations are endpoint inclusive in a running state, however, if we make this endpoint state modification apply while running as well then we could use this.
In this sort of a model I would just call it add/remove. This could either be AnimationTrigger.add/removeAnimation (similar to Element.add/removeEventListener) or AnimationTrigger.animations.add/remove (similar to Element.classList. We should also consider whether we really need to have multiple animations per trigger. If instead we had an animation attribute or setter on the trigger we could say that setting it to a different animation disassociates the trigger from the original animation and might simplify some of the state management. If developers want to use the same trigger for multiple animations there just needs to be an easy way to construct a trigger with the same options as an existing trigger. |
I'm generally fine with the above direction, with the following questions/reservations:
|
The doc Brian's last comment referred to is bit.ly/animation-trigger. I've modified it (leaving the previous stuff crossed out) to reflect the simpler model and API we're currently thinking about. |
Replying to @DavMila's comment:
Awesome. Yes, that's a good detail to be aware of.
Yes, I think those approaches would work too.
From what I understand that seems fine. I think it would be mostly unobservable too unless we expose
I was thinking about this and I actually wonder if it's even necessary. For example, would it be so bad if
Thank you so much for your understanding! |
Replying to @flackr's comment:
Good point. This will take a little investigation. From what I understand:
We'll have to think about the cleanest way to cover those cases.
Yes, that sounds better.
Interesting. If that's the case, then I suppose you wouldn't even need |
Replying to @ydaniv's comment:
Good point. I think you have a much better understanding of the ergonomics and use cases of this feature than me.
I agree.
This is a really good point. I was thinking about this after making my proposal and it got me thinking: why do playback methods (including If the trigger is acting at an arm's length and simply invoking My original concern about I'm not sure but I think having less magic and less coupling might be preferable.
I'm not sure that |
I still think cancel should result in an animation no longer being triggered. When we add an animation to a trigger it sits in a paused state waiting for the trigger, and that explains why it is "relevant" and returned by getAnimations. When you cancel, the animation would no longer be paused right? It would be strange to have two states that a triggered animation could be in. |
Yeah, we've been going about this back and forth, and I see that we still have different opinions about it. I suppose after we moved the state from the animation over to the trigger probably solved most of the issues we had around state. So perhaps now both options are plausible.
I also agree that Regarding @birtles' suggestion:
How should that actually work in the following scenarios:
It seems to me like both cases above force us to make this behavior stateful again.
So far defaulting to a trigger with the |
Right, it would be in the idle state. As a result, there would be two different states: "paused by trigger" and "idle with a trigger". From an author's point of view, the only way to get back from "idle with a trigger" to "paused by a trigger" would be to re-associate with the trigger. So I think that's a pretty good argument for having I think that's a bit unfortunate since it introduces hidden coupling between animations and triggers but perhaps in future it could be explained by having animations emit events and letting triggers listen to them (we've discussed animation mutation observers in other issues before).
It would do whatever primary triggering is defined to do. If triggering calls
Again, it simply does whatever an inverse trigger is defined to do. So if that calls
I'm afraid I'm not following why we'd need to make the behavior stateful. Could you elaborate?
Sorry for being dense here, but I'm not quite following this part. Could you elaborate on what "worked well" means here? And how the "none" and "auto" values play into this? Thank you! |
Yes, it would be nice to have a way to have a way to revert that state back to the initial paused state.
I agree it's not optimal, but I think otherwise we'll need something like
That sounds fine. What if both triggers are of type
Currently
So exactly the options I explained above. For example, if you want 2
No worries, by "worked well" I meant matching the value of |
+1 to having For other playback APIs, I don't have a particularly strong sense of what the right thing to do is, tbh. I like the idea that At a (somewhat hand-wavy) higher level, I've been thinking that we are giving authors 2 "modes" of controlling their animations' playback. "Manual" mode in which they need to invoke the playback APIs explicitly and "autopilot" mode in which they set up the trigger conditions and let that take care of controlling the animations' playback. And if, for whatever reason, the author invokes one of the playback APIs, they're "switching into manual mode" (like taking the wheel of a self-driving car :) ) and the trigger(s) will relinquish control of playback. I wondered whether it would help with making a choice here to think about which choice would be less disruptive to revert but it feels to me like the remedy for either choice is the same. If we auto-disassociate now, we could have some optional param, e.g. I think we've resolved the main question at the heart of this issue. I had filed 11914 earlier about the question of playback APIs disabling/disassociating triggers, So perhaps we could discuss it there?
+1. I had a similar feeling that giving CSS animations a trigger by default makes for a cleaner story of how they are played. A bit of trifling case perhaps but if an author specified |
I've update my proposal to reflect the current thinking. The two lingering questions seem to be:
I think that the main components in the proposal will stay pretty much the same regardless of how we answer those questions. If we think the proposal is a good representation of our current position, I'd like to put this issue back on the agenda so the working group can give their go/no-go. thanks! |
The discussion about how AnimationTrigger should work is ongoing. This comment[1] captures the main points about how triggers should work. The hope is to have a simple implementation which we can help guide what the edge cases are, which ones we care about, and what to do about the ones we care about. A few points about the behavior worth highlighting are: 1. We automatically call addAnimation for CSS animations. 2. When animation-play-state is "paused" and isn't being ignored, the trigger should not play the animation. 3. Trigger-related state tracking is done only in the AnimationTrigger. However, we keep Animation::AnimationTriggerData::css_play_state just so the trigger can know not to play animations for which `animation-play-state` is paused. A few notes about some other changes made in this patch. 1. inspector-protocol/tracing/animation-expected.txt is adjusted to expect "paused" instead of running because when we call addAnimation on the CSS animation, we'll pause it and Pause will be the first function to supply data to the inspector_animation_agent via NotifyProbe. 2. ElementAnimations::SetCompositedClipPathStatus is modified in a way that reflects the intention that kNotComposited and kNoAnimation clear clip_path_paint_worklet_candidate_, even if either of those was already recorded as the composited_clip_path_status_. The test case in clip_path_paint_definition_test.cc revealed (by unconditionally calling pause when the trigger is enabled) that ElementAnimations::RecalcCompositedStatus might set clip_path_paint_worklet_candidate_ and try to set the status to kNotComposited and kNoAnimation, but, because there is no change in the status, fail to clear clip_path_paint_worklet_candidate_. [1] w3c/csswg-drafts#12119 (comment) Bug: 390314945 Change-Id: I5bd41e2edbf874ce8e2f5662709f31143be8f1c5 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6605724 Reviewed-by: Kevin EllisCommit-Queue: David Awogbemila Cr-Commit-Position: refs/heads/main@{#1469239}
The discussion about how AnimationTrigger should work is ongoing. This comment[1] captures the main points about how triggers should work. The hope is to have a simple implementation which we can help guide what the edge cases are, which ones we care about, and what to do about the ones we care about. A few points about the behavior worth highlighting are: 1. We automatically call addAnimation for CSS animations. 2. When animation-play-state is "paused" and isn't being ignored, the trigger should not play the animation. 3. Trigger-related state tracking is done only in the AnimationTrigger. However, we keep Animation::AnimationTriggerData::css_play_state just so the trigger can know not to play animations for which `animation-play-state` is paused. A few notes about some other changes made in this patch. 1. inspector-protocol/tracing/animation-expected.txt is adjusted to expect "paused" instead of running because when we call addAnimation on the CSS animation, we'll pause it and Pause will be the first function to supply data to the inspector_animation_agent via NotifyProbe. 2. ElementAnimations::SetCompositedClipPathStatus is modified in a way that reflects the intention that kNotComposited and kNoAnimation clear clip_path_paint_worklet_candidate_, even if either of those was already recorded as the composited_clip_path_status_. The test case in clip_path_paint_definition_test.cc revealed (by unconditionally calling pause when the trigger is enabled) that ElementAnimations::RecalcCompositedStatus might set clip_path_paint_worklet_candidate_ and try to set the status to kNotComposited and kNoAnimation, but, because there is no change in the status, fail to clear clip_path_paint_worklet_candidate_. [1] w3c/csswg-drafts#12119 (comment) Bug: 390314945 Change-Id: I5bd41e2edbf874ce8e2f5662709f31143be8f1c5 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6605724 Reviewed-by: Kevin EllisCommit-Queue: David Awogbemila Cr-Commit-Position: refs/heads/main@{#1469239}
The discussion about how AnimationTrigger should work is ongoing. This comment[1] captures the main points about how triggers should work. The hope is to have a simple implementation which we can help guide what the edge cases are, which ones we care about, and what to do about the ones we care about. A few points about the behavior worth highlighting are: 1. We automatically call addAnimation for CSS animations. 2. When animation-play-state is "paused" and isn't being ignored, the trigger should not play the animation. 3. Trigger-related state tracking is done only in the AnimationTrigger. However, we keep Animation::AnimationTriggerData::css_play_state just so the trigger can know not to play animations for which `animation-play-state` is paused. A few notes about some other changes made in this patch. 1. inspector-protocol/tracing/animation-expected.txt is adjusted to expect "paused" instead of running because when we call addAnimation on the CSS animation, we'll pause it and Pause will be the first function to supply data to the inspector_animation_agent via NotifyProbe. 2. ElementAnimations::SetCompositedClipPathStatus is modified in a way that reflects the intention that kNotComposited and kNoAnimation clear clip_path_paint_worklet_candidate_, even if either of those was already recorded as the composited_clip_path_status_. The test case in clip_path_paint_definition_test.cc revealed (by unconditionally calling pause when the trigger is enabled) that ElementAnimations::RecalcCompositedStatus might set clip_path_paint_worklet_candidate_ and try to set the status to kNotComposited and kNoAnimation, but, because there is no change in the status, fail to clear clip_path_paint_worklet_candidate_. [1] w3c/csswg-drafts#12119 (comment) Bug: 390314945 Change-Id: I5bd41e2edbf874ce8e2f5662709f31143be8f1c5 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6605724 Reviewed-by: Kevin EllisCommit-Queue: David Awogbemila Cr-Commit-Position: refs/heads/main@{#1469239}
We have an experimental implementation of the behavior described in the proposal currently in Chrome Canary. It can be enabled by navigating to "chrome://flags/#enable-experimental-web-platform-features" and switching the setting to "Enabled". I've uploaded a demo with an example where you can interact with the animation by scrolling as well as by clicking buttons that invoke the playback APIs. It's still not clear to me how we should answer issue 11914 but I think with the agreement we have, we can put this issue on the agenda and get a resolution on the initial question in my first comment above: Should animation triggers function by invoking existing playback methods ( We could also resolve on adding the AnimationTrigger methods described in the proposal: |
To support scroll triggered animations,
animation-trigger
was proposed in #8942 and included in the css-animations-2 and web-animations-2 specs. I’m filing this umbrella meta issue so the CSS working group can get familiar with the drafted API and share thoughts that might help answer questions in issues (11914, 11915, 11910, 11918, 11932, 12064) related to this feature.To summarize,
animation-trigger
is a shorthand for some new animation triggering behavior usable both as CSS properties and Web Animation properties:animation-trigger-type
: whose values can beonce
: The default value; plays animations only the first time its trigger condition is met. [Demo]repeat
: Plays and resets animations each time its trigger conditions are met. [Demo]alternate
Plays and reverses animations each time its trigger conditions are met. [Demo]state
Plays and pauses animations each time its trigger conditions are met. [Demo]animation-trigger-timeline
: the timeline in which the trigger’s conditions are evaluatedanimation-trigger-range
: the range of the timeline in which the trigger’s play condition is met,animation-trigger-exit-range
: the range of the timeline in which the trigger’s reset/reverse/pause (depending on theanimation-trigger-type
) condition is met.All of the
animation-trigger
properties can also be specified to the web animations API (WAAPI) using theAnimationTrigger
interface.A scroll-triggered animation is one whose owning element’s trigger’s timeline is a ScrollTimeline or a ViewTimeline.
A quick note about the possibility of “time triggered animations” in the future:
The default
animation-trigger-timeline
is thedocument.timeline
. This allows the behavior of pre-animation-trigger
animations to be explained in the animation-trigger world - they are immediately “triggered” as time boundaries are not currently accepted as range values. The*-range
properties leave the door open for “time triggered animations” to be supported in the future by allowing the ranges to be specified in terms of time, e.g. “5s”, rather than scroll position.One thing we need to specify is exactly what it means for an animation to be triggered, i.e. how do triggers interact with Animation APIs like
play()
,pause()
andcancel()
which control playback of anAnimation
.I see two ways of thinking about this (the second of which @flackr pointed out to me):
play()
as a programmatic trigger. In this way,play()
is semantically the same as before, immediately causing an animation to start advancing. Triggers, OTOH, function by effectively implicitly invokingplay()
(and other APIs). A few questions occur to me about this:play()
when it is first generated. I imagine that if a developer sets up a trigger, they would not want the animation to run until its trigger condition is met so we would not want to callplay()
. We can address this by changing that portion of the CSS animations spec to not immediatelyplay()
but instead wait for its trigger. However, a developer will probably want theiranimation-fill-mode: both | backwards
animation to be visually in effect at the first keyframe while waiting for the trigger (In the demos above, e.g. alternate, the elements start animating when fully scrolled into view, but when only partially in view, they are kept at the initial keyframe). This is different from the behavior of a WAAPI animation which has been declared but not triggered/play()
ed. If you simply declare a WAAPIAnimation
, it will be idle and have no effect untilplay()
is called, regardless ofanimation-fill-mode
(demo). I think this should continue to be the case withAnimationTrigger
, i.e. declaringanim = new Animation(...); anim.trigger = new AnimationTrigger(...);
should leave the animation in an idle state with no visual effect until eitherplay()
is called or its trigger condition is met. So the question in this case is how a CSS animation withanimation-fill-mode: both | backwards
will be in effect having been neither triggered nor played. I suppose it might be okay for CSS animations to behave a bit differently from WAAPI animations in this regard.play()
(or other APIs), should triggers still function? I propose “yes” in 11914 but can see how “no” makes sense too.play()
arms a trigger. This moves the job of advancing an animation away fromplay()
and gives it to triggers.play()
simply arms/enables the trigger and when the trigger’s condition is met, the animation begins to advance. This wouldn’t require the above adjustment for CSS animations with triggers as we could simply say that afterplay()
has been called theanimation-fill-mode: both
animation can be in effect but frozen before its first keyframe. Only when its trigger condition is met will it begin to advance beyond the first keyframe. So CSS and WAAPI animations have the same behavior in this regard. However, a few things that will need to be figured out are:pause()
does. Currently, an idle animation will begin to have visual effect ifpause()
is called (though remaining paused). If we preserve this behavior, how shouldpause()
on an idle animation affect a trigger? I think we should preserve this behavior and I thinkpause()
should also be considered to arm the trigger, but leave it in a state where the animation isn’t advancing.play()
andpause()
still directly interact with the playback of an animation? Say an animation isplay()
ed and triggered and is advancing. I’d think a developer would expect that invokingpause()
would still pause the animation. And so I think they’d also expect that callingplay()
would cause the paused animation to resume playing. So it would seemplay()
can’t be thought of as strictly interacting with the trigger but will need to retain the ability to directly control playback.animation.trigger = null
, should callingplay()
do nothing or throw an error or something else? If we don't throw an error, should a trigger added later advance the animation? I'd think so.Part of my hope is that getting ideas about the above questions will inform how specific animation-related concepts, e.g.
playState
,currentTime
, animation events, animation ready promise, account for animation triggers.Some examples of related questions are:
playState
beidle
?paused
?running
? A new state? (keeping in mind that it could be visually in effect if itsanimation-fill-mode
allows)currentTime
be zero?null
?animation-play-state
)?The text was updated successfully, but these errors were encountered: