最近想实现一个黑白切换的动画,在 antfu 大佬的博客上面看到了效果,感觉很不错,看了看源码,正好我是 nuxt3 的,就只直接用了。
具体代码位置: https://github.com/antfu/antfu.me/blob/main/src/logics/index.ts
完整的代码
DarkToggle.vue
<script setup lang="ts">
const mode = useColorMode()
const isDark = computed<boolean>({
get() {
return mode.value === 'dark'
},
set() {
mode.preference = isDark.value ? 'light' : 'dark'
},
})
/**
* Credit to [@hooray](https://github.com/hooray)
* @see https://github.com/vuejs/vitepress/pull/2347
*/
function toggle(event?: MouseEvent) {
const isAppearanceTransition = typeof document !== 'undefined'
// @ts-expect-error document.startViewTransition can be undefined
&& document.startViewTransition
&& !window.matchMedia('(prefers-reduced-motion: reduce)').matches
if (!isAppearanceTransition || !event) {
isDark.value = !isDark.value
return
}
const x = event.clientX
const y = event.clientY
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y),
)
const transition = document.startViewTransition(async () => {
isDark.value = !isDark.value
await nextTick()
})
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.value
? clipPath.reverse()
: clipPath,
},
{
duration: 400,
easing: 'ease-out',
pseudoElement: isDark.value
? '::view-transition-old(root)'
: '::view-transition-new(root)',
},
)
})
}
const context = {
mode,
isDark,
toggle,
}
</script>
<template>
<ClientOnly placeholder-tag="span">
<slot v-bind="context" />
</ClientOnly>
</template>
<style>
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-old(root) {
z-index: 1;
}
::view-transition-new(root) {
z-index: 9999;
}
.dark::view-transition-old(root) {
z-index: 9999;
}
.dark::view-transition-new(root) {
z-index: 1;
}
</style>
收起
代码解释
mode
使用 useColorMode
钩子来获取当前的颜色模式。
isDark
是一个计算属性,用于判断当前模式是否为深色模式,并提供切换模式的功能。
toggle
函数用于切换深色模式和浅色模式。
isAppearanceTransition
检查是否支持视图过渡动画,并且用户没有选择减少动画效果。如果不支持视图过渡动画或没有事件传递,则直接切换模式。如果支持视图过渡动画,则计算点击位置和过渡动画的结束半径,并启动视图过渡动画,在动画过程中切换模式。
Math.hypot
计算直角三角形的斜边长度,确保动画覆盖整个视窗。
innerWidth
是一个只读属性,返回窗口的文档显示区的宽度(以像素为单位)。它包括滚动条的宽度(如果有)。在计算动画扩散半径时,我们使用 innerWidth
来确定从触发点到视窗边缘的最远距离。
transition.ready.then
中定义了过渡动画的效果,使用 clipPath
来实现圆形扩展的动画效果。
切换到深色模式:clipPath.reverse()
从大到小扩展。
切换到浅色模式:clipPath
从小到大扩展。
使用
TopBar.vue
<template>
...others
<DarkToggle v-if="isDesktop">
<template #default="{ toggle }">
<div cursor="pointer" @click="toggle">
<Icon size="20" name="material-symbols:light-mode-outline-rounded" dark="hidden" />
<Icon size="20" name="material-symbols:nightlight-outline-rounded" light="hidden" />
</div>
</template>
</DarkToggle>
...others
</template>