使用 View Transitions API 来实现切换主题
View Transitions API 提供了一种机制,可以在更新 DOM 内容的同时,轻松地创建不同 DOM 状态之间的动画过渡。同时还可以在单个步骤中更新 DOM 内容。这是官方对他的描述,详情请看这里。
原理
当调用document.startViewTransition()
时,API 会根据当前页面的屏幕截图,创建一个动画效果,将当前页面过渡到新的 DOM 状态。
这个startViewTransition
函数会返回一个回调函数,我们需要在回调函数中更新我们的Dom,从而产生一个动画。
使用
下方创建好了一个模板,其中包含一个按钮,点击按钮可以切换背景颜色,我们使用需要使用View Transitions API
实现一个简单的圆形扩散动画效果。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
:root {
--bg-color: #fff;
background-color: var(--bg-color);
}
:root.dark {
--bg-color: #000;
}
</style>
</head>
<body>
<button id="btn">切换</button>
</body>
<script>
btn.addEventListener('click', (e) => {
document.documentElement.classList.toggle('dark')
})
</script>
</html>
创建一个过渡
首先,我们需要创建一个过渡,使用document.startViewTransition()
函数,这个函数接收一个回调函数,我们在回调函数中更新DOM状态,会产生一个动画效果。
btn.addEventListener('click', (e) => {
document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})
})
这个时候会发现,页面动画产生了一个渐隐的效果,这是因为document.startViewTransition()
函数默认会创建的一个效果,如果我们想要变更的话就需要对他进行修改。
修改过渡状态
首先需要拿到document.startViewTransition()
产生的实例,这里使用transition
来接收,过渡状态可以通过transition.ready
属性来获取,它是一个Promise
对象。
const transition = document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})
transition.ready.then(() => {
// 实现过渡的过程
})
我们目标是要实现一个圆心扩散动画,这里可以用到clipPath
来裁剪屏幕截图,设定中心点为鼠标位置,从0扩散到100%,从而实现中心扩散的效果。
transition.ready.then(() => {
// 实现过渡的过程 circle
document.documentElement.animate(
{
clipPath: [
'circle(0 at 50% 50%)',
'circle(100% at 50% 50%)'
],
},
{
duration: 800,
pseudoElement: '::view-transition-new(root)'
}
)
})
这时候会发现我们的动画效果并没有发生,原因是::view-transition-xxx
的伪类自带的默认动画覆盖了自定义的动画,我们需要在::view-transition-new(root)
伪类中,添加animation: none;
来取消默认动画。
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
}
动画已经有了,这个时候来修改动画的圆心为鼠标点击位置,可以通过事件对象获取到。
const x = e.clientX
const y = e.clientY
transition.ready.then(() => {
// 实现过渡的过程 circle
document.documentElement.animate(
{
clipPath: [
`circle(0 at ${x}px ${y}px)`,
`circle(100% at ${x}px ${y}px)`,
],
},
{
duration: 800,
pseudoElement: '::view-transition-new(root)',
}
)
})
然而圆形的半径是固定的,我们想要实现一个圆形扩散的效果,就需要修改圆形的半径,这里可以根据勾股定理来计算。
const x = e.clientX
const y = e.clientY
// 从点击点到窗口最远边缘的距离,这个距离即为圆的半径,用于确定一个圆形裁剪路径 (clip path) 的最大尺寸,以便覆盖整个视窗。
// 勾股定理:a² + b² = c²
const radius = Math.sqrt(Math.max(x, (window.innerWidth - x)) ** 2 + Math.max(y, (window.innerHeight - y)) ** 2)
transition.ready.then(() => {
// 实现过渡的过程 circle
document.documentElement.animate(
{
clipPath: [
`circle(0 at ${x}px ${y}px)`,
`circle(${radius} at ${x}px ${y}px)`,
]
},
{
duration: 800,
pseudoElement: '::view-transition-new(root)',
}
)
})
结束
完整代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
:root {
--bg-color: #fff;
background-color: var(--bg-color);
}
:root.dark {
--bg-color: #000;
}
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
}
#btn{
position: absolute;
top: 90%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
</head>
<body>
<button id="btn">切换</button>
</body>
<script>
btn.addEventListener('click', (e) => {
const transition = document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})
const x = e.clientX
const y = e.clientY
// 从点击点到窗口最远边缘的距离,这个距离即为圆的半径,用于确定一个圆形裁剪路径 (clip path) 的最大尺寸,以便覆盖整个视窗。
// 勾股定理:a² + b² = c²
const radius = Math.sqrt(Math.max(x, (window.innerWidth - x)) ** 2 + Math.max(y, (window.innerHeight - y)) ** 2)
transition.ready.then(() => {
// 实现过渡的过程 circle
document.documentElement.animate(
{
clipPath: [
`circle(0 at ${x}px ${y}px)`,
`circle(${radius}px at ${x}px ${y}px)`,
]
},
{
duration: 800,
pseudoElement: '::view-transition-new(root)',
}
)
})
})
</script>
</html>
总结
View Transitions API
提供了一种简单的方式来创建动画效果,同时也可以在单个步骤中更新 DOM 内容。