/** * 主题切换动画工具函数 * 使用 View Transitions API 实现圆形扩展/收缩动画效果 */ /** * 计算从指定点到视口角落的最大距离 * @param {number} x - 点击位置的 x 坐标 * @param {number} y - 点击位置的 y 坐标 * @returns {number} 最大距离(像素) */ function getDistance(x, y) { const maxX = Math.max(x, window.innerWidth - x) const maxY = Math.max(y, window.innerHeight - y) return Math.sqrt(maxX * maxX + maxY * maxY) } /** * 执行主题切换动画 * @param {Function} callback - 主题切换的回调函数 * @param {MouseEvent} event - 点击事件对象(可选) */ export function toggleThemeWithTransition(callback, event = null) { // 检查浏览器是否支持 View Transitions API if (!document.startViewTransition) { // 不支持时直接执行回调,无动画 callback() return } // 获取当前主题状态(切换前的状态) const wasDark = document.documentElement.classList.contains('dark') // 获取点击位置,如果没有事件则使用默认位置(右上角图标位置) let iconX, iconY if (event) { iconX = event.clientX iconY = event.clientY } else { // 默认位置:右上角图标区域(可以根据实际图标位置调整) iconX = window.innerWidth - 100 iconY = 25 // 导航栏高度的一半 } // 左下角位置 const bottomLeftX = 0 const bottomLeftY = window.innerHeight // 计算从不同位置到视口边缘的最大距离 const iconRadius = getDistance(iconX, iconY) const bottomLeftRadius = getDistance(bottomLeftX, bottomLeftY) // 创建 View Transition const transition = document.startViewTransition(() => { callback() }) // 设置动画样式 transition.ready.then(() => { if (!wasDark) { // 当前是日间模式,切换到夜间模式 // 动画效果:从左下角扩展到右上角(图标位置) // 旧视图(日间模式)退出:淡出 document.documentElement.animate( [ { opacity: 1 }, { opacity: 0 } ], { duration: 1000, easing: 'ease-in', pseudoElement: '::view-transition-old(root)' } ) // 新视图(夜间模式)进入:从左下角圆形扩展到图标位置 document.documentElement.animate( [ { clipPath: `circle(0px at ${bottomLeftX}px ${bottomLeftY}px)`, opacity: 0 }, { clipPath: `circle(${iconRadius}px at ${iconX}px ${iconY}px)`, opacity: 1 } ], { duration: 1000, easing: 'cubic-bezier(0.4, 0, 0.2, 1)', pseudoElement: '::view-transition-new(root)' } ) } else { // 当前是夜间模式,切换到日间模式 // 动画效果:从图标位置收缩到左下角 // 旧视图(夜间模式)退出:从图标位置圆形收缩到左下角 document.documentElement.animate( [ { clipPath: `circle(${iconRadius}px at ${iconX}px ${iconY}px)`, opacity: 1 }, { clipPath: `circle(0px at ${bottomLeftX}px ${bottomLeftY}px)`, opacity: 0 } ], { duration: 600, easing: 'cubic-bezier(0.4, 0, 0.2, 1)', pseudoElement: '::view-transition-old(root)' } ) // 新视图(日间模式)进入:从图标位置扩展到全屏 document.documentElement.animate( [ { clipPath: `circle(0px at ${iconX}px ${iconY}px)`, opacity: 0 }, { clipPath: `circle(${iconRadius}px at ${iconX}px ${iconY}px)`, opacity: 1 } ], { duration: 600, easing: 'cubic-bezier(0.4, 0, 0.2, 1)', pseudoElement: '::view-transition-new(root)' } ) } }) } /** * 初始化 View Transitions API 的 CSS * 需要在应用启动时调用 */ export function initThemeTransitionCSS() { // 检查是否已经添加过样式 if (document.getElementById('theme-transition-style')) { return } const style = document.createElement('style') style.id = 'theme-transition-style' style.textContent = ` /* View Transitions API 样式 */ @view-transition { navigation: auto; } ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 0.6s; animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } /* 确保动画覆盖整个视口 */ ::view-transition-old(root), ::view-transition-new(root) { width: 100%; height: 100%; top: 0; left: 0; } ` document.head.appendChild(style) }