Skip to content

Commit

Permalink
improved hide-and-seek
Browse files Browse the repository at this point in the history
  • Loading branch information
martrapp committed Nov 19, 2024
1 parent 78efb9a commit f227b2b
Show file tree
Hide file tree
Showing 16 changed files with 349 additions and 145 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
dist/
# generated types
.astro/

.vscode/
# dependencies
node_modules/
tmp
Expand Down
7 changes: 6 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
{
"svg.preview.background": "editor"
"svg.preview.background": "editor",
"cSpell.words": [
"onpagereveal",
"pagereveal",
"pageswap"
]
}
20 changes: 20 additions & 0 deletions bin/og.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
#! /bin/bash


subtract_lists() {
echo "$1" | tr ' ' '\n' | grep -Fxv -f <(echo "$2"|tr ' ' '\n')
}

htmls=$(find dist -type f -name "*.html" | sort)

desc=$(grep -l '"og:description"' $htmls)
no_desc=$(subtract_lists "$htmls" "$desc")

echo $no_desc
exit
done=$(grep -o '"og:image" content="[^"]*"' $htmls|grep -v "https://vtbag.dev/social.png" | col1 :)

done+=" dist/index.html"
done=$(echo "$done" | tr ' ' '\n' | sort)
todo=$(subtract_lists "$htmls" "$done")
echo $todo
exit

find dist -type f -name "*.html" -exec grep -H -o '"og:url" content="[^"]*"' {} \; | sed -e 's/:/ /' -e 's/=/ /' -e 's/"//g' -e 's|og:url content https://vtbag.dev||' > pages

awk < pages '{print "test(\"" $2 "\", async ({ page }) => {await shoot(page, \"" $2 "\")});"; system("mkdir -p public/" $2);}' | sort
Expand Down
10 changes: 10 additions & 0 deletions src/components/NHead.astro
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ import "./vtbag-bar.css";
:active-view-transition-type(same) main {
view-transition-name: none;
}

main h1 {
view-transition-name: title-heading;
}

::view-transition-old(main) {
animation: forwardsSwingOut 500ms ease-in-out;
}
Expand Down Expand Up @@ -116,19 +121,24 @@ import "./vtbag-bar.css";
@keyframes shake-old {
to {
transform: scale(25);
filter: blur(10px);
opacity: 0;
}
}
@keyframes shake-new {
from {
opacity: 0;
transform: scale(0);
filter: blur(10px);
}
}
::view-transition-image-pair(main) {
overflow: hidden;
}

sl-sidebar-state-persist {
view-transition-name: sl-sidebar;
}
header.header {
view-transition-name: header;
}
Expand Down
37 changes: 37 additions & 0 deletions src/components/cutter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export function cutSVG(element: Element, cutters:{element:HTMLElement,inset: string}[]) {
if (!element) return;
const masks = ['<rect width="100%" height="100%" fill="white" />'];
const outerRect = element.getBoundingClientRect();
cutters.forEach((cutter) => {
const inner = cutter.element;
const insets = cutter.inset.split(" ").map(parseFloat);
for (let i = 0; i < 4; i++) {
const len = insets.length;
insets[i] = isNaN(insets[i] ?? 0)
? 0
: (insets[len === 3 && i === 3 ? 2 : i % len] ?? 0);
}
const innerRect = inner.getBoundingClientRect();
const innerStyle = getComputedStyle(inner);

const left = parseFloat(innerStyle.borderLeftWidth);
innerRect.width -=
left +
parseFloat(innerStyle.borderRightWidth) +
insets[1]! +
insets[3]!;
innerRect.x += left + insets[3]!;
const top = parseFloat(innerStyle.borderTopWidth);
innerRect.height -=
top +
parseFloat(innerStyle.borderBottomWidth) +
insets[0]! +
insets[2]!;
innerRect.y += top + insets[0]!;
masks.push(
`<rect x="${innerRect.x - outerRect.x}" y="${innerRect.y - outerRect.y}" width="${innerRect.width}" height="${innerRect.height}" fill="black" />`
);
});
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${outerRect.width}" height="${outerRect.height}">${masks.join("")}</svg>`;
return svg;
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
86 changes: 67 additions & 19 deletions src/content/docs/basics/hide-and-seek/index.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Hide and Seek
description: Making the most of view transitions while awaiting nested groups
description: How to render elements above and beneath of view transition images.
head:
- tag: meta
attrs:
Expand All @@ -24,7 +24,6 @@ This site features both subtle and striking examples. I’ll guide you through s

This is built with [Astro Starlight](https://starlight.astro.build/). Enabling cross-document view transitions for multi-page sites is surprisingly straightforward. [Most browsers](https://caniuse.com/?search=%40view-transition) support this feature; all you need to do is [add a simple CSS at-rule](/tips/css/) to your custom styles:


```css
@view-transition {
navigation: auto;
Expand All @@ -41,39 +40,46 @@ main {

Here is an interesting quirk: Scroll down a bit so part of the page content moves beneath the fixed search box at the top of the page.


<div class="small">
<span class="light:sl-hidden">![Searchbox hides main area](./_hidden-dark.png)</span>
<span class="dark:sl-hidden">![Searchbox hides main area](./_hidden-light.png)</span>
<span class="light:sl-hidden">
![Searchbox hides main area](./_hidden-dark.png)
</span>
<span class="dark:sl-hidden">
![Searchbox hides main area](./_hidden-light.png)
</span>
</div>

Now navigate to a different page. During the view transition, the `::view-transition-group(main)` pseudo-element, along with its old and new images, enters the view transition layer. Since these images extend taller than the viewport, they obscure the search box momentarily.



<div class="small">
<span class="light:sl-hidden">![View transition image hides searchboxV](./_unhidden-dark.png)</span>
<span class="dark:sl-hidden">![iew transition image hides searchbox](./_unhidden-light.png)</span>
<span class="light:sl-hidden">
![View transition image hides searchbox](./_unhidden-dark.png)
</span>
<span class="dark:sl-hidden">
![iew transition image hides searchbox](./_unhidden-light.png)
</span>
</div>

<style>{`
div.small img {
div.small img,
div.large img {
border:#888 solid 1pt;
box-shadow: 1px 1px 5px #8884;
}
div.small img {
margin-top: 1ex;
height: 100px;
object-fit: cover;
}
div.small {
width: max(250px, 50%);
width: max(250px, 67%);
margin-inline: auto;
}
`}</style>

As a result, the search box seems to vanish during the transition and reappear afterward. While this behavior isn't a big deal, it can feel a bit jarring.

To make the main are move behind the searchbox during view transitions, we have to paint the searchbox above `::view-transition-*(main)` images. As no normal DOM element can be painted above the view transition pseudo-elements, we have to define view transition images for the searchbox (or one of its descendants). That way we force an image of the searchbox into the view transition layer where it can cover the image of the main area. For our Starlight site we do this like this:

To prevent this, we need the search box to visually sit above the `::view-transition-group(main)` images during transitions. However, regular DOM elements cannot render above view transition pseudo-elements. The solution? Assign a view transition name to the searchbox (or one of its parents), forcing it to generate its own images in the view transition layer. These images can then visually cover the main area images.
To prevent this, we need the search box to visually sit above the `::view-transition-group(main)` images during transitions. However, regular DOM elements cannot render above view transition pseudo-elements. The solution? Assign a view transition name to the searchbox (or one of its parents), forcing it to generate its own images in the view transition layer. These images can then visually cover the main area images. For our Starlight site we do this like this:

```css
header.header {
Expand All @@ -83,17 +89,59 @@ mobile-starlight-toc nav {
view-transition-name: mobile-starlight-toc-nav;
}
```

The first rule targets the header which holds the searchbox. The second line targets the page navigation of the mobile view.

With this setup, the search box gracefully stays visible during transitions, ensuring a seamless and polished user experience.
The paint order for old images in the view transition layer mirrors the paint order of their corresponding elements in the DOM. Images exclusive to the new page [are painted after that](/basics/pseudos/#rendering-pseudo-elements). If you need precise control over the stacking order within the view transition layer, you can adjust it using the `z-index` CSS property.



## Clipping Images at other Pseudo Elements

Another powerful technique is clipping view transition images at their image pair or transition group. This website defines a sliding view transition animation for the main content. Without additional refinements, the content would overlap the sidebar on the left and the in-page navigation on the right:

<div class="large">
<span class="light:sl-hidden">
![View transition images for main content overlap with sidebars](./_main-unhidden-dark.png)
</span>
<span class="dark:sl-hidden">
![View transition images for main content overlap with sidebars](./_main-unhidden-light.png)
</span>
</div>


Here’s a polished version of the text:

This overlap can be avoided by clipping the old and new images at the edges of their image pair (or at the boundary of their transition group, which has the same position and dimensions):
```css
::view-transition-image-pair(main) {
overflow: hidden;
}
```

Activating the Inspection Chamber (and temporarily disabling most of the view transition images for clarity) reveals that the visible portions of the main content are now confined within the dotted yellow box, which represents the boundary of the `::view-transition-group(main)` respectively the `::view-transition-image-pair(main)`. The panel on the left confirms that overflow is hidden for the image pair.

Since the main content areas occupy the same position on both the old and new pages, the browser's default animation for the `main` transition group has no effect during the transition. The group doesn't morph or move, it remains stationary. The combined effect with the clipping is that the view transition of the main content section seems to slide below the sidebars.



<div class="large">
<span class="light:sl-hidden">
![View transition images for main content get clipped to their original position](./_main-hidden-dark.png)
</span>
<span class="dark:sl-hidden">
![View transition images for main content get clipped to their original position](./_main-hidden-light.png)
</span>
</div>

As of today, the transition groups form a flat list of children of the `::view-transition` pseudo element. Nested view transition groups are already defined in the [Level 2 spec](/basics/api/#w3c-drafts), but to my knowledge they are [not yet supported](/basics/test-page/) by any browser. With nested transition groups you will be able clip images not only at their own group but can also specify the clipping on the parent or another ancestor further up the tree.

Nested transition groups will introduce the ability to clip images not only within their own group but also using their parent or any ancestor further up the hierarchy.

## Clipping Images at the Transition-Group

tbd.
## Simulate

## Arbitrary Clipping

tbd.

### Naive Scrollable List

Expand Down
12 changes: 6 additions & 6 deletions src/content/docs/basics/pseudos.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ With the one to one relation between groups and image pairs, the group looks a b



## Creation of Pseudo-Elements
## Creation of Pseudo-Elements

The `::view-transition-old` pseudo elements are created in a pre-order depth first traversal of the old DOM. When an element with a `view-transition-name` is encountered, its old image is captured. During this process, other elements with view transition names are effectively ignored, as if their `visibility` were set to `hidden`. This ensures that the images have "holes" where other images can be properly placed.

Expand Down Expand Up @@ -279,11 +279,11 @@ When examining the `::view-transition-groups` in order, the elements at the star

## Rendering Pseudo-Elements

Before the view transition starts, the pseudo elements are rendered on top of the current page. For cross-document view transitions, this might start in the middle of the page load process. But there are [techniques to delay the start of the view transition](#delaying-cross-document-transitions).
Before the view transition starts, the pseudo elements are rendered on top of the current page.

The first pseudo-element that is rendered for a view transition is `::view-transition`. It has the size of the viewport and is full transparent by default. It works like a glass pane that prevents the user from interacting with elements below it (as long as you don't set `pointer-events: none;` on it).
The first pseudo-element that is rendered for a view transition is `::view-transition`. It has the size of the viewport and is full transparent by default. It works like a glass pane that prevents the user from interacting with elements below it (as long as you don't set `pointer-events: none` on it).

The transition groups are rendered in the order they appear as children of the `::view-transition` pseudo-element. Groups at the beginning of the list are rendered first, with later groups painted on top.
The transition groups are rendered [in the order they were created](#creation-of-pseudo-elements) and appear as children of the `::view-transition` pseudo-element. Groups at the beginning of the list are rendered first, with later groups painted on top.

There is a clear separation between regular DOM elements and the view transition pseudo-elements. The pseudo-elements form their own stacking context, known as the _view transition layer_. This one is special as it sits above all other elements of the page. As with regular elements, the `z-index` property can be used to alter how the pseudo-elements hide each other. However, no regular page elements can be forced into or above the view transition layer, even with high `z-index` values.

Expand All @@ -296,12 +296,12 @@ Just as pseudo-elements cannot be moved behind page elements, elements on the pa

## Delaying Cross Document Transitions

With cross-document transitions the browser has to guess what would be a good time to start the view transition. It has do be able to load at least the html with the elements that should participate in a view transition. And it definitively needs to load the stylesheets that describe the view transition. But it would be a bad user experience if it would wait for a long page to load completely and await all scripts to run before the transition starts.
With cross-document transitions the browser has to guess what would be a good time to start the animaitons of the view transition. It needs to load at least the HTML with the elements that should participate in a view transition. And it definitively needs to load the stylesheets that describe the view transition. But it would be a bad user experience if it would wait for a long page to load completely and await all scripts to run before the transition starts.

Start of view transitions will always await the `<head>` to be loaded. External stylesheets inside the head will also be awaited for. If you want to make sure that view transitions wait for an external script, add the `blocking="render"` attribute to the script element. You can also instruct view transitions to wait until some fragment URL of the current page is loaded by adding a link to the `<head>` like this:
```html
<link rel="expect" href="#somewhere" blocking="render"/>
```

haven't yet been able to wait for an image to load, but the View Transition API points toward a different approach. [Example 5](https://drafts.csswg.org/css-view-transitions-2/#cross-doc-examples) suggests checking if an image has fully loaded, and if not, modifying the view transitions to handle the situation rather than waiting for the image to load.
I haven't yet been able to wait for an image to load, but the View Transition API points toward a different approach. [Example 5](https://drafts.csswg.org/css-view-transitions-2/#cross-doc-examples) suggests checking if an image has fully loaded, and if not, modifying the view transitions to handle the situation rather than waiting for the image to load.

2 changes: 1 addition & 1 deletion src/pages/demo-explainer/FailIM.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ The HTML of this example is very simple:
The view transition swaps the two DOMs, each containing a background div and an image.


The key to understanding this example lies in the order in which the `::view-transition-group` pseudo-elements [are created](/basics/pseudos/#creation--of-pseudo-elements) by the API and [hide each other](/basics/pseudos/#rendering-pseudo-elements).
The key to understanding this example lies in the order in which the `::view-transition-group` pseudo-elements [are created](/basics/pseudos/#creation-of-pseudo-elements) by the API and [hide each other](/basics/pseudos/#rendering-pseudo-elements).


When snapshots of the new images are taken in the new DOM, the `::view-transition-new` pseudo-elements are added to existing transition groups based on their transition names. These groups either already exist if there is an old image with the same name or they are created created on the fly. Elements that only exist in the new DOM will create new groups at the end of the list. When rendered, these late additions may obscure other images.
Expand Down
Loading

0 comments on commit f227b2b

Please sign in to comment.