Skip to content
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

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

Closed
khushalsagar opened this issue Jan 5, 2023 · 124 comments · Fixed by #10616
Closed
Labels
css-view-transitions-2 View Transitions; New feature requests Needs Edits

Comments

@khushalsagar
Copy link
Member

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:

  • Add a new CSS property view-transition-offscreen which customizes the behavior for named elements based on their position in the snapshot viewport.
  • 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".
  • absent indicates that if the element's ink overflow rectangle does not intersect the snapshot viewport, the element does not participate in the transition (as-if its view-transition-name's computed value was none).
  • The computation for the element's viewport position for the decision above can be done here for old elements. And here for new elements. The subtlety is that for new elements we do it before resolving the ready promise so script can observe the final pseudo DOM structure.

See prior discussion on this here.

@khushalsagar khushalsagar added css-view-transitions-1 View Transitions; Bugs only css-view-transitions-2 View Transitions; New feature requests and removed css-view-transitions-1 View Transitions; Bugs only labels Jan 5, 2023
@jakearchibald
Copy link
Contributor

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
Copy link
Contributor

  • 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 #7874

@vmpstr
Copy link
Member

vmpstr commented Jan 9, 2023

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
Copy link
Contributor

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
Copy link
Member Author

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
Copy link
Contributor

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

@khushalsagar
Copy link
Member Author

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
Copy link
Contributor

bramus commented Oct 5, 2023

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
Copy link
Contributor

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
Copy link
Member Author

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
Copy link
Collaborator

noamr commented Oct 23, 2023

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:

  • visible: current behavior
  • auto: similar to content-visibility: auto. element acts as if it doesn't have a view-transition-name if it's not intersecting with the viewport.
  • hidden: element ignores its view-transition-name. As this attribute is inherited, this can be used to hide an entire tree from the view transitioning capture algorithm
  • clip: the element participates in the transition, but only contents of the element that intersect with the viewport are captured..

@khushalsagar
Copy link
Member Author

+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
Copy link
Collaborator

noamr commented Oct 30, 2023

A few additional comments:

  • 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.
  • 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? e.g. perhaps still show the ink-overflow? something about clip feels a bit different as it clips the content rather than removes the whole capture.

@khushalsagar
Copy link
Member Author

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: #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 #9354 should eventually be handled by view-transition-visibility.

@jakearchibald
Copy link
Contributor

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
Copy link
Contributor

jakearchibald commented Nov 8, 2023

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
Copy link
Member

vmpstr commented Nov 8, 2023

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
Copy link
Member Author

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 [css-overflow-3] Specify extent of ink overflow #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.

@jakearchibald
Copy link
Contributor

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

@noamr
Copy link
Collaborator

noamr commented Nov 16, 2023

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 [css-overflow-3] Specify extent of ink overflow #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
Copy link
Contributor

jakearchibald commented Nov 16, 2023

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
Copy link
Collaborator

noamr commented Nov 16, 2023

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
Copy link
Collaborator

noamr commented Nov 16, 2023

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
Copy link
Contributor

It's perhaps hacky to use SDA for this

It is. I'm sad that the OP has been ignored given the number of developers asking for this. I guess we can continue to solve it with JavaScript.

@noamr
Copy link
Collaborator

noamr commented Jul 15, 2024

It's perhaps hacky to use SDA for this

It is. I'm sad that the OP has been ignored given the number of developers asking for this. I guess we can continue to solve it with JavaScript.

Given the following:

@keyframes apply-view-transition-name-when-in-range {
  from { view-transition-name: var(--view-transition-name);  }
  to { view-transition-name: var(--view-transition-name); }
}

header {
  --view-transition-name: header;
  animation-name: apply-view-transition-name-when-in-range;
  animation-timeline: view();
}

Can you be more specific about what behavior the OP requests that's different? Is it just the aesthetic aspect of using an animation? If It looked something like this (with the exact same behavior) would it make any difference?

header {
  @in-view {
     view-transition-name: header;
  }
}

Note that using view timelines for this has the added value of being able to control a different behavior when slightly off screen and far away from screen:

@keyframes apply-view-transition-name{
  from { 
   view-transition-name: var(--view-transition-name);  
   view-transition-class: near;
  }
  10% { view-transition-class:  inside; }
  90% { view-transition-class:  inside; }
  to { 
    view-transition-class: near;
    view-transition-name: var(--view-transition-name); 
  }
}

header {
  --view-transition-name: header;
  animation-name: apply-view-transition-name;
  view-timeline-inset: 50vmax;
  animation-timeline: view();
}

I think the problem with this issue is that it defines a scroll-driven behavior for view-transitions and we are trying to do this as a shortcut, while we already have a scroll-driven primitive that can do this, and the main issue with it is that it's tied to "animations"?.

@jakearchibald
Copy link
Contributor

Is it just the aesthetic aspect of using an animation?

Pretty much, yeah. I'm guessing it doesn't account for ink overflow either? Are Chrome's dev rels happy with documenting this feature as a hack with scroll timelines?

@noamr
Copy link
Collaborator

noamr commented Jul 15, 2024

Is it just the aesthetic aspect of using an animation?

Pretty much, yeah. I'm guessing it doesn't account for ink overflow either?

Correct, you have to set view-timeline-inset explicitly. But this is a known view-timeline issue, e.g. if you currently use text as your view timeline it wouldn't account for the overflowing edges.

Are Chrome's dev rels happy with documenting this feature as a hack with scroll timelines?

We'll discuss this internally to see if people are content with it. I have it on my list to document it (and another hack to override the "don't capture contents if it's far from the screen" behavior we resolved on here) as ways to change view-transition behavior based on scroll position of participating elements.

@khushalsagar
Copy link
Member Author

I'm sad that the OP has been ignored given the number of developers asking for this.

@jakearchibald apologies if it came across that this feature request is being ignored. OP's use-case of customizing the UX when content is offscreen was explicitly discussed at the last CSS f2f, the detailed notes are here.

One of the questions raised in that discussion was if we should be providing the offscreen state of the old/new DOM elements as a pseudo-class. Something like :offscreen and :offscreen(start | end | both). Then authors can choose to react to these states by specifying a custom animation. That seems more flexible than a property which says, "ignore this element's name if its offscreen".

@noamr
Copy link
Collaborator

noamr commented Jul 16, 2024

Created #10581 and #10582 to account for the two issues with using view-timeline for this:

  • Account for ink-overflow somehow (beyond the explicit view-timeline-inset), perhaps with view-timeline-range.
  • using animation for something that's not really an animation (but just a range) is weird

I believe that these issues belong in scroll-driven animations and not in view transitions, as they define scroll-related behavior. For example, the same patterns can be used to change the behavior of a regular CSS transition if it's away from the viewport.

@noamr
Copy link
Collaborator

noamr commented Jul 23, 2024

Summary of internal discussion and #10587:
WebKit has raised an implementation concern regarding displaying the live image twice.
We see a few options on how to resolve this:

  1. try to figure it out in implementations
  2. make this implementation-defined (maybe less recommended, a new cross-browser difference)
  3. not capture any pixels at all for off-screen elements, and find some way to reflect this to the developer (a pseudo-class?). This might break some use-cases such as morphing large out-of-viewport elements into small in-viewport elements.
  4. Allow user-agents to capture a lower-resolution image for offscreen elements. This is already text to that effect here with a different purpose, elements with a large ink overflow, we can tune it a bit to clarify that it also applies for out-of-viewport elements.
  5. Capture the old state later, e.g. when capturing the new state if at that time the element intersects with the viewport.

I currently tend to like (4) as it allows optimizing the important things without totally breaking the user-experience or creating a new mental load on authors. It also allows different tiers of optimizations based on device/memory/whatever as the results are not web-observable and the tradeoff between performance and visual degradation can be played with.

@jakearchibald
Copy link
Contributor

WebKit has raised an implementation concern regarding displaying the live image twice.

-webkit-box-reflect is already a thing 😄

@noamr
Copy link
Collaborator

noamr commented Jul 23, 2024

WebKit has raised an implementation concern regarding displaying the live image twice.

-webkit-box-reflect is already a thing 😄

True, https://github.com/WebKit/WebKit/blob/main/Source/WebCore/platform/graphics/ca/GraphicsLayerCA.h#L654 seems to do something like cloning a layer but it's been a while since I touched CA so will leave this to @nt1m and @mattwoodrow

@nt1m
Copy link
Member

nt1m commented Jul 23, 2024

-webkit-box-reflect isn't a pattern we'd like to repeat. It's intrusive and broken in many ways (e.g. video layers are not cloned for instance).

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [css-view-transitions-2] Ignore offscreen elements from participating in transitions, and agreed to the following:

  • RESOLVED: UA is allowed to capture old elements in low resolution if they are off-screen
The full IRC log of that discussion <fantasai> noamr: We were discussing optimization to render new live element in place of old element snapshot for when it's offscreen
<fantasai> noamr: but there were some concerns from WebKit wrt having two live images
<noamr> https://github.com//issues/8282#issuecomment-2245542731
<fantasai> noamr: Discussed using transparent image, but that would create a cross-fade that's not necessarily what's wanted
<fantasai> s/cross-fade/fade-in/
<fantasai> noamr: could make it implementation defined, but we lose compatibility here
<fantasai> noamr: another option is to figure out the implementation
<fantasai> noamr: Option I like the best is keep spec as-is, but say that if an element is off-screen, the UA can capture it at low resolution
<fantasai> noamr: so if you have elements cross-fading from far away, might appear blurry
<fantasai> noamr: also, using view-timeline, authors that want to override can do so
<fantasai> noamr: by giving a different view transition class and use a different animation
<fantasai> noamr: other option that was raised was to make one capture at the beginning of the new element, and use that as the old element
<khush> q+
<Rossen2> ack khush
<fantasai> khush: I like idea of having UA decide the rasterization range
<fantasai> khush: and browser can make trade-offs based on, is this a low-end device, etc.
<TabAtkins> fantasai: I think the idea of doing [missed] and a capture of the new state is interesting. not sure what makes the msot sense, tryign to get Tim to sign on
<fantasai> s/[missed]/low-res captures
<fantasai> noamr: Concern with using new image as old image is the semantic difference
<fantasai> noamr: Want to also not create junk
<flackr> s/junk/jank
<fantasai> fantasai: You mentioned could capture low-res or not at all
<TabAtkins> fantasai: If you have something that isnt' captyured; you're saying an opt is a low-res or not capturing at all, if you realize partway thru that you need the old image and didn't capture it, what do you do
<vmpstr> q+
<noamr> you'd see whatever it is you've captured
<fantasai> flackr: you'd either capture or not up front, and then have transparency if didn't capture
<Rossen2> ack vmpstr
<fantasai> vmpstr: Another option is we don't capture anything in the old, but we also don't do the cross-fade by default
<fantasai> vmpstr: only display the new pseudo-element
<noamr> (rejoining call)
<Rossen2> ack fantasai
<TabAtkins> fantasai: for "only dispaly the new element", I think in that case you'd need to somehow allow the author to select those cases
<TabAtkins> fantasai: so if *they're* doing a cross-fade they'll realize they can't do it
<khush> q+
<TabAtkins> fantasai: if their animation depends on the old image existing, so they can adapt to it
<TabAtkins> fantasai: So on the "this is an optimization" perspective, doing a low-res capture makes more sense to me
<TabAtkins> fantasai: In cases where the UA didn't capture the old el and realized they need it, I think transparent might be problematic
<TabAtkins> fantasai: So instead in that case, if you optimzie too hard, just take a cpature of the new dom and use that instead
<fantasai> vmpstr: Low-res capture cross-fading to that might have artifacts that are unappealing
<TabAtkins> fantasai: to be clear, not saying... you'd only use the new dom in place of the old if you'd *failed* to catpure anything at all. if you have a lowres capture you shoudl use that
<fantasai> khush: On aspect of authors should be able to detect this"
<fantasai> khush: for not capturing the old
<fantasai> khush: would it be enough to give the author a pseudo-class?
<fantasai> khush: UA would not do a cross-fade, and the author can then do a customization
<vmpstr> +1
<Rossen2> ack khush
<fantasai> khush: [something] is not trivial to do
<fantasai> khush: implementation concerns were raised on WK side
<noamr> q+
<fantasai> noamr: aside from being detectable, also want good default
<fantasai> noamr: I like the idea of having one capture of new element in the rare case where we didn't capture anything
<Rossen2> ack noamr
<fantasai> ntim: I would prefer low-res capture of the old state
<fantasai> ntim: I think that's the easiest thing to implement
<noamr> q+
<fantasai> ntim: for authors [garbled]
<fantasai> ntim: show nothing solution is more disruptive to the user
<fantasai> noamr: I would suggest something
<Rossen2> ack noamr
<fantasai> noamr: we say that you can display low-resolution image of the element
<fantasai> noamr: that way we discourage showing totally transparent images
<fantasai> noamr: we don't have to resolve on what happens when we didn't capture anything right now
<fantasai> noamr: we can capture a low-res image, and we discourage capturing fully transparent
<fantasai> noamr: if there's an issue, we can resolve on it then; might not be necessary
<khush> q+
<fantasai> ntim: low-res of old capture seems reasonable to me
<Rossen2> ack khush
<fantasai> khush: do we want authors to be able to detect this?
<fantasai> khush: low-res cross-fade could be looking bad
<fantasai> vmpstr: use case would be e.g. grid-reorder where image is coming in from far off the screen
<fantasai> khush: right now you see the same content, but now you'd see cross-fade between low-res and high-res
<fantasai> vmpstr: can detect if something came from offscreen using view-timeline
<fantasai> vmpstr: e.g. in grid-reordering case
<fantasai> vmpstr: maybe we can resolve on the degradation in a different resolution
<khush> i'm ok with that
<vmpstr> yep., that's fine
<fantasai> ntim: agree with noamr, detecting degradation should be a separate issue so we can get the syntax right
<fantasai> ntim: but right now you can detect animations from off-screen
<fantasai> s/vmpstr/noamr/
<fantasai> s/vmpstr/noamr/
<fantasai> s/vmpstr/noamr/
<flackr> q+
<fantasai> noamr: proposed resolution, if old element is off-screen, UA can capture in low resolutoin
<fantasai> flackr: should we change the UA animation at least to use the new image, since we know that's better?
<fantasai> noamr: That defeats the purpose
<fantasai> flackr: developer animation would still work using old image
<fantasai> flackr: but the UA animation would not need cross-fade..
<Rossen2> ack flackr
<fantasai> flackr: so this doesn't fix not needing to capture issue?
<fantasai> noamr: it fixes a perf issue, a lot less memory etc.
<fantasai> RESOLVED: UA is allowed to capture old elements in low resolution if they are off-screen

@noamr
Copy link
Collaborator

noamr commented Jul 25, 2024

Closed because the optimization use-case seems resolved for now. #10581 and #10582 are still open to track the issue of improving the ergonomics of controlling how view-transitions behave based on distance from viewport.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
css-view-transitions-2 View Transitions; New feature requests Needs Edits
Projects
Status: Thursday afternoon
Status: unsorted FTF