diff --git a/src/assets/main.css b/src/assets/main.css index 243469f..60ed582 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -546,27 +546,56 @@ body, position: unset; } -/* Theme transition animation (from Element Plus) */ +/* Theme transition animation */ +:root { + --theme-x: 50%; + --theme-y: 50%; + --theme-r: 1500px; +} + ::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: 2147483646; + animation: switch-to-light 400ms ease-in-out forwards; } +/* 切换深色(变黑):旧视图在顶层,向点击位置收缩 */ .dark::view-transition-old(root) { z-index: 2147483646; + animation: switch-to-dark 400ms ease-in-out forwards; } .dark::view-transition-new(root) { z-index: 1; + animation: none; +} + +@keyframes switch-to-light { + from { + clip-path: circle(0px at var(--theme-x) var(--theme-y)); + } + to { + clip-path: circle(var(--theme-r) at var(--theme-x) var(--theme-y)); + } +} + +@keyframes switch-to-dark { + from { + clip-path: circle(var(--theme-r) at var(--theme-x) var(--theme-y)); + } + to { + clip-path: circle(0px at var(--theme-x) var(--theme-y)); + } } /* 整体编辑器区域 */ diff --git a/src/services/menu/MenuService.js b/src/services/menu/MenuService.js index ffc4e61..f6b9601 100644 --- a/src/services/menu/MenuService.js +++ b/src/services/menu/MenuService.js @@ -138,15 +138,15 @@ export const useThemeAndLocaleMenus = () => { const globalConfigStore = useGlobalConfigStore() const toggleTheme = (event) => { - // Attempt to get coordinates from the event or global event - // Element Plus el-menu-item emits a custom object, not a MouseEvent, so event.clientX might be missing. const mouseEvent = event && typeof event.clientX === 'number' ? event : window.event - const x = mouseEvent?.clientX ?? window.innerWidth / 2 - const y = mouseEvent?.clientY ?? window.innerHeight / 2 + const targetEl = mouseEvent?.target || document.querySelector('.icon-moon, .icon-sunny')?.closest('.el-menu-item, button, .el-dropdown-link') + const rect = targetEl?.getBoundingClientRect?.() + const x = mouseEvent?.clientX ?? (rect ? Math.round(rect.left + rect.width / 2) : window.innerWidth - 80) + const y = mouseEvent?.clientY ?? (rect ? Math.round(rect.top + rect.height / 2) : 30) const endRadius = Math.hypot( - Math.max(x, innerWidth - x), - Math.max(y, innerHeight - y) + Math.max(x, window.innerWidth - x), + Math.max(y, window.innerHeight - y) ) // Fallback for browsers without View Transition API @@ -155,46 +155,14 @@ export const useThemeAndLocaleMenus = () => { return } - // Disable transitions to prevent "fading" snapshot - const css = document.createElement('style') - css.appendChild(document.createTextNode(`* { - -webkit-transition: none !important; - -moz-transition: none !important; - -o-transition: none !important; - -ms-transition: none !important; - transition: none !important; - }`)) - document.head.appendChild(css) + document.documentElement.style.setProperty('--theme-x', `${x}px`) + document.documentElement.style.setProperty('--theme-y', `${y}px`) + document.documentElement.style.setProperty('--theme-r', `${endRadius}px`) - const transition = document.startViewTransition(async () => { + document.startViewTransition(async () => { globalConfigStore.changeTheme(!globalConfigStore.isDarkTheme) 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: globalConfigStore.isDarkTheme ? [...clipPath].reverse() : clipPath - }, - { - duration: 400, - easing: 'ease-in', - fill: 'forwards', - pseudoElement: globalConfigStore.isDarkTheme - ? '::view-transition-old(root)' - : '::view-transition-new(root)' - } - ) - }) - - // Clean up the transition disable style after the animation finishes - transition.finished.then(() => { - document.head.removeChild(css) - }) } return [{