Skip to content

Commit 849943b

Browse files
committed
View Transition API
1 parent 304228c commit 849943b

File tree

6 files changed

+144
-23
lines changed

6 files changed

+144
-23
lines changed

main.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ function getArchive(title, iters) {
6161
(acc, { date, summary }) => {
6262
const place = `/./posts/${handleUTC(date)}/`
6363
return acc +
64-
`<p><a class="decoration-line" href=${place} target="_blank"> ${summary} ··· ${convertToUSA(date)
64+
`<p><a class="decoration-line" href=${place} target="_blank"> ${summary} ··· ${
65+
convertToUSA(date)
6566
}</a></p>`
6667
},
6768
'',
@@ -179,9 +180,10 @@ async function Others() {
179180
const sitemap = new URL('./sitemap.xml', dist)
180181
const itemsSitemap = `<?xml version="1.0" encoding="UTF-8"?>
181182
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
182-
${metaData.reduce((acc, { date }) =>
183-
`${acc}<url><loc>${WEBSITE}posts/${handleUTC(date)}/</loc></url>`, '')
184-
}
183+
${
184+
metaData.reduce((acc, { date }) =>
185+
`${acc}<url><loc>${WEBSITE}posts/${handleUTC(date)}/</loc></url>`, '')
186+
}
185187
</urlset>`
186188

187189
// robots
@@ -222,9 +224,7 @@ async function Home() {
222224
})
223225

224226
if ((index + 1) % 8 === 0 || index + 1 === mLength) {
225-
const cur = index + 1 === mLength
226-
? lastPage
227-
: Math.floor((index + 1) / 8)
227+
const cur = index + 1 === mLength ? lastPage : Math.floor((index + 1) / 8)
228228
const process = templateProcess({
229229
before: cur > 2 ? `/./home/${cur - 1}/` : '/',
230230
page: `${cur} / ${lastPage}`,
@@ -284,8 +284,9 @@ async function About() {
284284

285285
const [, md] = parseYaml(about)
286286
const content = await markdown(md)
287-
const generated = `${head}${header}${templateArticle({ title: '关于我', content })
288-
}${footer}`
287+
const generated = `${head}${header}${
288+
templateArticle({ title: '关于我', content })
289+
}${footer}`
289290
await Deno.writeTextFile(__dist_about, generated)
290291
}
291292

public/JavaScript/index.js

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
///<reference lib="dom" />
22

3+
// Regex to match the content of the main tag
34
const regex =
45
/\<head\>[\s\S]*\<\/head\>[\s\S]*?\<main[\s\S]*?\>([\s\S]*)\<\/main\>/
56

67
let isDark = globalThis.matchMedia('(prefers-color-scheme: dark)').matches
78

8-
/**
9-
* @type {HTMLMetaElement}
10-
*/
9+
// check if the browser supports view transition
10+
const isViewTransition = document.startViewTransition &&
11+
!globalThis.matchMedia('(prefers-reduced-motion: reduce)').matches
12+
13+
/** @type {HTMLMetaElement}*/
1114
const metaTheme = document.head.querySelector("meta[name='theme-color']")
1215

1316
/**
1417
* @param {boolean} isDarkTheme
1518
* @param {Element} e
19+
* @param {Array<[string, string]>} colors
1620
*/
1721
function toggleColor(isDarkTheme, e) {
1822
e.classList.toggle('fa-sun')
1923
e.classList.toggle('fa-moon')
20-
window.localStorage.setItem('darkMode', isDarkTheme ? 'dark' : 'light')
24+
globalThis.localStorage.setItem('darkMode', isDarkTheme ? 'dark' : 'light')
25+
2126
const colors = [
2227
['--theme-color', isDarkTheme ? '#ffffff' : 'rgb(0, 0, 0)'],
2328
[
@@ -46,7 +51,7 @@ document.addEventListener('DOMContentLoaded', () => {
4651
toyNavHeight + 'px',
4752
)
4853

49-
const localDarkMode = window.localStorage.getItem('darkMode')
54+
const localDarkMode = globalThis.localStorage.getItem('darkMode')
5055
isDark = localDarkMode === 'undefined' ? isDark : localDarkMode === 'dark'
5156

5257
const model = document.querySelector('a.model')
@@ -56,10 +61,30 @@ document.addEventListener('DOMContentLoaded', () => {
5661

5762
model.addEventListener('click', (e) => {
5863
e.preventDefault()
59-
const darkMode = window.localStorage.getItem('darkMode') === 'dark'
64+
const darkMode = globalThis.localStorage.getItem('darkMode') === 'dark'
6065
? 'light'
6166
: 'dark'
62-
toggleColor(darkMode === 'dark', darkIcon)
67+
68+
if (!isViewTransition) {
69+
return toggleColor(darkMode === 'dark', darkIcon)
70+
}
71+
72+
// 存储点击坐标
73+
const x = e.clientX
74+
const y = e.clientY
75+
const endRadius = Math.hypot(
76+
Math.max(x, innerWidth - x),
77+
Math.max(y, innerHeight - y),
78+
)
79+
80+
document.startViewTransition(
81+
() => toggleColor(darkMode === 'dark', darkIcon),
82+
)
83+
;[
84+
['--click-x', `${x}px`],
85+
['--click-y', `${y}px`],
86+
['--end-radius', `${endRadius}px`]
87+
].forEach(([v, c]) => document.documentElement.style.setProperty(v, c))
6388
})
6489

6590
const header = document.querySelector('header')
@@ -133,7 +158,7 @@ const renderPage = async (e) => {
133158
reactionsEnabled: '1',
134159
emitMetadata: '1',
135160
inputPosition: 'bottom',
136-
theme: window.localStorage.getItem('darkMode') ??
161+
theme: globalThis.localStorage.getItem('darkMode') ??
137162
'preferred_color_scheme',
138163
lang: 'zh-CN',
139164
},

public/css/base.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,24 @@ img {
4040

4141
.text-center {
4242
text-align: center;
43+
}
44+
45+
::view-transition-new(root) {
46+
animation: turnOn 800ms ease-in-out;
47+
mix-blend-mode: normal;
48+
}
49+
50+
::view-transition-old(root) {
51+
animation: none;
52+
}
53+
54+
/* 新视图过渡 */
55+
@keyframes turnOn {
56+
0% {
57+
clip-path: circle(0% at var(--click-x) var(--click-y));
58+
}
59+
60+
100% {
61+
clip-path: circle(var(--end-radius) at var(--click-x) var(--click-y));
62+
}
4363
}

public/css/index.css

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@
3939
body {
4040
background-color: var(--bg-color);
4141
background-size: cover;
42-
padding-block-end: 0.5rem;
4342
position: relative;
4443
color: var(--theme-color);
44+
padding-block-start: calc(var(--header-height) + 1rem);
4545

46-
/* 防止溢出 */
46+
/* prevent overflow */
4747
word-break: break-all;
4848

4949
&.loading {
@@ -125,13 +125,12 @@ main.blog-main {
125125
min-height: calc(100vh - var(--header-height) - var(--footer-height));
126126
display: flex;
127127
justify-content: center;
128+
margin-block-end: 1.2rem;
128129

129130
&:has(article.blog-article) {
130131
align-items: flex-start;
131132
}
132133

133-
margin-block-start: calc(var(--header-height) + 1rem);
134-
margin-block-end: 1.2rem;
135134

136135
/* 除了锚点 a 都要换行 */
137136
& :not(:where(h2, h3, h4, h5, h6)) a[href^="http"] {

src/posts/css_animation.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,80 @@ summary: 用于收集一些常见的 CSS 动画效果
1919
示例:可以看该提交是如何修改 height 动画 <https://github.com/fwqaaq/fwqaaq.github.io/commit/068be22f827e8b75070f78a8ab5c05875842ce80>
2020

2121
参见:<https://developer.chrome.com/docs/css-ui/animate-to-height-auto>[`<calc-sum>`](https://developer.mozilla.org/en-US/docs/Web/CSS/calc-sum)
22+
23+
## [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API)
24+
25+
这是一个用于实现在不同 DOM 之间转换的动画效果的 API,并且可以实现非常平滑的过渡效果,它主要是对于 Transition API 的扩展,并且可以实现更加复杂的过渡效果。
26+
27+
```txt
28+
::view-transition
29+
└─ ::view-transition-group(root)
30+
└─ ::view-transition-image-pair(root)
31+
├─ ::view-transition-old(root)
32+
└─ ::view-transition-new(root)
33+
```
34+
35+
对于该 API 的使用,主要的参数分类分别是:`*`(匹配所有过渡的元素)、`root`(匹配过渡的根元素)以及 [`<custom-ident>`](https://developer.mozilla.org/zh-CN/docs/Web/CSS/custom-ident)(通过 [view-transition-name](https://developer.mozilla.org/zh-CN/docs/Web/CSS/view-transition-name) 属性分配给某个元素从而达到过渡效果)。
36+
37+
示例:
38+
39+
```css
40+
header {
41+
view-transition-name: header-card;
42+
}
43+
/**带有 header-card 的都会实现以下基础的过渡效果*/
44+
::view-transition-group(header-card) {
45+
animation-duration: 0.5s;
46+
}
47+
```
48+
49+
这里需要注意的是,主要用的是 `::view-transition-old``::view-transition-new` 这两个伪元素,浏览器会在后台为这两个元素创建快照,old 表示过渡之前的快照,new 表示过渡之后的快照。
50+
51+
在过渡过程中,浏览器会同时显示旧页面和新页面的元素,并根据计算好的动画路径对元素进行平滑的变换。例如,如果一个元素的位置、大小或颜色发生了变化,浏览器会以流畅的动画方式来展示这种变化,而不是突然的切换。
52+
53+
```css
54+
::view-transition-new(root) {
55+
animation: turnOn 800ms ease-in-out;
56+
mix-blend-mode: normal;
57+
}
58+
59+
::view-transition-old(root) {
60+
animation: none;
61+
}
62+
63+
/* 新视图过渡 */
64+
@keyframes turnOn {
65+
0% {
66+
clip-path: circle(0% at var(--click-x) var(--click-y));
67+
}
68+
69+
100% {
70+
clip-path: circle(var(--end-radius) at var(--click-x) var(--click-y));
71+
}
72+
}
73+
```
74+
75+
可以完全使用 CSS 的伪元素配合增加或者移除****来实现过渡效果,但是使用 JavaScript 控制会更加灵活。
76+
77+
特性 | 自动触发 `startViewTransition` | 手动触发
78+
---|----------------------------|-----
79+
适用场景 | 简单的状态类切换 | 复杂的 DOM 变更或跨页面动画
80+
动画控制 | 受限于 CSS 动画定义 | 完全可控,可插入任意逻辑
81+
实现复杂性 | 更简单 | 需要额外的 JS 代码
82+
83+
### [JavaScript 相关的 API](https://developer.mozilla.org/zh-CN/docs/Web/API/Document/startViewTransition)
84+
85+
`document.startViewTransition` 会接受一个改变当前 DOM 的回调函数,该回调函数完成兑现时,会返回一个 `ViewTransition` 对象。该对象可以的 `ready` 方法会在伪元素树被创建且过渡动画即将开始时兑现,如果过渡已经完成,则 `finished` 方法会返回一个已兑现的 `Promise`
86+
87+
配合以上的 CSS 代码,就可以实现一个非常流畅的黑暗模式切换效果。
88+
89+
```js
90+
document.startViewTransition(
91+
() => toggleColor(darkMode === 'dark', darkIcon),
92+
)
93+
;[
94+
['--click-x', `${x}px`],
95+
['--click-y', `${y}px`],
96+
['--end-radius', `${endRadius}px`]
97+
].forEach(([v, c]) => document.documentElement.style.setProperty(v, c))
98+
```

src/util/remark/markdown.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const markdown = async (file) =>
2020
.use(remarkList)
2121
.use(remarkRehype, { allowDangerousHtml: true })
2222
.use(rehypeShiki, {
23-
theme: "andromeeda",
23+
theme: 'andromeeda',
2424
defaultColor: false,
2525
addLanguageClass: true,
2626
})
@@ -38,4 +38,3 @@ export const markdown = async (file) =>
3838
})
3939
.use(rehypeStringify)
4040
.process(file)
41-

0 commit comments

Comments
 (0)