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] First pass at layered capture #11045

Merged
merged 13 commits into from
Nov 5, 2024
190 changes: 180 additions & 10 deletions css-view-transitions-2/Overview.bs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ spec:css-view-transitions-1;
text: snapshot containing block; type: dfn;
text: old transform; for: captured element; type: dfn;
text: new element; for: captured element; type: dfn;
text: old width; for: captured element; type: dfn;
text: old height; for: captured element; type: dfn;
text: updateCallbackDone; type: property; for: ViewTransition;
text: phase; type: dfn; for: ViewTransition;
text: call the update callback; type: dfn;
Expand All @@ -46,6 +48,8 @@ spec:css-view-transitions-1;
text: group styles rule; type: dfn;
text: update pseudo-element styles rule; type: dfn;
text: document-scoped view transition name; type: dfn;
text: capture rendering characteristics; type: dfn;
text: capturing the image; type: dfn;
spec:dom; type:dfn; text:document
spec:css22; type:dfn; text:element
spec:selectors-4; type:dfn;
Expand All @@ -63,7 +67,23 @@ spec:geometry-1
text:multiply; type:dfn;
text:matrix; type:dfn;
spec:infra; type:dfn; text:list
spec:css-borders-4; type: property; text:border-radius;
spec:css-borders-4;
type: property; text:border-color;
type: property; text:border-radius;
type: property; text:box-shadow;
type: property; text:border-top;
type: property; text:border-right;
type: property; text:border-bottom;
type: property; text:border-left;
type: property; text:border-left-width;
type: property; text:border-top-width;
spec:css-sizing-3;
type: value; text:box-sizing;
type: value; text:border-box;
type: value; text:content-box;
spec:css-box-4;
type: dfn; text:padding box; for: /;
type: dfn; text:content box; for: /;
</pre>

<style>
Expand Down Expand Up @@ -885,6 +905,46 @@ and by applying ''view-transition-group'' to the internal element referencing th
The [=used value=] for 'view-transition-group' resolves to a 'view-transition-name' in its ancestor chain, or to ''view-transition-name/none''. When generating the [=named view transition pseudo-element=], the ''::view-transition-group()'' with that name
would be the parent of the ''::view-transition-group()'' generated for this element's 'view-transition-name'.

# Layered capture # {#layered-capture}

## Overview ## {#layered-capture-overview}

In [[css-view-transitions-1]], the old and new states are captured as snapshots, and the initial and final geometry are captured,
khushalsagar marked this conversation as resolved.
Show resolved Hide resolved
creating a crossfade animation by default. This is a simple model and mostly creates the desired outcome.

However, crossfading two flattened snapshots is not always the most expressive animation. CSS allows animating borders, gradient backgrounds, 'filter', 'box-shadow' etc.
which can feel more expressive and natural than crossfade depending on the desired UX.

In addition, when using <a href="#nested-view-transitions">nested view transitions</a>, some of the animations could look "wrong" when the CSS properties are flattened to a snapshot.
This includes tree effects, such as 'opacity', 'mask', 'clip-path' and 'filter', and also clipping using 'overflow'. These effects are designed to apply to the whole tree of elements, not just to one element and its content.

With layered capture, all the CSS properties that affect the entire tree, as well as box decorations, are captured as style and animate as part of the group.

Issue: should this behavior be an opt-in/opt-out with a CSS property? See <a href="https://github.com/w3c/csswg-drafts/issues/11078">issue 11078</a>.


## Table of captured CSS properties {#layered-captured-css-properties}

The following list of <dfn>layered capture properties</dfn> defines the CSS properties that participate in layered capture.
When the old or new state of the element are captured, these properties are captured as style, and the user agent must render the snapshot
with disregard to that property:

- 'background'
- 'border-left'
- 'border-top'
- 'border-bottom'
- 'border-right'
- 'border-radius'
- 'border-image'
- 'box-shadow'
- 'box-sizing'
- 'clip-path'
- 'filter'
- 'mask'
- 'opacity'
- 'outline'
- 'padding'

# Algorithms # {#algorithms}

## Data structures ## {#cross-doc-data-structures}
Expand Down Expand Up @@ -939,15 +999,32 @@ It has the following [=struct/items=]:

### Captured elements extension ### {#capture-classes-data-structure}
The [=captured element=] struct should contain these fields, in addition to the existing ones:
<dl>
: <dfn for="captured element">class list</dfn>
<dl dfn-for="captured element">
: <dfn>class list</dfn>
:: a [=/list=] of strings, initially empty.

: <dfn for="captured element">containing group name</dfn>
: <dfn>containing group name</dfn>
:: Null or a string, initially null.

: <dfn for="captured element">transform from snapshot containing block</dfn>
:: A [=matrix=], initially the identity matrix.
: <dfn>old layered-capture style</dfn>
khushalsagar marked this conversation as resolved.
Show resolved Hide resolved
:: A string.

: <dfn>transform from snapshot containing block</dfn>
:: A [=matrix=], initially the [=identity transform function=].

: <dfn>old box properties
: <dfn>new box properties
:: A [=layered box properties=] or null, initially null.
</dl>

The <dfn>layered box properties</dfn> is a struct, containing the following fields:
<dl dfn-for="layered box properties">
: <dfn>box sizing</dfn>
:: ''box-sizing/border-box'' or ''box-sizing/content-box''.

: <dfn>content box</dfn>
: <dfn>padding box</dfn>
:: A rectangle, in CSS pixel units, relative to the border box.
</dl>

## Resolving the ''@view-transition'' rule ## {#vt-rule-algo}
Expand Down Expand Up @@ -1237,25 +1314,118 @@ When [[css-view-transitions-1#setup-transition-pseudo-elements-algorithm|setting

1. Append |group| to |parentGroup|.

1. When setting the animation keyframes given |transform|, [=multiply=] |transform| by the inverse matrix of |groupContainerElement|'s [=old transform=].
1. When setting the animation keyframes given |transform|, [=adjust the nested group transform=] to |transform|, given |groupContainerElement|'s [=old transform=], |groupCotnainerElement|'s [=captured element/old width=], |groupCotnainerElement|'s [=old height=], and |groupContainerElement|'s [=old box properties=].
noamr marked this conversation as resolved.
Show resolved Hide resolved

Note: It is TBD how this is resolved when the old and new groups mismatch. See <a href="https://github.com/w3c/csswg-drafts/issues/10631">Issue 10631</a>.
</div>

### Adjust the group's 'transform' to be relative to its containing ''::view-transition-group()'' ### {#vt-group-transform-adjust}
<div algorithm="additional pseudo-element style update steps (group)">
When [[css-view-transitions-1#style-transition-pseudo-elements-algorithm|updating the style of the transition pseudo-element]], perform the following steps before setting the [=captured element/group styles rule=], given a [=captured element=] |capturedElement|, given a |transform| and a {{ViewTransition}} |transition|:
When [[css-view-transitions-1#style-transition-pseudo-elements-algorithm|updating the style of the transition pseudo-element]], perform the following steps before setting the [=captured element/group styles rule=], given a [=captured element=] |capturedElement|, a |transform|, and a {{ViewTransition}} |transition|:

1. Set |capturedElement|'s [=transform from snapshot containing block=] to |transform|.

1. If |capturedElement|'s [=containing group name=] is not null, then:

1. Let |groupContainerElement| be |transition|'s [=ViewTransition/named elements=][|capturedElement|'s [=containing group name=]].
1. Let |groupContainerElement| be |transition|'s [=ViewTransition/named elements=][|capturedElement|'s [=containing group name=].

1. Let |containerRect| be [=snapshot containing block=] if |capturedElement| is the [=document element=],
otherwise, |capturedElement|'s [=border box=].

1. [=Adjust the nested group transform=] to |transform|, given |groupContainerElement|'s [=transform from snapshot containing block=], |containerRect|'s width |containerRect|s height, and |groupContainerElement|'s [=new box properties=].</div>
noamr marked this conversation as resolved.
Show resolved Hide resolved

## Capturing layered CSS properties ## {#layered-capture-algorithms}

### Compute the layered-capture style ### {#layered-capture-compute-style}

1. [=Multiply=] |transform| by the inverse matrix of |groupContainerElement|'s [=transform from snapshot containing block=].
<div algorithm="compute layered capture style">
To compute the <dfn>layered capture style</dfn> of an {{Element}} |element|:

1. Let |propertiesToCapture| be a new [=/list=] corresponding to the [=layered capture properties=],
1. Issue: Specify the behavior of 'overflow' and containment. See <a href="https://github.com/w3c/csswg-drafts/issues/11079">issue 11079</a>.
1. Let |styles| be a « ».
1. For each |property| in |propertiesToCapture|, [=list/append=] the [=string/concatenate|concatentation=] of « |property|, `":"`, |element|'s [=computed value=] of |property|, `";"` » to |styles|.
1. Return the [=string/concatenate|concatentation=] of |styles|.
</div>

<div algorithm="compute layered capture geometry">
To compute the <dfn>layered capture geometry</dfn> of an {{Element}} |element|, return a new [=layered box properties=],
whose [=layered box properties/box sizing=] is |element|'s [=computed value|computed=] 'box-sizing',
[=layered box properties/padding box=] is |element|'s [=/padding box=],
and whose [=layered box properties/content box=] is |element|'s [=/content box=].
</div>

### Capture the old layered properties ### {#capture-new-layered-props-algorithm}
noamr marked this conversation as resolved.
Show resolved Hide resolved
<div algorithm="additional old capture steps (layered)">
When [[css-view-transitions-1#capture-new-state-algorithm|capturing the old state for an element]], perform the following steps given a [=captured element=] |capturedElement| and an [=element=] |element|:

1. Set |capturedElement|'s [=old layered-capture style=] to |element|'s [=layered capture style=].
1. Set |capturedElement|'s [=old box properties=] to |element|'s [=layered capture geometry=].
</div>

### Adjustment to image capture size ### {#capture-image-size-algorithm}
<div algorithm="adjust image capture size (layered)">
When [=capturing the image=] given |element|:
1. Let |geometry| be |element|'s [=layered capture geometry=].
1. If |geometry| is not null and |geometry|'s [=layered box properties/box sizing=] is ''box-sizing/content-box'',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The snapshot capture is independent of box-sizing, box-sizing only impacts the CSS of the pseudos. But the snapshot would always be content box size because there's no reason to capture borders as a part of it. So it wouldn't make sense for the snapshot to be larger than the element's content.

You can fold this into the "layered-capture-rendering" algorithm below.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this is the spot in L1 where we define the natural dimension of the image and how it's positioned...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh ok, my main point was about not making this conditional based on box-sizing. Does that make sense?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

then the [=natural dimensions=] of the image will be based on |geometry|'s [=layered box properties/content box=]'s size rather than on the [=border box=].
</div>

### Render the snapshot with layered capture ### {#layered-capture-rendering}
khushalsagar marked this conversation as resolved.
Show resolved Hide resolved

When capturing an element into a snapshot, only the [=element contents=] are painted, without the element's effects and box decorations.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would love to get @vmpstr's input on this phrasing too.

Taking inspiration from the wording in L1: https://drafts.csswg.org/css-view-transitions-1/#capture-the-image-algorithm:~:text=containing%20block.-,Otherwise,-%3A.

  • We want to say what is painted, which is covered here.
  • We want to say what the image size is internally in the UA which I think would be the union of ink overflow rectangles of all the descendants + size of replaced content..?
  • And we want to define the web observable size of the image which is covered above as content box.

Specifically, the element's 'background', 'border', 'border-image', 'box-shadow', and 'outline' are not painted, and its 'border-radius', 'clip-path', 'filter', 'mask', and 'opacity' are not applied.

### Adjust the nested group transform ### {#vt-adjust-nested-group-transform}
To <dfn>adjust the nested group transform</dfn> given a [=matrix=] |transform|, a [=matrix=] |parentTransform|, |borderBoxWidth|, |borderBoxHeight|, and a [=layered box properties=] |boxProperties|, perform the following steps:
khushalsagar marked this conversation as resolved.
Show resolved Hide resolved
1. [=Multiply=] |transform| with the inverse matrix of |parentTransform|.
1. Let (left, top) be the border edge based on |boxProperties|'s [=layered box properties/padding box=] inside (|borderBoxWidth|, |borderBoxHeight|).
1. Translate |transform| by (-left, -top).

Note: These operations ensure that the default position of this nested group would be equivalent to the original element's position, given that by default a nested ':view-transition-group' would be positioned relative to the parent's [=padding edge=].

### Apply the old layered-capture properties to ''::view-transition-group()'' keyframes ### {#vt-layered-capture-apply-keyframes}
<div algorithm="additional pseudo-element setup steps (layered capture keyframes)">
When [[css-view-transitions-1#setup-transition-pseudo-elements-algorithm|setting up the transition pseudo-element]] for a [=captured element=] |capturedElement|, given a |transitionName|:
1. Let |keyframesName| be the [=string/concatenate|concatentation=] of « `-ua-view-transition-group-anim-`, |transitionName| »
1. Append |capturedElement|'s [=old layered-capture style=] to the constructed rule for |keyframesName|.
</div>

### Apply the new layered-capture properties to ''::view-transition-group()'' ### {#vt-layered-capture-apply-new-state}
<div algorithm="additional pseudo-element style update steps (layered capture new state)">
When [[css-view-transitions-1#style-transition-pseudo-elements-algorithm|updating the style of the transition pseudo-element]], perform the following steps before setting the [=captured element/group styles rule=], given a |transitionName|, a |capturedElement|, |width|, |height|, and an {{Element}} |element|:

1. Let |style| be |element|'s [=layered capture style=].
1. Set |capturedElement|'s [=new box properties=] to |element|'s [=layered capture properties=].
1. Append the [=string/concatenate|concatentation=] of « `"::view-transition-group("`, |transitionName|, `") {"`, |style| , `"}"` » to the constructed user-agent stylesheet.
1. Let (|oldContentWidth|, |oldContentHeight|) be (|capturedElement|'s [=captured element/old width=], |capturedElement|'s [=captured element/old height=]) if |capturedElement|'s [=old box properties=] is null, otherwise |capturedElement|'s [=old box properties=]'s [=layered box properties/content box=]'s size.
1. Let (|newContentWidth|, |newContentHeight|) be (|width|, |height|) if [=new box properties=] is null, otherwise |capturedElement|'s [=new box properties=]'s [=layered box properties/content box=]'s size.
1. Append the next string (with replaced variables) to the user agent stylesheet:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://chromium-review.googlesource.com/c/chromium/src/+/5953705 is appending the extra styles for image-pair only if layered capture is in use. Did you mean to do the same here as well? No need to override the existing styles in image-pair in flat capture mode.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, but we haven't resolved yet on "when are we in capture mode". So currently the capture mode spec changes assume that we're in that mode.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm ok. Then maybe a link to the issue saying this is tentative is good enough.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's already there in the overview...


<pre highlight="css">
@keyframes -ua-view-transition-content-geometry-<var>transitionName</var> {
from {
width: <var>oldContentWidth</var>;
height: <var>oldContentHeight</var>;
}
}

:root::view-transition-image-pair(<var>transitionName</var>) {
position: relative;
inset: unset;
width: <var>newContentWidth</var>;
height: <var>newContentHeight</var>;
animation-name: -ua-view-transition-<var>transitionName</var>;
animation-direction: inherit;
animation-timing-function: inherit;
animation-iteration-count: inherit;
animation-duration: inherit;
}
</pre>

Note: the ':view-transition-image-pair' pseudo-element is using ''position/relative'' positioning so that the group's 'padding' will take effect.

</div>

<h2 id="priv" class="no-num">Privacy Considerations</h2>

Expand Down