Skip to content

Commit 18d06fd

Browse files
[css-view-transitions-1] Expanded intro section (w3c#8337)
1 parent 5375984 commit 18d06fd

File tree

12 files changed

+833
-149
lines changed

12 files changed

+833
-149
lines changed

css-view-transitions-1/Overview.bs

Lines changed: 450 additions & 149 deletions
Large diffs are not rendered by default.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<!DOCTYPE html>
2+
<style>
3+
html {
4+
font-family: sans-serif;
5+
}
6+
body {
7+
margin: 0;
8+
}
9+
.stage {
10+
width: 1920px;
11+
height: 1080px;
12+
background: #fff;
13+
position: relative;
14+
}
15+
code {
16+
font-family: Courier, monospace;
17+
}
18+
spec-slide {
19+
position: absolute;
20+
inset: 0;
21+
}
22+
.vt-demo {
23+
position: absolute;
24+
inset: 25px 0;
25+
display: grid;
26+
grid-template-columns: 1fr 1fr 1fr;
27+
grid-template-rows: 1fr 95px;
28+
gap: 80px 60px;
29+
color: #000;
30+
font: normal 68px sans-serif;
31+
text-align: center;
32+
}
33+
34+
.step {
35+
grid-column-end: span 3;
36+
contain: layout;
37+
}
38+
39+
.example {
40+
display: grid;
41+
grid-template-rows: max-content 1fr;
42+
gap: 37px;
43+
}
44+
45+
.page {
46+
position: relative;
47+
border: 7px solid #000;
48+
}
49+
50+
.states {
51+
display: grid;
52+
isolation: isolate;
53+
position: absolute;
54+
top: 30px;
55+
left: 30px;
56+
}
57+
58+
.states .state-2 {
59+
opacity: 0;
60+
transform: none;
61+
}
62+
63+
.state-1,
64+
.state-2 {
65+
background: green;
66+
color: white;
67+
padding: 20px 50px;
68+
border-radius: 50px;
69+
position: absolute;
70+
top: 30px;
71+
left: 30px;
72+
width: 200px;
73+
contain: layout;
74+
font-size: 56px;
75+
}
76+
77+
.state-2 {
78+
background: orange;
79+
color: black;
80+
transform: translate(219px, 469px);
81+
}
82+
83+
.states > * {
84+
grid-area: 1 / 1;
85+
mix-blend-mode: plus-lighter;
86+
position: relative;
87+
top: 0;
88+
left: 0;
89+
}
90+
91+
.what-user-sees {
92+
position: absolute;
93+
inset: auto 0 35px;
94+
font-size: 53px;
95+
}
96+
</style>
97+
<script type="module" src="script.js"></script>
98+
<div class="stage"></div>
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { Slide, transition, transitionFrom } from "../resources/slides.js";
2+
3+
const slide = new Slide(async function* () {
4+
slide.innerHTML = `
5+
<div class="vt-demo">
6+
<div class="example" aria-hidden="true">
7+
<div class="title">Main DOM</div>
8+
<div class="page">
9+
<div class="state-1">State 1</div>
10+
</div>
11+
</div>
12+
<div class="example" aria-hidden="true">
13+
<div class="title">Transition root</div>
14+
<div class="page transition-page"></div>
15+
</div>
16+
<div class="example" aria-hidden="true">
17+
<div class="title">User sees</div>
18+
<div class="page combined-page">
19+
<div class="states">
20+
<div class="state-1">State 1</div>
21+
<div class="state-2">State 2</div>
22+
</div>
23+
<div class="what-user-sees">(Main DOM)</div>
24+
</div>
25+
</div>
26+
<div class="step" aria-live="polite">Developer calls <code>document.startViewTransition()</code></div>
27+
</div>
28+
`;
29+
30+
/** @type {HTMLElement[]} */
31+
const [domPage, transitionPage, combinedPage] =
32+
slide.querySelectorAll(".page");
33+
34+
/** @type {HTMLElement} */
35+
const whatUserSees = slide.querySelector(".what-user-sees");
36+
37+
// This pauses the slide until 'next' is clicked.
38+
yield;
39+
40+
/** @type {HTMLElement} */
41+
const step = slide.querySelector(".step");
42+
step.textContent = `Current state captured as the "old" state`;
43+
44+
yield;
45+
46+
step.textContent = "Rendering paused";
47+
whatUserSees.textContent = "(Paused render)";
48+
49+
yield;
50+
51+
step.textContent = "Developer updates document state";
52+
domPage.innerHTML = `<div class="state-2">State 2</div>`;
53+
54+
yield;
55+
56+
step.textContent = `Current state captured as the "new" state`;
57+
58+
yield;
59+
60+
step.textContent = "Transition pseudo-elements created";
61+
transitionPage.innerHTML = `
62+
<div class="states">
63+
<div class="state-1">State 1</div>
64+
<div class="state-2">State 2</div>
65+
</div>
66+
`;
67+
68+
yield;
69+
70+
step.textContent =
71+
"Rendering unpaused, revealing the transition pseudo-elements";
72+
whatUserSees.textContent = "(Transition root)";
73+
74+
yield;
75+
76+
step.textContent = "Pseudo-elements animate";
77+
78+
// Wow, this would be way easier with view transitions…
79+
const states = [transitionPage, combinedPage].map((el) =>
80+
el.querySelector(".states")
81+
);
82+
const state1s = [transitionPage, combinedPage].map((el) =>
83+
el.querySelector(".state-1")
84+
);
85+
const state2s = [transitionPage, combinedPage].map((el) =>
86+
el.querySelector(".state-2")
87+
);
88+
89+
for (const state of states) {
90+
transition(
91+
state,
92+
{ transform: "translate(219px, 469px)" },
93+
{
94+
duration: 1000,
95+
easing: "ease-in-out",
96+
}
97+
);
98+
}
99+
100+
for (const state1 of state1s) {
101+
transition(
102+
state1,
103+
{ opacity: "0" },
104+
{
105+
duration: 1000,
106+
easing: "ease-in-out",
107+
}
108+
);
109+
}
110+
111+
for (const state2 of state2s) {
112+
transition(
113+
state2,
114+
{ opacity: "1" },
115+
{
116+
duration: 1000,
117+
easing: "ease-in-out",
118+
}
119+
);
120+
}
121+
122+
yield;
123+
124+
step.textContent = "Transition pseudo-elements removed";
125+
transitionPage.innerHTML = "";
126+
whatUserSees.textContent = "(Main DOM)";
127+
});
128+
129+
document.querySelector(".stage").append(slide);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
export class Scaler extends HTMLElement {
2+
static observedAttributes = ["canvaswidth", "canvasheight"];
3+
4+
#shadowRoot;
5+
#scaledElement;
6+
#contentElement;
7+
8+
attributeChangedCallback(name, oldValue, newValue) {
9+
const width = Number(this.getAttribute("canvaswidth"));
10+
const height = Number(this.getAttribute("canvasheight"));
11+
12+
this.#contentElement.style.aspectRatio = `${width} / ${height}`;
13+
this.#scaledElement.style.width = `${width}px`;
14+
this.#scaledElement.style.height = `${height}px`;
15+
}
16+
17+
constructor() {
18+
super();
19+
this.#shadowRoot = this.attachShadow({ mode: "closed" });
20+
this.#shadowRoot.innerHTML = `
21+
<style>
22+
.content {
23+
contain: strict;
24+
overflow: hidden;
25+
}
26+
.scaled {
27+
transform-origin: 0 0;
28+
}
29+
</style>
30+
<div class="content">
31+
<div class="scaled"><slot></slot></div>
32+
</div>
33+
`;
34+
this.#scaledElement = this.#shadowRoot.querySelector(".scaled");
35+
this.#contentElement = this.#shadowRoot.querySelector(".content");
36+
37+
new ResizeObserver(([entry]) => {
38+
this.#scaledElement.style.transform = `scale(${
39+
entry.contentRect.width / Number(this.getAttribute("canvaswidth"))
40+
})`;
41+
}).observe(this.#contentElement);
42+
}
43+
}
44+
45+
customElements.define("spec-scaler", Scaler);
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
export class Slide extends HTMLElement {
2+
#slideFunction;
3+
#slideIterator;
4+
#currentState = -1;
5+
#queueChain = Promise.resolve();
6+
#done = false;
7+
8+
useTransitions = false;
9+
10+
constructor(slideFunction = async function* () {}) {
11+
super();
12+
this.#slideFunction = slideFunction;
13+
this.goto(0);
14+
}
15+
16+
async #unqueuedGoto(targetState) {
17+
this.innerHTML = "";
18+
19+
this.#done = false;
20+
this.#slideIterator = this.#slideFunction(this);
21+
this.#currentState = -1;
22+
23+
while (this.#currentState !== targetState) {
24+
await this.#advance({ useTransitions: false });
25+
if (!this.hasNext) return;
26+
}
27+
}
28+
29+
#queue(callback) {
30+
return (this.#queueChain = this.#queueChain.finally(callback));
31+
}
32+
33+
goto(targetState) {
34+
return this.#queue(() => this.#unqueuedGoto(targetState));
35+
}
36+
37+
async #advance({ useTransitions }) {
38+
if (this.#done) return;
39+
this.useTransitions = useTransitions;
40+
41+
this.#currentState++;
42+
const { done } = await this.#slideIterator.next();
43+
this.#done = done;
44+
}
45+
46+
previous() {
47+
return this.#queue(() => {
48+
if (this.#currentState === 0) return;
49+
return this.#unqueuedGoto(this.#currentState - 1);
50+
});
51+
}
52+
53+
next() {
54+
return this.#queue(() => this.#advance({ useTransitions: true }));
55+
}
56+
57+
get hasNext() {
58+
return !this.#done;
59+
}
60+
61+
get hasPrevious() {
62+
return this.#currentState > 0;
63+
}
64+
}
65+
66+
customElements.define("spec-slide", Slide);
67+
68+
/**
69+
* @param {HTMLElement} element
70+
* @param {Keyframe[] | PropertyIndexedKeyframes} from
71+
* @param {KeyframeAnimationOptions} options
72+
*/
73+
export function transitionFrom(element, from, options) {
74+
const slide = element.closest("spec-slide");
75+
if (!slide) throw Error("Transitioning element must be within spec-slide");
76+
77+
from = Array.isArray(from) ? from : { ...from, offset: 0 };
78+
79+
const anim = element.animate(from, {
80+
...options,
81+
fill: "backwards",
82+
duration: slide.useTransitions ? options.duration : 0,
83+
delay: slide.useTransitions ? options.delay : 0,
84+
});
85+
86+
return anim;
87+
}
88+
89+
/**
90+
* @param {HTMLElement} element
91+
* @param {Keyframe[] | PropertyIndexedKeyframes} to
92+
* @param {KeyframeAnimationOptions} options
93+
*/
94+
export function transition(element, to, options) {
95+
const slide = element.closest("spec-slide");
96+
if (!slide) throw Error("Transitioning element must be within spec-slide");
97+
98+
const anim = element.animate(to, {
99+
...options,
100+
fill: "both",
101+
duration: slide.useTransitions ? options.duration : 0,
102+
delay: slide.useTransitions ? options.delay : 0,
103+
});
104+
105+
anim.finished.then(() => {
106+
anim.commitStyles();
107+
anim.cancel();
108+
});
109+
110+
return anim;
111+
}
119 KB
Binary file not shown.
58.5 KB
Binary file not shown.
76.1 KB
Binary file not shown.
112 KB
Binary file not shown.
59.3 KB
Binary file not shown.

0 commit comments

Comments
 (0)