w3c / csswg-drafts

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

[css-view-transitions-2] Ignore offscreen elements from participating in transitions #8282

Open khushalsagar opened 1 year ago

khushalsagar commented 1 year ago

Currently if an element has a non-none computed value for view-transition-name, it participates in the transition irrespective of whether it is in the visible viewport. This means the element will be rendered, which has significant computational and memory overhead, even if it is never seen by the user. If the developer wants to avoid this overhead, they have to keep track of the visibility of each element and only add view-transition-name to the onscreen ones.

The proposal to make this case easier is as follows:

See prior discussion on this here.

jakearchibald commented 1 year ago

There are memory and computational benefits to this feature, but I originally proposed this as more of a visual / developer experience feature.

If two pages have a common heading, you may want that to be static in a transition, so you give it a page-transition-name.

However, if one of the pages is scrolled 8000px, the transition between the two will be bad, as the header will fly in from 8000px away.

With this feature, developers will be able to create a special incoming/outgoing animation for the header in this case (using the :only-child selector).

jakearchibald commented 1 year ago
  • The property has 2 values: auto and absent. auto indicates that the UA should render the element irrespective of its viewport position (as-if its onscreen). Would be nice to allow flexibility to the UA to optimize out such elements in case the transition is on memory constrained devices. That's why "should" instead of "must".

Since it significantly changes the animation, I'm not sure we can, or should, do this automatically. Since we won't be doing it in the first release, and it will only happen on constrained devices, I think it'll lead to things appearing broken when this 'auto' behaviour kicks in. I'd rather say that transitions can be skipped if the device is constrained, since that's a more reliable fallback that developers will already be catering for.

That said, I support the default value being auto, since elements are sometimes ignored due to content-visibility https://github.com/w3c/csswg-drafts/issues/7874

vmpstr commented 1 year ago

It feels like this is one of these options that you should give to startViewTransition to optimize things because you know that you've marked way too much with the view-transition-name. Although I do see the appeal of the new css property since you can use that in MPA cases too, to me it doesn't really feel like the right abstraction

jakearchibald commented 1 year ago

I like it as a CSS property since the instruction can be per transition item. Eg, if you're reordering a list, you still want things to transition from outside the viewport. Whereas you might not want that behaviour for a header. Both might happen in the same transition.

khushalsagar commented 1 year ago

A third value which would be useful for a case like https://deploy-preview-32--infrequently.netlify.app/, the page has a massive element which will be captured in entirety while the animation doesn't need that.

Syntax ideas, view-transition-offscreen: clip allows the UA to snapshot an intersection of the element's ink overflow rectangle with the visible viewport or snapshot root. Potential add-on is view-transition-offscreen: clip inset(10px), second argument allows specifying an explicit skirt by which the snapshot should be expanded in either direction.

jakearchibald commented 1 year ago

Isn't the clipping just supposed to happen automatically? https://drafts.csswg.org/css-view-transitions-1/#compute-the-interest-rectangle-algorithm

khushalsagar commented 1 year ago

Isn't the clipping just supposed to happen automatically? https://drafts.csswg.org/css-view-transitions-1/#compute-the-interest-rectangle-algorithm

That automatic clipping is very conservative, only if we must because of constraints like max texture size. Technically an implementation doesn't need to be constrained by it (you could create a tiled image), but the option gives UA flexibility.

This property would be an explicit hint from the developer that only the onscreen content (or a skirt around it) of this DOM element will be animated during the transition. The UA can use this knowledge to aggressively optimize for memory by painting and snapshotting a subset of the DOM element.

bramus commented 9 months ago

On a hackathon I coached at this was a dealbreaker for some, as they saw performance get tanked on non-highend devices. They were animating 1 element out of a list of 50, of which only 7 of them where visible in the viewport. Having a way to easily exclude these offscreen elements would surely be beneficial here.

jakearchibald commented 9 months ago

Are they happy with some items being :only-child, and animating as such, even though they existed in both states.

I wonder if we need some other feature in that case, where the group still animates from old to new, but there's only a new in the pair, or the group is dropped if the final position of the group is still out of view.

khushalsagar commented 9 months ago

I wonder if we need some other feature in that case, where the group still animates from old to new, but there's only a new in the pair, or the group is dropped if the final position of the group is still out of view.

@jakearchibald we have a couple of other issues with the characteristics you mentioned:

We haven't dug into the exact API shape but given how related these 3 features are, I feel like we should tackle them together. Like a new CSS property to specify one of these modes?

noamr commented 8 months ago

I like the direction this conversation was going. The main thing that justifies a new attribute for this is the idea that this could be a UX choice rather than a mere optimization (e.g. preventing a header from jumping).

I think the semantics here should be similar to content-visibility and intersection observers, however because this is observable and not just an optimization, the definitions need to be exact and customizable via a margin (same as rootMargin in IntersectionObserver).

Another thing I think we should do is make this new attribute inherited, this way the author can decide that a container makes its entire set of descendants behave in a certain way (and this can be overridden further down the tree).

Perhaps this can be view-transition-visibility or view-transition-overflow with:

khushalsagar commented 8 months ago

+1 to making the property inherited. I'm assuming the initial value will be visible.

For the auto/clip case, is there any use-case to let developers expand the snapshot viewport by some margin. So any elements/area within that is considered visible. This can also be a future extension with the current capability limited to clipping at snapshot viewport boundary.

noamr commented 8 months ago

A few additional comments:

khushalsagar commented 8 months ago

I wouldn't do anything regarding the ink overflow. It shouldn't be web observable... so I think the intersection should work according to the IntersectionObserver rules and disregard ink-overflow.

Hmmm, I think there will be cases where disregarding the ink overflow would end up with glitchy behaviour. Like a widget whose shadow is in the viewport. Should we really ignore it? This has come up for IO in the past too: https://github.com/w3c/csswg-drafts/issues/8649. @szager-chromium on that.

I was curious how content-visibility handles this. Looks like it relies on overflow-clip-margin (which is developer provides) and uses that here. Even if the overflow clip edge is further than the actual ink or scroll overflow, we rely on the specified edge.

With VT we intentionally decided not to have paint containment, so that won't work for us.

Not sure if clip should be something a different attribute. When the whole content is clipped-out, do you want to treat it as an empty image or not have it at all?

Oh I just read what you said carefully and I tend to agree. We should have separate properties which decide whether the element participates in the transition. If it is participating, then a separate property has knobs for deciding how its captured. So omitting clip sounds good.

In that regard, maybe the use-case in https://github.com/w3c/csswg-drafts/issues/9354 should eventually be handled by view-transition-visibility.

jakearchibald commented 8 months ago

We should have separate properties which decide whether the element participates in the transition. If it is participating, then a separate property has knobs for deciding how its captured. So omitting clip sounds good.

Absolutely agree.

jakearchibald commented 8 months ago

I think there will be cases where disregarding the ink overflow would end up with glitchy behaviour.

Also agree. I think ink-overflow intersection with the viewport counts as visible in terms of a view transition, else you risk 'seeing double'.

vmpstr commented 8 months ago

I was curious how content-visibility handles this.

Yeah, content-visibility uses paint containment for this reason, so that we don't need to render the subtree to figure out the extent of the overflow, all of the needed information is on the box itself (border box + overflow-clip-margin)

khushalsagar commented 7 months ago

One of the open questions for this feature is defining when an element is considered visible. There's 2 aspects to it:

jakearchibald commented 7 months ago

I'm pretty certain we don't need to care about occlusion. Just viewport intersection.

noamr commented 7 months ago

One of the open questions for this feature is defining when an element is considered visible. There's 2 aspects to it:

  • Intersection with the viewport (snapshot root from VT perspective). Ideally we'd use the element's ink overflow rect for it. IntersectionObserver already uses ink overflow rect to detect occlusion here. And #8649 is in progress to make the ink overflow bounds interoperable.

  • Occlusion by other elements on the page. I haven't seen a use-case where a named element was occluded so I don't know if we need to consider it. Occlusion calculations are also more expensive so better if we can avoid it.

In either case I think we should be consistent with IntersectionObserver. For viewport intersection IntersectionObserver doesn't take the ink overflow into account, so this shouldn't either. If IO exposed ink overflow in some way, eg for occlusion, we could consider doing the same.

Note that with a big enough root margin, being accurate about the ink overflow becomes less important.

jakearchibald commented 7 months ago

hmm, I think the developer intent of this feature trumps consistency with intersection observer here. We can avoid using the word 'intersection' if that's where the problem is.

I'll ask Shopify folks though.

noamr commented 7 months ago

hmm, I think the developer intent of this feature trumps consistency with intersection observer here.

How is the developer intent here different from the developer intent in IntersectionObserver? I think consistency is important here, having several APIs that rely on viewport-intersection and work in a slightly different manner would create confusing UX bugs and confusion. If we want to expose ink-overflow, we should do that in IntersectionObserver as well somehow.

noamr commented 7 months ago

I think the underlying question here is how we would expect elements to behave that are right outside the edge of the viewport. If this was just about performance, we could make some educated guess and tweak it. But this about excluding elements that are outside the viewport from participating.

So let's say you have an element that has a top: 100vh or some such - deliberately right outside the viewport. Should a 1px blur make it suddenly participate in the transition? Feels to me that this should not be something that relies on implementation-specific things and the author should be able to curate this with properties rather than rely on this unspeced behavior.

jakearchibald commented 7 months ago

How is the developer intent here different from the developer intent in IntersectionObserver?

IntersectionObserver is more about reacting to an element when it's in the viewport, that's why it has options for being completely in the viewport, and everything in between. It was originally created with lazy-loading in mind.

With view transitions, we're in a very visual space. So "not there" becomes more of a visual question than a layout question. If we count "partially visible" as "not there", then you'll end up with transitions where the old thing and the new thing are both visible (to some degree) in the transition, but they won't transition as part of the same group, so the user will see double. I don't think that's a desirable outcome of this feature.

Also, parts of intersection observer do consider ink overflow.

noamr commented 7 months ago

Also, parts of intersection observer do consider ink overflow.

There's an open proposal about occlusion, is that what you mean? Otherwise can you be specific?

jakearchibald commented 7 months ago

The occlusion stuff is shipped in Chrome, no?

noamr commented 7 months ago

With view transitions, we're in a very visual space. So "not there" becomes more of a visual question than a layout question. If we count "partially visible" as "not there", then you'll end up with transitions where the old thing and the new thing are both visible (to some degree) in the transition, but they won't transition as part of the same group, so the user will see double. I don't think that's a desirable outcome of this feature.

I can totally see this point. The other side of this is that due to the implementation-specific nature of ink overflow, the outcome of using it here would be that in some cases you'd have elements that are visually 100% outside the viewport (on the edge) participate in the transition even if you gave them view-transition-visibility: auto - let's say they have a few pixels of blur that are reserved but not actually rendered, and you'd have no recourse to fixing it (except for maybe using IntersectionObserver yourself). Having a property for this (some viewport margin or element clip margin is enough) gives this control to the developer rather than to implementation details that can change at any time.

An alternative would be to actually specify some sort of max ink-overflow for the different visual effects (though glyphs also have an ink overflow which makes this a bit icky).

noamr commented 7 months ago

One thing we can do rather than rely on ink overflow specifically but would have the same effect, is to say that elements outside the viewport have an implementation-defined margin around them as a buffer, which can be overridden with a CSS property. This way if the author wants a very specific control of how this works they can do that, and the default is known to be implementation-defined.

toddpadwick commented 4 months ago

running into quite a few use cases where having the captured transition views limited to just the viewport would be a requirement. upvote from me 👍

noamr commented 4 months ago

This would be really useful. running into quite a few issues where this would drastically improve transitions. upvote from me 👍

Would this be more about a performance optimization for you, or for design (avoiding things flying in/out of the screen)?

toddpadwick commented 4 months ago

Hey @noamr mainly design but performance is an issue as well, since if you're applying transform transitions to a whole page, and if the page is really long that can have cause huge issues on performance, and unexpected rendering consequences. In terms of design:

  1. Just as you've flagged, without having to do hacky functions to add view transition names based on intersection observer (which I think just takes away to beauty of how wonderfully simple the view transition API has the potential to be), things fly in and out unintentionally.
  2. Lets say you wish to slide in the old view followed by the new view, a bit like a slideshow, you can't (unless I am missing something), because the whole page content is included, not just the clipped viewport content. See mock up below: Screenshot 2024-02-19 at 16 33 33
noamr commented 4 months ago

Hey @noamr mainly design but performance is an issue as well, since if you're applying transform transitions to a whole page, and if the page is really long that can have cause huge issues on performance, and unexpected rendering consequences. In terms of design:

  1. Just as you've flagged, without having to do hacky functions to add view transition names based on intersection observer (which I think just takes away to beauty of how wonderfully simple the view transition API has the potential to be), things fly in and out unintentionally.
  2. Lets say you wish to slide in the old view followed by the new view, a bit like a slideshow, you can't (unless I am missing something), because the whole page content is included, not just the clipped viewport content. See mock up below: Screenshot 2024-02-19 at 16 33 33

This feels like a different issue... this issue is about not capturing elements below the fold, the image you added is more about clipping the content and adjusting the size to only include the viewport. You can do that today by styling the pseudo-elements and giving them height.

toddpadwick commented 4 months ago

Really? I may be confused still but seems like its still relevant - since I do not want anything outside the viewport from participating in the transition - i want the transition-view image to just be the bounding box of the viewport. If I change the height of them, it compresses the whole page transition view to be 100vh, which is not right.

eg this is the code I hope would achieve desired result:

::view-transition-old(root) {
    animation:slide-view-out 1s;
}

::view-transition-new(root) {
   animation:slide-view-in 1s;
}

@keyframes slide-view-out {
    0% {
      transform:translateY(0);
    }
    100% {
      transform:translateY(-100vh);
    }
  }

  @keyframes slide-view-in {
    0% {
      transform:translateY(100vh);
    }
    100% {
      transform:translateY(0);
    }
  }
noamr commented 4 months ago

Really? I may be confused still but seems like its still relevant - since I do not want anything outside the viewport from participating in the transition

"Participating in the transition" is about whether the elements themselves participate individually, rather than whether the content is clipped and the size of the pseudo-element adjusting.

i want the transition-view image to just be the bounding box of the viewport. If I change the height of them, it compresses the whole page transition view to be 100vh, which is not right.

I think object-fit: cover would help here? (Need to try some of this, added a note...)

toddpadwick commented 4 months ago

ah I see what you mean. Just left the office but will give that object-fit cover a go in the morning :) thanks @noamr

toddpadwick commented 4 months ago

Hey @noamr Just to let you know, I gave that object-fit cover a go - didn't seem to help – it still has the same issue where it shrinks the height of the whole page (so you see a flash of the distorted page for a split second. I'm going to revert to using Vue JS page transitions for the time being. on another note on view-transition performance, I am finding quite a bit of jumpiness on some devices - see this staging site https://functionandform2024.netlify.app/ and click on one of the work case studies further down the page. However, this is probably unrelated

noamr commented 4 months ago

Hey @noamr Just to let you know, I gave that object-fit cover a go - didn't seem to help – it still has the same issue where it shrinks the height of the whole page (so you see a flash of the distorted page for a split second. I'm going to revert to using Vue JS page transitions for the time being.

on another note on view-transition performance, I am finding quite a bit of jumpiness on some devices - see this staging site https://functionandform2024.netlify.app/ and click on one of the work case studies further down the page. However, this is probably unrelated

Thanks for the details, let me raise these internally .

bokand commented 4 months ago

@toddpadwick - do you have an example URL of the "slideshow" transition not working (apologies if it's in the above URL but I can't find it...)? The root snapshot should be limited to what's in the viewport and I've used this approach in some toy examples (https://viewtransition.glitch.me/test/gesturedemo-vertical.html).

The only thing I could think of is on mobile, if your site is wider than the device screen but you have initial-scale=1 in your viewport meta you're technically zoomed in so the "captured viewport" is larger than your visual viewport. But that's not a consideration on desktop if that's where you're seeing it. If you have a link I can take a closer look...

khushalsagar commented 4 months ago

@toddpadwick the spec explicitly clips the root snapshot to the viewport, see text here. If you have a case where this is not happening for the root element, it's an implementation bug.

That said, the use-case you mentioned is still relevant for non-root elements and is being tracked in https://github.com/w3c/csswg-drafts/issues/9481. This issue is about whether an element participates in the transition and #9481 is about what portion of its content is captured in an image if it participates in the transition.

noamr commented 3 months ago

A potential name for this: view-transition-cutoff: viewport | none.

jakearchibald commented 3 months ago

cutoff feels like it could mean clipping, whereas this is about excluding.

I'm not saying any of these are better, but here goes nothing:

ugh.

jakearchibald commented 3 months ago

Developers want this feature to exclude items that don't render within the viewport, and that includes considering ink overflow. I think it'd be hard to explain to them that they can't have the feature they want because that concept is too hard to spec 😄

noamr commented 3 months ago

Developers want this feature to exclude items that don't render within the viewport, and that includes considering ink overflow. I think it'd be hard to explain to them that they can't have the feature they want because that concept is too hard to spec 😄

Looking at https://github.com/w3c/csswg-drafts/issues/8649#issuecomment-2010712895, there is a security issue with using the ink overflow as is, as things like a11y extensions can affect it (e.g. adding an outline/background to fonts that is not web-observable).

jakearchibald commented 3 months ago

Yeah, that's fair

noamr commented 3 months ago

cutoff feels like it could mean clipping, whereas this is about excluding.

I'm not saying any of these are better, but here goes nothing:

  • view-transition-requirements: viewport-intersection
  • view-transition-exclude: outside-viewport

ugh.

How about view-transition-scissor: viewport | document ? It's kind of like glScissor. Or view-transition-extents: viewport

bramus commented 3 months ago

How about view-transition-scissor: viewport | document ?

Iterating on this:

noamr commented 3 months ago

How about view-transition-scissor: viewport | document ?

Iterating on this:

  • For the property name: view-transition-snapshot-area.
  • For the values: viewport | root, with root being the VT root (which is document or, in the case of a scoped transition, the element on which the author called .startViewTransition()) root could be confusing as one might think it’s :root. I have considered element but that doesn’t map well to document. Another alternative for this could be scope. Also: what should be the default value?

I think today it's root? Per spec we don't capture elements that go beyond the document rect.

jakearchibald commented 3 months ago

I feel scissor has the same issue as cutoff - it feels like the exclusion could be partial. Whereas this feature is about the element being included or excluded from capture.

noamr commented 3 months ago

view-transition-inclusion-area: viewport ? A bit verbose :) Anyway, let's deal with the bikeshedding once we figure out the main issue (extents/ink-overflow).

jakearchibald commented 3 months ago

That seems ok. Although I'm not sure how you'd add the "but consider ink overflow" option in future. (I realise I'm not being a lot of help here)