Skip to content

Commit

Permalink
[css-view-transitions-2] First pass at layered capture (w3c#11045)
Browse files Browse the repository at this point in the history
* [css-view-transitions-2] First pass at layered capture

Describe how layered capture works, which CSS properties it targets and how it affects rendering.
Based on [this resolution](w3c#10585 (comment)).

This is a first pass, and still has open issues (will open separately):
1. Should there be a CSS property to decide on this? What should be the default?
2. How do we capture overflow/box-sizing?
3. (More capture questions if they come up)

Closes w3c#10585

* Adjust border

* Adjust nested transform by border

* Add note about border-box

* Update css-view-transitions-2/Overview.bs

Co-authored-by: Khushal Sagar <[email protected]>

* Update css-view-transitions-2/Overview.bs

Co-authored-by: Khushal Sagar <[email protected]>

* Refactor a few things

* note

* Overhaul nested geometry

* Fold padding back into regular props

* Apply suggestions from code review

Co-authored-by: Khushal Sagar <[email protected]>

* Update Overview.bs

* Apply box-sizing in the right place

---------

Co-authored-by: Khushal Sagar <[email protected]>
  • Loading branch information
noamr and khushalsagar authored Nov 5, 2024
1 parent ed7566f commit 02eb632
Showing 1 changed file with 186 additions and 10 deletions.
196 changes: 186 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,48 @@ 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}

*This section is non-normative.*

In [[css-view-transitions-1]], the old and new states are captured as snapshots, and the initial and final geometry are captured,
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 +1001,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>
:: 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 @@ -1239,25 +1318,122 @@ 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=], |groupContainerElement|'s [=captured element/old width=], |groupContainerElement|'s [=old height=], and |groupContainerElement|'s [=old box properties=].

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>

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

To compute the <dfn>default group size</dfn> given |element|
return |element|'s [=/content box=]'s size if |element|'s [=computed value|computed=] 'box-sizing' is ''box-sizing/content-box'', otherwise |element|'s [=/border box=]'s size.

### Capture the old layered properties ### {#capture-new-layered-props-algorithm}
<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=].
1. Set |capturedElement|'s ([=old width=], [=old height=] to |element|'s [=default group size=].
</div>

### Adjustment to image capture size ### {#capture-image-size-algorithm}
<div algorithm="adjust image capture size (layered)">
When [=capturing the image=], the [=natural dimensions=] of the image will be based on the element's [=/content box=]'s size rather than on the [=border box=].
</div>

### Render the snapshot with layered capture ### {#layered-capture-rendering}

When capturing an element into a snapshot, only the [=element contents=] are painted, without the element's effects and box decorations.
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:
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|:

Note: this might change and |width|, |height|.

1. Let |style| be |element|'s [=layered capture style=].
1. Set |capturedElement|'s [=new box properties=] to |element|'s [=layered capture properties=].
1. Set (|width|, |height|) to the |element|'s [=default group size=].
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:

<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

0 comments on commit 02eb632

Please sign in to comment.