5 min read 约918字
前几天在使用 element ui的时候,发现它有个很酷的主题切换动画,感觉很有趣,就研究了一下它的实现原理。
原来它是通过浏览器的 View Transitions API
来实现的,具体的实现原理可以参考 View Transitions API。
在这个view-transition
API中,我们可以在css中设置多个::view::
元素,每个::view::
元素代表一个视图,当我们切换主题的时候,浏览器会自动将两个::view::
元素的样式属性平滑过渡到目标样式。它包含若干个视图状态,我们可以设置不同的样式属性来实现不同的过渡效果。在 element ui 中,它使用了 ::view-transition-old()
和 ::view-transition-new()
两个伪元素来实现主题切换的动画效果。
::view-transition-group(root) {
animation-duration: 1.25s;
}
::view-transition-new(root),
::view-transition-old(root) {
mix-blend-mode: normal;
}
::view-transition-new(root) {
animation-name: reveal-light;
}
::view-transition-old(root),
.dark::view-transition-old(root) {
animation: none;
}
.dark::view-transition-new(root) {
animation-name: reveal-dark;
}
@keyframes reveal-dark {
from {
clip-path: polygon(-30% 0, -30% 0, -15% 100%, -10% 115%);
}
to {
clip-path: polygon(-30% 0, 130% 0, 115% 100%, -10% 115%);
}
}
@keyframes reveal-light {
from {
clip-path: polygon(130% 0, 130% 0, 115% 100%, 110% 115%);
}
to {
clip-path: polygon(130% 0, -30% 0, -15% 100%, 110% 115%);
}
}
::view-transition-group(root)
:这个选择器用于定义整个视图切换过程的动画效果。在这里,设置了动画持续时间为 1.25 秒。
::view-transition-new(root)
和 ::view-transition-old(root)
:这两个选择器分别表示新视图和旧视图。在这里,设置了 mix-blend-mode: normal
;,表示混合模式为正常模式,即新视图会完全覆盖旧视图。
::view-transition-new(root)
和 .dark::view-transition-new(root)
:这两个选择器定义了新视图的动画效果。在明亮主题和暗黑主题下分别使用了不同的动画。动画名称分别为 reveal-light 和 reveal-dark。
::view-transition-old(root)
和 .dark::view-transition-old(root)
:这两个选择器用于定义旧视图的动画效果。在这里,明亮主题和暗黑主题下都取消了旧视图的动画,即设置了 animation: none;,旧视图会立即消失而不使用动画效果。
@keyframes reveal-dark
和 @keyframes reveal-light
:这两个 @keyframes 定义了两种不同的动画效果,分别用于暗黑主题和明亮主题。这些动画效果通过改变 clip-path 的值来实现视图的逐渐显现或消失效果。
我写了个小小的demo
if (document.startViewTransition) {
// 你的代码
} else {
// 降级方案
}
我使用的是svelte框架,下面是我的代码:
const toggleTheme = (event) => {
const isDark = getCurrentTheme() === 'dark';
const x = event?.clientX;
const y = event?.clientY;
const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
if (!document.startViewTransition) {
setTheme(isDark ? 'light' : 'dark');
return;
}
const transition = document.startViewTransition(() => {
document.documentElement.classList.toggle('dark');
});
transition.ready.then(() => {
const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`];
document.documentElement
.animate(
{
clipPath: isDark ? [...clipPath].reverse() : clipPath
},
{
duration: 400,
easing: 'ease-in-out',
pseudoElement: isDark ? '::view-transition-old(root)' : '::view-transition-new(root)'
}
)
.finished.then(() => {
setTheme(document.documentElement.classList.contains('dark') ? 'dark' : 'light');
});
});
};
如果你有兴趣,可以尝试实现一个类似的功能,实现效果点击右边的按钮