w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.38k stars 641 forks source link

[css-view-transitions-1] How should scroll timeline animations be treated? #9901

Open bokand opened 5 months ago

bokand commented 5 months ago

A view transition is removed when all constituent animations are no longer running or paused. However, the spec only defines this for document timelines:

  1. Let hasActiveAnimations be a boolean, initially false.
  2. For each element of transition’s transition root pseudo-element's inclusive descendants:
    1. For each animation whose timeline is a document timeline associated with document, and contains at least one associated effect whose effect target is element, set hasActiveAnimations to true if any of the following conditions is true:
      • animation’s play state is paused or running.
      • document’s pending animation event queue has any events associated with animation.

We should at least define what happens for other kinds of timelines.

However, as @bramus found it might be more useful to keep a view transition alive while it has a scroll timeline regardless of its current state, since it can be reversed by the user (unlike a document timeline). If we do that, I think authors can finish the view transition manually by calling cancel() on the transition animations? Perhaps we could make this more convenient via the ViewTransition object?

bramus commented 5 months ago

Thank you @bokand for filing this. Had this on my backlog to do :)

I did indeed find the need to prevent a View Transition from automatically finishing. This when using a non-monotonic timeline as the source of time progress that drives the animations – in my case a ScrollTimeline.

While you could say this auto-finish behavior is fine for ScrollTimeline in my specific demo – the VT should after all end at a certain point in time, it becomes more tricky when the timeline’s animation range is the full 100% or for things like a (currently spec-fictional) GestureTimeline.

Practically, continuing with GestureTimeline, I’m thinking of a situation where you want to drive a VT via a drag gesture over a dragdistance of 200px. When crossing the 200px boundary you don’t want the VT to finish immediately as the user – while still dragging – might change their mind and drag back to the 180px point. When doing so the same VT should then rewind a little bit, instead of creating a new one. Only on pointerup that VT should either snap back to its original state (when dragging back) or play to its end state (when dragging forward).

That to say: It depends on the type of timeline (ScrollTimeline, ViewTimeline, GestureTimeline, MediaPlaybackTimeline, …) and the use case.

So maybe this should be an opt-in, e.g. vtObject.preventAutoFinish() or document.startViewTransition({ callback, autofinish: false });?

(I also thought of maybe using vtObject.pause() instead of vtObject.preventAutoFinish() here but that doesn’t completely make sense here because you aren’t always pausing the VT’s animations)


I think authors can finish the view transition manually by calling cancel() on the transition animations?

What about vtObject.finish()? I choose finish() here because the vt would first play (forwards) and reach the finished state.


Along with vtObject.finish(), it would be nice here if there also were something like vtObject.revert() to have the animations play back to the start and then have the VT undo the DOM update. This for the situation where one starts dragging in one direction but then changes their mind and drags a little bit back in the opposite direction.

bokand commented 5 months ago

What about vtObject.finish()? I choose finish() here because the vt would first play (forwards) and reach the finished state.

Yeah, if we had some manual control on the vtObject that might make sense. I was considering the case where we just make it manual by default (in the case of a non-monotonic timeline). In that scenario, calling animation.finish wouldn't work since the VT no longer ends when the animations are finished; you'd have to use animation.cancel. But I suppose we could also add a vtObject.finish for that case as well...

Along with vtObject.finish(), it would be nice here if there also were something like vtObject.revert() to have the animations play back to the start and then have the VT undo the DOM update.

The hard part is undoing the DOM update - I don't think it's feasible to undo the update automatically; the author would have to pass in an "undo" callback. If this is common enough though it might be a nice ergonomic improvement (the author can already do all this themselves today)

bramus commented 5 months ago

the author would have to pass in an "undo" callback. If this is common enough though it might be a nice ergonomic improvement

This is effectively what I did in the gesture demo. When the VT has finished but it turned out to be a no-op (i.e. the animations reverted to the old state), I execute some logic to undo the DOM update.

khushalsagar commented 5 months ago

The idea of a VT not automatically finishing SGTM. We already had a use-case for this with rAF driven animations in https://github.com/w3c/csswg-drafts/issues/8132 and its trivial to implement. There's an alternate API suggestion there which does both: indicate that the transition shouldn't automatically finish and a promise to listen to for finishing it.

With the above, I'm assuming we wouldn't need to change anything about our logic for how "auto finish" works. Both conditions need to happen for a transition to finish:

  1. All animations with a monotonic animation timeline, which IIUC is limited to document timeline, have to finish.
  2. If there are any waitUntil promises, they have to settle.

I think 2) also helps with: "The hard part is undoing the DOM update". If the transition needs to be aborted, the author plays the animation in reverse. Now the DOM is only showing ::view-transition-old snapshots. The author updates the DOM and resolves the waitUntil promises. The DOM state should match the old snapshots being displayed so flip from VT pseudo-DOM to live old DOM is seamless.

flackr commented 1 month ago

The is current property in web-animations captures the concept of animations on non-monotonic timelines which may still be animating.

css-meeting-bot commented 1 month ago

The CSS Working Group just discussed [css-view-transitions-1] How should scroll timeline animations be treated?, and agreed to the following:

The full IRC log of that discussion <TabAtkins> bramus: When the animations contained in a VT all reach the finished state, the VT itself also reaches finished, and goes away
<TabAtkins> bramus: When implementing draggable VTs, or scroll-driven, there's a need to prevent that from happening.
<TabAtkins> bramus: If you touch the screen and drag the animation, you hit the bottom it reaches 100%, but if you drag back up you still want it to reverse.
<TabAtkins> bramus: So request is to prevent it from reaching that finished state
<khush> q+
<TabAtkins> bramus: Proposal is to add a method on the VT object that prevents it from finishing. Propose .preventAutoFinish()
<ydaniv> q+
<TabAtkins> bramus: But also a counterpart for allowAutoFinish(); if you lift your finger and it's finished you want to really finish it
<astearns> ack khush
<flackr> q+
<TabAtkins> khush: Two things to add. Similar cam efor scroll-driven, if you're doing something in a rAF loop the brwoser can't tell when you're done
<TabAtkins> khush: the other is the API option I added on the issue is a new api called waitUntil() which takes a promise
<TabAtkins> khush: lets you combine the browser figuring out when the animation is done and other things
<ydaniv> q-
<TabAtkins> khush: Also good because multiple parts can each call .waitUntil() and give their own promise, and it won't be done until they all settle
<astearns> ack flackr
<TabAtkins> flackr: for the animations case, WA has an .isCurrent concept for if the animation can continue to produce updates. we use that for .getAnimations() too, ,to return animations that are finished but can become active again
<TabAtkins> flackr: so VT might want to use this instead of the strict "finished" concept
<TabAtkins> flackr: but khushal pointed out ones driven entirey by the user. maybe do that with CustomEffect, called out in WA spec. A JS animation that's driven by a declarative aniamtion of some duration.
<bramus> On the side: demo of a (hacked together) Scroll-Driven View Transition: https://codepen.io/bramus/pen/BabRVLg
<TabAtkins> khush: I'm not sure about asking devs to use CustomEffect for something like this
<TabAtkins> khush: right now they work around it by setting up an infinite declarative animation, and just call abort when they're done
<TabAtkins> khush: there's a need for a pattern when they don't want or need anything declarative, doing it all in script, and just want an easy way to teell the browser when they're done
<TabAtkins> flackr: I think you could do the similar thing with an infinite duration customeffect, and you just cancel that when you're done animating
<bramus> q+
<TabAtkins> flackr: whether it's better is an open question, it just avoids the need for an additional api, which might be nice
<TabAtkins> khush: right now in the spec we ignore animations unless they use tehe doucment timeline. this was because it was hard to reason about the scroll timeline not finishing, etc
<TabAtkins> khush: now if the author overrides all animations with scroll timeline, is it possible for the browser to automatically identify when the animation is done?
<TabAtkins> flackr: no, they'd be considered to be permanently active
<astearns> ack bramus
<TabAtkins> khush: the new api was meant to address that too
<TabAtkins> bramus: wanted to mention animations using a non-document timeline
<emilio> q+
<TabAtkins> bramus: right now the behavior is if you drag outside the specified range, the VT gets zapped, i want to prevent that
<TabAtkins> flackr: .isCurrent "fixes" that, but it means we'd never be done with the VT
<TabAtkins> bramus: which isn't something you want since while tehe VT is running the snapshots are inert
<TabAtkins> khush: reading bramus's proposal, i don't mind the explicit finish call, but then you have to have in userland when all the animations are done
<TabAtkins> khush: the waitUntil() proposal lets each point hook in their own promises so userland doesn't have to manage things
<TabAtkins> bramus: so for a demo - i'd start on pointerdown, prevent it from autofinishing, then on pointerfinish unlock it again
<ydaniv> q+
<astearns> ack emilio
<TabAtkins> TabAtkins: You can just start a promise on pointerdown, resolve it on pointerup. then it's back under browse control
<TabAtkins> emilio: I think I'd prefer a prevent - the non-promise version, because building the promise on top seems more trivial. easy to leak with promises.
<TabAtkins> emilio: seems simpler that a bug is becuase someone didn't call .finish() versus tracking a never-resolved promise
<TabAtkins> emilio: but no strong objection either way. just feels like providing the lower-level api is a little nicer, a little less footgunny
<TabAtkins> flackr: devil's advocate, the one thing harder with the direct api is coordinating between libraries
<TabAtkins> flackr: one library might say it's done and end it
<TabAtkins> flackr: maybe not a strong reason, just pointing it out
<astearns> ack ydaniv
<TabAtkins> ydaniv: i'm concerned it might be risky to give any api and then everything can get stalled
<TabAtkins> ydaniv: what if we default to if you reach 100% plus a scrollend was fired. would that be a reasonable default?
<TabAtkins> bramus: I think that's specifically targeted to this one use-case for scrolling, there might be more.
<TabAtkins> bramus: I'm also coming around more to the promises, there's a lot of promises on the web.
<TabAtkins> khush: the demos we've seen involve taking raw touch events and mapping them to gestures without having a scroll container
<TabAtkins> astearns: for my clarity, the promise-based is an alternative to the bare prevent/allowFinnish() calls
<TabAtkins> astearns: is there a summary of the promise-based that could be added?
<bramus> s/allowFinnish/allowFinish
<khush> transition.waitUntil(promise) keeps transition from finishing until all promises passed to waitUntil settle
<TabAtkins> (can be called multiple times to add promises to the list)
<TabAtkins> astearns: do we want to resolve, or take it back to the issue?
<flackr> q+
<TabAtkins> bramus: can we bikeshed the name? waitUntil() can be a little confusing...
<TabAtkins> TabAtkins: waitUntil() is already used in Service Worker for exactly this purpose
<astearns> ack flackr
<TabAtkins> flackr: I was gonna have a minor complaint about adding complexity, but if there's precedent...
<TabAtkins> emilio: I'm okay resolving on it. I think I'd rather build the simpler thing first, but not a super stron gopinion either way.
<TabAtkins> astearns: If we do this, is there a possibility we'll add the lower-level one anyway
<TabAtkins> TabAtkins: They can be layered either way - both can be built on top of the other
<TabAtkins> astearns: so proposal is to add a .waitUntil(promise) that keeps the transition from settling until all of the promises settle.
<TabAtkins> RESOLVED: Add .waitUntil() promise to the VT object, prevents VT from finishing until the promises settle