element 官网的主题切换动画

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');
			});
	});
};

交作业

如果你有兴趣,可以尝试实现一个类似的功能,实现效果点击右边的按钮

respect🤫

发表评论🍒

网站已运行22天

© 2024 GoDom. All rights reserved.

writter by GoDom

not by AI