w3c / csswg-drafts

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

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

Closed khushalsagar closed 1 month 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.

khushalsagar commented 2 months ago

avoiding a fly-in that is "quite far" but the element still intersects the viewport by a single pixel

IIUC the farthest animation distance in this case would be animating from 1 edge of the viewport to the other (for the longer axis). I was assuming that is not the case authors are trying to fix in that, the element is in the viewport throughout the animation. It's the cases where the element stays offscreen for majority of the animation that looks awkward. Because then instead of getting an animation, the element just "shows up" in the last few frames of the animation. I could very likely be missing an edge case!

The other reason I like this proposal is that it makes it trivial for authors to optimize away forced rendering of offscreen content that VT currently does. But the property has to be useful in most cases for authors to actually use it. So definitely happy to get more feedback on whether this actually addresses the target use-cases!

bramus commented 2 months ago

It could be that a customizable margin property in addition would fix that

We could use insets to determine the capture area? Positive values for insets go inwards and negative ones go outwards.

Making up properties as I go, it would result in something like this:

view-transition-capture-inset: auto | <percentage-length>{1,4}; /* To be set on the transition root upon which you call startViewTransition(). For `document`, set this on `:root` */
view-transition-visibility: always | not-clipped; /* To be set on individual elements */

An alternative to view-transition-visibility as suggested a few times here is to determine the “capture things or not” on the transition root itself. Something like:

view-transition-capture-inset: auto | <percentage-length>{1,4};
view-transition-capture-mode: all | not-clipped;

The case against view-transition-visibility is that you need to do this on a per-element basis. OTOH is does give you fine-grained control.

The case against view-transition-capture-mode is that it’s “all or nothing”. OTOH it’s “set once and you’re good”.

Side note: a hack would be to use scroll-driven animations to unset the view-transition-name when an element out of view, but that’s a hack that we shouldn’t really rely on here.

khushalsagar commented 2 months ago

We can add view-transition-capture-inset to customize the margin for determining an element's visibility on top of view-transition-visibility going forward.

Between view-transition-visibility and view-transition-capture-mode, you could do the latter with something like:

#transition-root * {
  view-transition-visibility: not-clipped;
}

So seems nicer to give view-transition-visibility which allows per element choice..?

Also, just to clarify, @noamr's argument is not about how the author expresses whether the element should be captured (based on visibility) or not. But whether the API should be about controlling whether the element is captured. Or just exposing what the element's visibility was. For example, ::view-transition-old:in-viewport/::view-transition-old:out-of-viewport, and let the author decide how they want to react for each case. One of which could be to override the UA default morph animation with an entry/exit animation.

My take is that the above is more complicated than it needs to be, especially given that this feature is about providing an easy declarative way to do something which is already possible in script. So the declarative solution should make ~most of the cases easy instead of trying to solve all possible cases. And also allow authors to optimize away offscreen rendering.

khushalsagar commented 2 months ago

One more thing, one good point which has been raised on this issue is that if we default to ignoring offscreen elements it would break legit use-cases. And authors would just do something like * { view-transition-capture: all } to get back the old behaviour.

I suspect most cases where authors add a name to a lot of elements is list items (like changing a grid layout), where likely only the element position is changing and its content is the same. So for the perf thing, maybe https://github.com/w3c/csswg-drafts/issues/9406 is a better fix. We can default to skip caching paint for offscreen content and require authors to explicitly opt-in. Since for new live elements, we can generate paint when the pseudo comes onscreen this won't be an issue unless a cross-fade was really needed. And I don't see why that would be necessary if the old content was offscreen to begin with.

css-meeting-bot commented 2 months ago

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

The full IRC log of that discussion <TabAtkins> khush: two problems
<TabAtkins> khush: one on my mind intiially, one brought by authors later. i'll talk about author problem first
<TabAtkins> khush: the way we decide aniamtions during an aimtion is the classic FLIP - start state, end state, set up an animation using it
<TabAtkins> khush: ways to have header/foother on the page that can be scrolled away
<TabAtkins> khush: might be super far away after scrolling
<TabAtkins> khush: simple FLIP animation gives a 1000px translation happening in 250ms which is mostly offscreen, which is effectively no animation - only on screen in last frame
<TabAtkins> khush: can work around it with intersectionobserver and conditionally put a name on it
<TabAtkins> khush: not great, needs JS
<TabAtkins> khush: so want a declarative way - rather than have a morph animation, make it entry/exit if the start/end state is offscreen
<TabAtkins> khush: my issue was - if you put a name on something, no matter how offscreen it is (content-visibility aside) then the browser will render it
<TabAtkins> khush: worse than style and layout, have to actually paint, and keep those pixels in memory
<TabAtkins> khush: costs a lot to do offscreen rednering
<TabAtkins> khush: and in the common case, offscreen things stay offscreen. then spending those resources for nothing.
<TabAtkins> khush: was hoping this could be used to let authors optimize this case for thsemelves. maybe even change the default behavior.
<TabAtkins> khush: three qs
<TabAtkins> khush: first, how to define offscreen
<TabAtkins> khush: related to the reference rect discussion, we want ink overflow, and i think that's sufficinetly specced now
<TabAtkins> khush: consistent with anchor pos too
<TabAtkins> khush: second, syntax
<TabAtkins> khush: lot of options on this issue
<TabAtkins> khush: proposing view-transition-visibility: visible | clip
<TabAtkins> khush: in the issue was suggested to match position-visibility value space, always | not-clipped
<vmpstr> q?
<bramus> q-
<vmpstr> q+
<TabAtkins> khush: we can bikeshed thenames
<TabAtkins> khush: 3, should we make clipping the default behavior?
<ydaniv> q+
<TabAtkins> khush: so easy to have offscreen content animating without realizing
<TabAtkins> khush: some author pushback, because this isn't just perf, it changes the animation
<TabAtkins> khush: like if you have a list, you delete an item, the next item is just offscreen, it just fades in rather than morphing
<TabAtkins> q+
<astearns> ack vmpstr
<TabAtkins> vmpstr: to add to pushback cases, i think the default is web-breaking, to be clear
<TabAtkins> changing the default, that is
<TabAtkins> vmpstr: some pushback on using viewport + ink-overflow
<TabAtkins> vmpstr: it's a very binary behavior, no smooth in-between
<TabAtkins> vmpstr: may be awkward to have such a different experience based on 1px scroll difference
<astearns> ack ydaniv
<TabAtkins> ydaniv: regarding name, my proposal is overflow, becuase visibility of VT sounds like your'e changing the visibility of the transition itself
<TabAtkins> ydaniv: overflow, sound slike you're talking about what you're clipping, or about elements that overflow the viewport
<TabAtkins> ydaniv: so view-transition-overflow: clip | visible
<TabAtkins> ydaniv: I think the use-cases are really common tho, think we should take care of it
<TabAtkins> ydaniv: need something to let it get clipped, don't blow up just because i have some things out of view
<TabAtkins> ydaniv: and if we add overflow-inset or margin, to increase level of tolerance
<astearns> ack TabAtkins
<astearns> q+
<fantasai> TabAtkins: I also have a minor objection to using the same definition as anchor-visibility does
<khush> q+
<fantasai> TabAtkins: I think it should be more like content-visibility where it's UA by default
<fantasai> TabAtkins: but also has a bit of margin of offscreen, so that you're ready
<fantasai> TabAtkins: it's not just offscreen, it's wayyy offscreen, so the animation is not visible at all
<fantasai> TabAtkins: So larger margin, smae concept as content-visibility
<fantasai> TabAtkins: don't think we need exact precision for it
<vmpstr> q+
<fantasai> TabAtkins: anchorpos needs it to be immediately as soon as it goes offscreen, because that's the use case
<TabAtkins> khush: i mentioned this exact thing on the issue, the dev response was with c-v the fact there's a ua-defined margin is there's a big perf thing, you don't see the difference based on the ua's choice
<vmpstr> q-
<vmpstr> +1
<TabAtkins> khush: if i'm debugging and see one behavior on chrome i won't chekc safari for a different behavior
<TabAtkins> khush: so point was to be explicit, since it changes the visual animation
<TabAtkins> khush: we can add a margin and let devs control
<fantasai> TabAtkins: viewport + margin is what I care about
<fantasai> TabAtkins: don't want super abrupt case
<fantasai> TabAtkins: We're either doing X or Y, it's abrupt
<ydaniv> q+
<astearns> ack astearns
<fantasai> TabAtkins: making it explicit and not paying cost for invisible frames just acknowledging the reality
<TabAtkins> astearns: i think an addition margin helps but doesn't entirely fix
<TabAtkins> astearns: i fyou have a list of cards, you're deleting some, and 3 or 4 move up into view - it's weird that some close to the view have a smooth transition while further ones are fading in
<TabAtkins> astearns: can do a larger margin, but still hit the case for things sufficiently far away
<astearns> ack khush
<TabAtkins> astearns: so i think i disagree we're fixing *only* the case where none of the naimation is being visible, it's also affecting where *less* of the animation is visible
<TabAtkins> (that's why we'd ahve something controllable, fwiw
<TabAtkins> )
<TabAtkins> khush: noam brought up a point - this feature is right now combining two aspects - whether the elemnt is offscreen or not, and what the browser shoudl do when it is offscreen
<TabAtkins> khush: noam's suggestion is to expose the state to the author. we dont' change wehther we capture or not, but let you know the elment was offscreen in one state (pseudo-class) so you can choose to react to that
<fantasai> TabAtkins: Doesn't solve perf problem though
<TabAtkins> khush: that's true, it's why i was pushing back
<TabAtkins> khush: i have another issue for the same perf problem
<TabAtkins> khush: like, if something is offscreen, we capture the box, just not its painted contents
<TabAtkins> khush: then we just don't cross fade, jsut use the new paint
<fantasai> TabAtkins: I quite like that
<fantasai> TabAtkins: fixes the case Alan spoke about
<fantasai> TabAtkins: and in general, seems like it's muuuch more likely to be compatible with existing content
<fantasai> TabAtkins: because geometry of animation is identical between now and new version
<fantasai> TabAtkins: only change is appearance in all but final frame, and only slightly
<fantasai> TabAtkins: so that sounds great
<TabAtkins> vmpstr: and if you're reordering lists and content isn't changing, you can just disable the crossfade and only show the new content, this solves that problem
<TabAtkins> khush: in most of these cases you really just want the transform, not the crossfade or size
<TabAtkins> khush: might want to think about size at all
<TabAtkins> khush: but if it's just moving, tkhis works perfectly well
<astearns> ack ydaniv
<TabAtkins> ydaniv: can you repeat the suggestion?
<TabAtkins> khush: [repeats it]
<TabAtkins> khush: and i think that's ok becuase you want to cross-fade when both old and new is visible to the user. if one state wasn't visible, no need to crossfade
<TabAtkins> ydaniv: yeah. and to alan's point, right now everything is captured, so you'll see all the cards.
<TabAtkins> ydaniv: I think the suggestion is that you'll clip.
<astearns> ack fantasai
<TabAtkins> [not 100% sure i captured ydaniv properly there]
<TabAtkins> fantasai: +1 to this idea too
<TabAtkins> fantasai: and for the pseudo-element indicating what's happening
<TabAtkins> fantasai: pseudo-class
<TabAtkins> fantasai: i think you need to expose all three cases - off-screen start, end, or both
<ydaniv> s/I think the suggestion is that you'll clip./the suggested property is an opt-in so you're asking to clip it for you when you want to optimize/
<TabAtkins> fantasai: maybe :offscreen and :offscreen(start | end | both)
<fantasai> s/start | end/new | old/
<TabAtkins> flackr: case to consider - new is above the screen, old is below the screen, so the animation passes *thru* the screen
<TabAtkins> fantasai: yes, interesting, should think about it
<TabAtkins> fantasai: might want to not transition at all
<TabAtkins> bramus: or might want to explicitly see that - if you're reordering a list and the part on screen stays still, want an indicator that the rest is flying around
<TabAtkins> fantasai: so want to consider the conditional styling there
<TabAtkins> khush: guess in that case you jsut want the current behavior
<TabAtkins> khush: whatever animation the browser sets up today
<TabAtkins> flackr: I think you'd be able to skip the capture, but still animate the new image
<fantasai> TabAtkins: Falls into same problem as you might see only 1 frame of the animation
<fantasai> TabAtkins: I think we can probably use the new state render for that and call it a day
<TabAtkins> astearns: so should we take all this discussion to the issue and synthesize a new proposal?
<TabAtkins> fantasai: seem to have agreement to pursue keeping the element, nto capturing the bitmap
<flackr> +1
<ydaniv> +1
<TabAtkins> fantasai: and maybe a pseudoclass for doing something special when something is offscreen
<fantasai> astearns: by how much offscreen?
<fantasai> TabAtkins: I think we can let the UA define
<fantasai> flackr: could be somewhat similar to content-visibility
<fantasai> vmpstr: new property or change behavior?
<fantasai> TabAtkins: I think you could just change the behavior.
<fantasai> astearns: How about we resolve that we want to add an optimization for offscreen and continue
<TabAtkins> astearns: let's jsut resolve that we want to add an optimization for VTs of offscreen elements. continue the discussion in the issue.
<TabAtkins> fantasai: I think we are specific about the bitmap dropping
<TabAtkins> astearns: proposed: we *will* add an optimization for offscreen things where we wont' retain the bitmap
<TabAtkins> RESOLVED: We will design an optimization for VT of "offscreen" elements wherein we don't capture the bitmap for their offscreen state. Details TBD.
khushalsagar commented 2 months ago

Offline discussion on this today. We have a proposal for how the pseudo-element rendering changes if the old element is offscreen at capture time.

Morph Animation

This is the case where both old and new elements exist for the same name:

A few other options considered were:

Exit animation

This is the case where only the old element exists for a name. A couple of options here:

  1. Generate the old pseudo but render no content in it. And switch from a fade out animation to a 0->0 opacity animation.
  2. Don't generate any pseudos for this name at all, since we'll basically be doing animations with empty boxes.

Leaning towards 1) because again least compat risk.


The question for how we define an element is offscreen is still open and can be discussed separately.

css-meeting-bot commented 2 months ago

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

The full IRC log of that discussion <fantasai> s/any other/Seeing strong consensus here, can always re-open. Any other/
<chrishtr> khush: this is a continuation of something discussed at the F2F
<chrishtr> khush: we had a high-level resolution that we should avoid capturing things offscreen, and in particular not capture images for them
<chrishtr> khush: today I'd like to go into UA styles for this
<chrishtr> khush: for something in the old and new state, proposal is that we keep size animations
<fantasai> +1
<chrishtr> khush: another question was whether to keep the pseudo element for the old thing. propose to keep it because there developer code expects it
<chrishtr> khush: "there is developer code that expects it"
<chrishtr> khush: think there would be less of a compat risk if the new image had something instead of nothing?
<chrishtr> khush: by default you get a cross-fade in these cases, but don't want to do that and instead just show the new image
<chrishtr> khush: propose that there be a 0->0 opacity animation for the old and 1->1 for the new. reiterating that all this is to minimize compat risk.
<chrishtr> khush: also propose removing mix-blend-mode: plus-lighther
<chrishtr> khush: for exit animations, seems less clear
<chrishtr> khush: in these case you're animating an element that has no content
<chrishtr> khush: one option is to generate a new pseudo and animations are technically there but irrelevant but it doesn't exist, another option is to have no pseudo
<Rossen7> ack fantasai
<chrishtr> fantasai: I agree that the option about not generating a pseudo is a bad option, we shouldn't do that
<chrishtr> fantasai: as for generating the pseudo but having no image backing it, the whole point is for cases where it's offscreen before and after right?
<chrishtr> khush: propose we do it based only on the old state, because it has to be done at the beginning of the animation.
<chrishtr> khush: even for cases where things animate on screen it seems ok to skip the image because the user never saw the old content anyway
<chrishtr> khush: reason to keep around the pseudo is because the developer might have animation keyframes for it
<chrishtr> s/khush/flackr/
<chrishtr> fantasai: the alternative would be that the old pseudo has no image and the new one is 1->1. Either way you're just showing the new image
<chrishtr> flackr: default behavior is the same in either case
<chrishtr> khush: that's why I'm lukewarm on it, because it's hard to say what is less compat risky without deeper analysis
<chrishtr> fantasai: would it be a problem if we set up the cross-fade animation as normal?
<chrishtr> khush: if you set up the cross-fade as normal, then you're assuming the pseudos are all set up and it cross-fades?
<chrishtr> khush: yes, but it makes it harder to optimize out in the browser implementationo
<chrishtr> fantasai: if it's really needed for optimization that's fine, but otherwise just due to implementation complexity..
<chrishtr> vmpstr: we can probably optimize the animation by detecting this situation
<chrishtr> khush: if we keep the default cross-fade it is harder to detect...
<chrishtr> vmpstr: harder optimization but plausible to do so
<chrishtr> khush: if needed I can find a hack to optimize
<chrishtr> flackr: from a developer perspective it's better not to have animations be different in these situations
<chrishtr> khush: remaining questions is just about not having cross-fade, and ?
<chrishtr> khush: ok resolving on the first two question and then investigating implementability of avoiding cross-fade changes
<flackr> +1
<khush> `::view-transition-old` renders new live image (if available) if old element is offscreen at capture time
<chrishtr> RESOLVED: ::view-transition-old` renders new live image (if available) if old element is offscreen at capture time
<khush> The geometry animation on the `::view-transition-group` pseudo is same as before.
<chrishtr> RESOLVED: The geometry animation on the `::view-transition-group` pseudo is same as before.
noamr commented 1 month ago

I think that in addition to the resolution we need to enable authors ways to override/customize this behavior:

khushalsagar commented 1 month ago

override the default, stating that a particular element should not have its contents clipped.

Did you mean to say, "a particular element should have its contents clipped". Because the default for everything except the root is no ancestor/viewport clipping. Agreed that we should have something like this but we can treat it as a separate problem? Just want to confirm that there's no reason a property like this should be done along with the behaviour change we resolved on.

Customize the default animation behavior when we're displaying the new live snapshot instead of the old snapshot.

This makes sense, authors should be able to detect this state. How about old-content-skipped or old-content-captured? The name should make it clear that old content was available (this isn't an entry animation) but we explicitly didn't capture it.

We'd likely eventually add a property to opt-in to this behaviour even for cases where the old element is onscreen. All the grid re-layout type of transitions where elements move onscreen without changing content can be optimized then. So the name shouldn't make it sound like the old content was offscreen. This is where I wasn't sure if old-content-hidden is ambiguous.

noamr commented 1 month ago

override the default, stating that a particular element should not have its contents clipped.

Did you mean to say, "a particular element should have its contents clipped". Because the default for everything except the root is no ancestor/viewport clipping. Agreed that we should have something like this but we can treat it as a separate problem? Just want to confirm that there's no reason a property like this should be done along with the behaviour change we resolved on.

No, sorry, I mean "a particular element should not have its content clipped, even if it is away from the viewport".

Customize the default animation behavior when we're displaying the new live snapshot instead of the old snapshot.

This makes sense, authors should be able to detect this state. How about old-content-skipped or old-content-captured? The name should make it clear that old content was available (this isn't an entry animation) but we explicitly didn't capture it.

Sounds good!

We'd likely eventually add a property to opt-in to this behaviour even for cases where the old element is onscreen. All the grid re-layout type of transitions where elements move onscreen without changing content can be optimized then. So the name shouldn't make it sound like the old content was offscreen. This is where I wasn't sure if old-content-hidden is ambiguous.

+1

khushalsagar commented 1 month ago

No, sorry, I mean "a particular element should not have its content clipped, even if it is away from the viewport".

Ah got it. More like, "a particular element's content should be captured (instead of using the new content) even if its away from the viewport".

In that case I meant the same property as you in "We'd likely eventually add a property to opt-in to this behaviour even for cases where the old element is onscreen". A tri-state CSS property to decide capturing of old content: always capture, never capture and only capture when onscreen (which is the default). Can bikeshed on naming.

noamr commented 1 month ago

As far as the OP goes, this is achievable today using scroll-driven animations. See this demo: https://codepen.io/noamr-the-selector/pen/jOjPBXP

Basically the element in question has a view-timeline-name and an animation-timeline referencing itself, with @keyframes that animate the view-transition-name property. This makes it so that only when the element is in the viewport it has a view-transition-name. The offset can be controlled using view-timeline-inset.

It's perhaps hacky to use SDA for this but it works today and can be used for other things than view-transitions as well!

jakearchibald commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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

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 commented 1 month ago

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 commented 1 month ago

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

-webkit-box-reflect is already a thing 😄

noamr commented 1 month ago

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 commented 1 month ago

-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 commented 1 month ago

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

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/w3c/csswg-drafts/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 commented 1 month ago

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.