diff --git a/src/components/NavBar/index.vue b/src/components/NavBar/index.vue index c852551..acba7ba 100644 --- a/src/components/NavBar/index.vue +++ b/src/components/NavBar/index.vue @@ -1,8 +1,8 @@ diff --git a/src/hooks/useToggleDarkMode.ts b/src/hooks/useToggleDarkMode.ts index 783fbee..8ebfee5 100644 --- a/src/hooks/useToggleDarkMode.ts +++ b/src/hooks/useToggleDarkMode.ts @@ -4,6 +4,6 @@ export function useDarkMode() { return useDarkModeStoreHook().darkMode; } -export function useToggleDarkMode() { - useDarkModeStoreHook().toggleDarkMode(); +export function useToggleDarkMode(event?: TouchEvent | MouseEvent) { + useDarkModeStoreHook().toggleDarkMode(event); } diff --git a/src/store/modules/darkMode.ts b/src/store/modules/darkMode.ts index 109a2ae..c149569 100644 --- a/src/store/modules/darkMode.ts +++ b/src/store/modules/darkMode.ts @@ -1,5 +1,6 @@ import { defineStore } from "pinia"; import { store } from "@/store"; +import { nextTick } from "vue"; const darkModeKey = "__dark_mode__"; const isDarkMode = () => { @@ -17,15 +18,67 @@ export const useDarkModeStore = defineStore({ darkMode: isDarkMode() }), actions: { - toggleDarkMode() { - this.darkMode = !this.darkMode; - if (this.darkMode) { - document.documentElement.classList.add("dark"); - window.localStorage.setItem(darkModeKey, "true"); + toggleDarkMode(event?: TouchEvent | MouseEvent) { + const isAppearanceTransition = + "startViewTransition" in document && + !window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + const toggle = () => { + this.darkMode = !this.darkMode; + if (this.darkMode) { + document.documentElement.classList.add("dark"); + window.localStorage.setItem(darkModeKey, "true"); + } else { + document.documentElement.classList.remove("dark"); + window.localStorage.setItem(darkModeKey, "false"); + } + }; + + if (!isAppearanceTransition) { + toggle(); + return; + } + + let x: number, y: number; + if (event instanceof TouchEvent) { + x = event.touches[0].clientX; + y = event.touches[0].clientY; + } else if (event instanceof MouseEvent) { + x = event.clientX; + y = event.clientY; } else { - document.documentElement.classList.remove("dark"); - window.localStorage.setItem(darkModeKey, "false"); + // 如果没有事件对象,使用屏幕右上角作为默认位置 + x = window.innerWidth; + y = 0; } + const endRadius = Math.hypot( + Math.max(x, window.innerWidth - x), + Math.max(y, window.innerHeight - y) + ); + + const transition = (document as any).startViewTransition(async () => { + toggle(); + 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: this.darkMode ? [...clipPath].reverse() : clipPath + }, + { + duration: 400, + easing: "ease-out", + pseudoElement: this.darkMode + ? "::view-transition-old(root)" + : "::view-transition-new(root)" + } + ); + }); } } }); diff --git a/src/styles/index.less b/src/styles/index.less index a63d279..4e9f414 100644 --- a/src/styles/index.less +++ b/src/styles/index.less @@ -33,3 +33,22 @@ a:hover { color: inherit; text-decoration: none; } + +// see https://github.com/vuejs/vitepress/pull/2347 +::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; +}