请稍等...

小波Note

四川 · 成都市小雨7 ℃
中文

黑白切换动画

本文可选语言
阿坝藏族羌族自治州2025年2月2日周日 19时3.54k272预计阅读时间 7 分钟
二维码
收藏Ctrl + D

最近想实现一个黑白切换的动画,在 antfu 大佬的博客上面看到了效果,感觉很不错,看了看源码,正好我是 nuxt3 的,就只直接用了。

antfu

具体代码位置: 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>

    

效果

it-fb

星空