w3c / csswg-drafts

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

[css-view-transitions-1] when to update the pseudo element styles #7812

Closed vmpstr closed 2 years ago

vmpstr commented 2 years ago

Shared Element Transition spec describes pseudo elements that are containers, and that they should animate, for example, from the outgoing element's transform to the new element's transform.

Since this is done with CSS animations, we would need to keep these animations up to date to reflect any changes to the represented element.

Consider the animation is started, and then the represented element is moved around in a setTimeout. Does the animation update any time the represented element changes? Or is there a particular time at which we "update" the pseudo element styles?

I propose that we only update the styles once per frame, around rAF timing. This is still likely to cause us to update the style & layout twice, since we need the first time to determine the represented element's properties, then apply those to the pseudo elements which will cause a style invalidation and run a second style & layout to produce the visual frame.

The only way I can think of to avoid the "double render" is to always lag one frame behind when updating these pseudo element styles, but that doesn't seem ideal

khushalsagar commented 2 years ago

I propose that we only update the styles once per frame, around rAF timing.

+1. In fact we should do after step 14 of update the rendering loop. This allows all script callbacks to run before we query the element's layout size for generating the pseudo-element style.

cause us to update the style & layout twice

The second style&layout is pretty cheap because it only needs to walk the pseudo-element tree. So I think that's reasonable.

khushalsagar commented 2 years ago

We had an offline discussion about this today, driven by ongoing implementation work. I'm adding a summary of the discussion and the updated proposal:

Caching computed values for capture

When script invokes createTransition(), we wait until after ResizeObserver callbacks have been invoked (step 14) at the next update the rendering loop to execute the steps here. This includes identifying elements which will participate in a transition (based on page-transition-tag and other constraints) and caching pixels + other state for them.

The above is by design since we want to cache the state above based on what will be displayed to the user in that frame. Since ResizeObserver callbacks synchronously execute script, mutations for them have to be reflected in the cached state.

Constructing pseudo-DOM

Once the frame above has been presented, the updateDOM callback is invoked (asynchronously) and we wait for it to settle. The task which runs when the promise returned by updateDOM resolves synchronously identifies elements which will participate in a transition in the new DOM. This implies forcing clean style/layout outside of a frame (update the rendering loop) but is necessary here to construct the pseudo-DOM. After this the ready promise is resolved that developer can use to hook their customizations.

The tricky part here is that style on the pseudo-elements depends on style/layout of the corresponding DOM element. The spec text currently indicates that this is also updated only during update the rendering loop. So for example, the following code would result in an incorrect style value:

document.createTransition(...).ready.then(() => {
  // Change the width of a tagged element.
  target.style.width = "100px";

  console.log(window.getComputedStyle(document.documentElement, "::page-transition-container(target)").style.width);
});

It's simpler to treat these pseudo-elements like any other element where from the developer's perspective, the style is always up to date once the ready promise resolves and they are accessible in script. The user agent does whatever computation it needs to do (lazily) to ensure that.

We need to sort out what this means for UA animations (#7813) which will also be impacted by DOM mutations above.

vmpstr commented 2 years ago

t's simpler to treat these pseudo-elements like any other element where from the developer's perspective, the style is always up to date once the ready promise resolves and they are accessible in script.

To be pedantic here, there are two notions here: the script is up to date as specified in the UA stylesheet, and when that UA stylesheet is updated to reflect a changed source element. I agree that script should always observe values in the UA stylesheet when they getComputedStyle, and that something that already happens for all elements.

However, when the stylesheet is updated to represent the changed source element does not have a precedent, so treating this as we would in any other element doesn't mean much.

I think you're suggesting updating the UA stylesheet whenever script requests getComputedStyle, but I suspect that complicates the implementation quite a bit. I would still prefer to have an explicit step with specified timing that takes the changed source element values and updates the UA stylesheet

khushalsagar commented 2 years ago

when the stylesheet is updated to represent the changed source element does not have a precedent

Conceptually generating style for these elements is similar to computing the layout for any element IMO. Let's take the ::page-transition-container as an example. The style values for it are computed based on the text here : "Set width to the current width of capturedElement’s incoming element's border box."

So the browser would need to update layout to get the corresponding element's layout bounds and generate the style values for this pseudo-element. A similar layout operation would've been forced if the developer queried offsetWidth for that DOM element. So why can't we do the same thing here if the developer queries style for that pseudo-element? Trying to limit this to specific events makes it difficult for the developer to reason about when to query this state.

khushalsagar commented 2 years ago

Summarizing an offline discussion:

  1. It's ok to synchronously generate the UA stylesheet for the pseudo DOM when the developer callback is done and before resolving the ready promise. This ensures that developers have a hook in script to customize based on this style information.

    Proposed Resolution: Synchronously generate styles for pseudo-DOM after DomUpdateCallback and before resolving ready promise.

  2. We will also need to re-generate the UA stylesheet after ResizeObserver (when all script mutations are done) to ensure the pseudo-elements reflect the state the DOM is painted with. For example if the DOM element's painting is at a size which is inconsistent with the pseudo-element's style (because its UA stylesheet is stale) then we'll get incorrect rendering.

    Proposed Resolution: Generate styles for pseudo-DOM after step 14 of update the rendering of every frame after resolving ready promise.

  3. It's unclear whether we should regenerate the UA stylesheet for all script APIs like getComputedStyle. The reason is implementation complexity. We'll need to run one style/layout pass over author DOM to compute the requisite state, then generate a UA stylesheet using these values, then trigger another style/layout pass for the pseudo-DOM.

    The downside is that style values on the pseudo-element observed by script will always be a frame late. For example, in the following case the style value will be wrong:

      function prepareFirstFrame() {
         // Change the width of a tagged element.
         target.style.width = "100px";
    
         // This value is based on the stylesheet generated before resolving ready promise.
         console.log(window.getComputedStyle(document.documentElement, "::page-transition-container(target)").style.width);
      }
    
      async function doTransition() {
         await document.createDocumentTransition(...).ready;
         requestAnimationFrame(prepareFirstFrame);
      }

    It's possible that the the first frame of the new DOM depends on script callbacks which run during update the rendering (like ResizeObserver). So the developer won't be able to use that information in customizing their animations.

    Proposed Resolution: UA styles on the pseudo-DOM stay in sync with author DOM for any developer observable API after the ready promise is resolved.

css-meeting-bot commented 2 years ago

The CSS Working Group just discussed when to update pseudo-element styles, and agreed to the following:

The full IRC log of that discussion <TabAtkins> Topic: when to update pseudo-element styles
<TabAtkins> github: https://github.com/w3c/csswg-drafts/issues/7812
<TabAtkins> khush: Need a precise timing for when to generate a UA stylesheet for these pseudos
<TabAtkins> khush: Complicated becuase their styles come from the actual DOM element
<TabAtkins> khush: The process is you call the function, we snapshot things, at the end of that frame we cache all the computed layout info
<TabAtkins> khush: Then when the author has finished with their dom-update callback, we do a sync style+layout to get the same computed layout again
<TabAtkins> khush: Then use that to generate a UA stylesheet, and resolve the promise
<TabAtkins> khush: Doing this before the promise is author has a hook to know what the styles look like before they do their own custom animations with WebAnim, for example
<TabAtkins> khush: So I've got three potential resolutions
<TabAtkins> khush: So first is, when the dev callback is done for DOM updating, is it ok to do a sync style+layout, generate a UA stylesheet, then resolve the promise?
<astearns> Proposed Resolution: Synchronously generate styles for pseudo-DOM after DomUpdateCallback and before resolving ready promise.
<TabAtkins> RESOLVED: Synchronously generate styles for pseudo-DOM after DomUpdateCallback and before resolving ready promise.
<astearns> Proposed Resolution: Generate styles for pseudo-DOM after step 14 of update the rendering of every frame after resolving ready promise.
<TabAtkins> khush: Second, your transition is happening, but author changes the destination element.
<TabAtkins> khush: The snapshot is *live*, so the snapshot size changes.
<TabAtkins> khush: If we don't reflect this in the pseudo it'll look bad
<TabAtkins> khush: So the second spot is where we say we update the UA stylesheet is step 14 in update the rendering, right after ResizeObserver callbacks are dispatched
<TabAtkins> khush: Only after that step do we know that all dev updates are done
<TabAtkins> TabAtkins: Can we just rely on the "implicit end property state" from animations?
<TabAtkins> khush: Too complex, need to adjust things like object-viewbox
<TabAtkins> TabAtkins: Makes sense
<fremy> thanks astearns
<TabAtkins> RESOLVED: Update the UA stylesheet as needed in a new step after the current step 14 of "update the rendering" (after RO callbacks are done)
khushalsagar commented 2 years ago

Adding Agenda+ to resolve on the third one:

Proposed Resolution: UA styles on the pseudo-DOM stay in sync with author DOM for any developer observable API after the ready promise is resolved.

css-meeting-bot commented 2 years ago

The CSS Working Group just discussed [css-view-transitions-1] when to update the pseudo element styles, and agreed to the following:

The full IRC log of that discussion <fantasai> Topic: [css-view-transitions-1] when to update the pseudo element styles
<fantasai> github: https://github.com/w3c/csswg-drafts/issues/7812
<fantasai> khush: Resolution in the last meeting, recap
<fantasai> khush: feature creates a pseudo-element which has snapshot and position from new DOM
<fantasai> khush: live, in sense that new DOM's painting changes will be reflecting
<fantasai> khush: also if position / layout changes, will be reflected
<fantasai> khush: keeping these two in sync requires step where we do layout on author DOM, and then generate a UA stylesheet
<fantasai> khush: do we conceptually see this UA style sheet generation as part of style resolution
<fantasai> khush: e.g. if author changes something about the DOM element, should they get those changes reflected in the pseudo-element if they query via script?
<fantasai> khush: or do we treat this as an explicit step that's part of the feature, and there's a specific point where we get these two things synced up
<fantasai> khush: last time we resolved on two explicit points
<fantasai> khush: this is only relevant if dev queries using script
<fantasai> khush: two options are:
<fantasai> khush: 1. Always keep in sync, treat as part of style resolution. If you call getComputedStyle(), we'll have to resolve style, generate styles and give an answer
<fantasai> khush: 2. Defined point on a frame, so you'll get stale data, whatever was computed in the last frame
<flackr> q+
<fantasai> khush: I think keeping in sync is more correct, but it's also harder because you have to do this loop to resolve style
<fantasai> khush: and we don't have a strong use case for why author would need this to always be in sync
<emilio> q+
<fantasai> khush: so my suggestion is go with 2 defined points where we do this step
<fantasai> khush: if they query after updating DOM, they'll get stale data
<fantasai> khush: and in the future if we want we can make it more correct
<fantasai> khush: so that all updates are guaranteed to reflect correctly
<Rossen_> ack flackr
<fantasai> flackr: My preference is for reflecting up to date values, because that's what will be presented on screen
<fantasai> flackr: so more consistent
<fantasai> flackr: we only have to do this if the author queries the style, so most of the time we won't be doing this extra style update
<Rossen_> ack emilio
<fantasai> emilio: what's the use case for querying these pseudo-elements, and is there a defined answer assuming can target elements in the pseudo tree?
<fantasai> emilio: can't you write selectors that match multiple of these elements?
<fantasai> khush: use case is developer customization, could use information about what the element's transform or size is
<fantasai> khush: can query existing API
<fantasai> khush: element's size will be its border-box size
<fantasai> khush: Some things not available now, e.g. ink overflow bounds
<fantasai> khush: unless we expose no API for it
<fantasai> khush: I don't have any strong use case, just a speculative maybe you want to do a funky animation thing
<fantasai> emilio: My point is there's no good answer, if you write ::part() and that something matches 2-3 elements, what does getComputedStyle return?
<Rossen_> q?
<fantasai> khush: getComputedStyle has option to query an exact pseudo-element, so you can pass ::view-transition-group(name) and get from that element
<fantasai> emilio: a pseudo-element selector can match multiple pseudo-elements, so is it well-defined what happens?
<fantasai> khush: Similar discussion for web animations API, if it matches multiple pseudo-elements, treat it like querySelector and return the first one
<fantasai> emilio: so well-defined, ok
<fantasai> emilio: if there's a good answer, then sure
<fantasai> emilio: I assume if it works for web animations, making it work for gcs also makes sense
<fantasai> khush: I think flackr mentioned he preferred if we get these in sync
<vmpstr> q+
<fantasai> khush: also my ideal preference, but would it be ok for implementation complexity if we didn't do that for now, and if a use case do it right later?
<fantasai> flackr: if you don't do that, values would always be a frame out of date
<fantasai> flackr: wouldn't be able to use them
<fantasai> khush: answer for first frame is correct
<fantasai> khush: after UA does consruction
<fantasai> khush: It's only if you change as part of RAF as part of ??
<fantasai> khush: We don't retarget animations now anyway, so you'll get visual jumps
<fantasai> flackr: I've seen authors do things in RAF every frame, to manually position other elements in response to frames
<Rossen_> ack vmpstr
<fantasai> vmpstr: My preference is not keeping it always up to date, it's because there's a distinction between keeping styles conceptually up to date
<fantasai> vmpstr: but this isn't technically style, but UA style sheet that needs to be updated
<fantasai> vmpstr: ensuring that these values are kept up to date as style is unnecessary, other than point flackr raised
<fantasai> vmpstr: so I would preferred to have a defined step and update rendering, this is when the stylesheet is updated to reflect DOM changes
<fantasai> vmpstr: reason is, from implementation perspective, we would have to force style and layout to get this information to put in stylesheet and it's a lot more forced work
<fantasai> vmpstr: seems a lot more rendering needs to be forced in these cases
<Rossen_> fantasai: why not both?
<fantasai> s/both/allow both/
<Rossen_> ack fantasai
<dholbert> fantasai: text wrapping
<fantasai> fantasai: make it a quality of implementation issue
<emilio> q+
<fantasai> fantasai: and if it becomes a problem, people can file bugs against the implementations
<flackr> sgtm to leave it up to implementer
<fantasai> fantasai: to improve their fidelity
<fantasai> khush: I think for wording, use "should"
<flackr> This is also similar to the way reading scroll position may be more or less out of sync on diff browsers
<khush> UA styles on the pseudo-DOM should stay in sync with author DOM for any developer observable API
<fantasai> emilio: I think this is quite different from text-wrapping, it's an API you're asking a question
<fantasai> emilio: I think we can't do hard thing if we do the hard thing if we do the easy thing at the beginning?
<fantasai> emilio: people will rely on whatever ansewr you give them
<dholbert> fantasai: but if you give them more accurate answers, would that be a problem?
<fantasai> emilio: maybe relying on perf characteristics
<fantasai> emilio: if in a loop, and then changing DOM, implicitly relying on existing behavior or else stuff becomes super slow
<fantasai> emilio: maybe I'm being too pessimistic...
<fantasai> emilio: my preference would be to define one answer
<fantasai> Rossen_: more correct and harder way?
<fantasai> emilio: Don't particularly care. Do understand the implementation complexity. Also don't see a lot of use cases for this to begin with
<fantasai> Rossen_: I want us to move forward here, take a resolution that's going to unblock us quicker
<flackr> I suppose if we do the easy thing first we can always try to change it to the more correct thing and consider the compat risk then
<fantasai> Rossen_: going back to original proposal, that was for the easier one which we can get to basically go to market quicker
<fantasai> Rossen_: get implementation experience and feedback, that's initial ask?
<fantasai> khush: That would be nicer, that said my proposal was on the assumption that this would be easy to change later
<fantasai> khush: that is, if we make API more correct we would not have a compat risk
<fantasai> khush: but if we have a compat risk, then I'm ok to do the harder thing first
<fantasai> Rossen_: from point of view of API and its evolution, taking scoped resolution here doesn't preclude us doing better later
<fantasai> Rossen_: our decisions today are open to improvement tomorrow
<fantasai> khush: I didn't quite follow, what are we resolving on?
<fantasai> Rossen_: the easier one
<fantasai> khush: Emilio was concerned changing later would be bad for compat
<fantasai> emilio: I think so
<fantasai> emilio: since these are relatively expensive updates
<fantasai> emilio: it's very easy to depend on not doing hard work
<fantasai> Rossen_: If I'm hearing, you want to do harder work now now
<fantasai> emilio: I don't mind which we choose, as long as we choose one
<fantasai> khush: If we have to make one choice right now, and given uncertaintly about valid use case, we prefer developer ease over implementer ease so we do harder thing first
<fantasai> emilio: No strong opinion on which one
<fantasai> emilio: my point is, I think if we start with easy one, we can't switch to hard one
<fantasai> emilio: not leave it up to UA or future
<fantasai> emilio: let's just decide what we want
<fantasai> emilio: I've no strong opinion, but khush seems to think harder one is better for devs
<khush> UA styles on the pseudo-DOM stay in sync with author DOM for any developer observable API
<fantasai> khush: proposal ^
<fantasai> RESOLVED: UA styles on the pseudo-DOM stay in sync with author DOM for any developer observable API