opt: 优化主题切换动画效果,防止出现动画断层

This commit is contained in:
gary.fu
2026-09-09 15:00:40 +08:00
parent 975b6f08a0
commit a788c09c50
2 changed files with 40 additions and 43 deletions

View File

@@ -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));
}
}
/* 整体编辑器区域 */

View File

@@ -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 [{