62 lines
2.3 KiB
TypeScript
62 lines
2.3 KiB
TypeScript
import { nextTick } from 'vue'
|
|
|
|
import { useAppStore } from '@/stores/app'
|
|
|
|
/** 扩散揭示时长;与 main.css 中 view-transition 的默认动画禁用配套 */
|
|
const RIPPLE_DURATION = 560
|
|
|
|
/** 进行中标志:扩散未结束时忽略再次触发,防止打断上一个 transition 造成瞬跳闪烁 */
|
|
let rippling = false
|
|
|
|
/** 明暗主题切换,新主题以触发元素为圆心向外扩散揭示(View Transitions);
|
|
* 浏览器不支持或用户偏好减少动效时退化为直接切换 */
|
|
export function useThemeRipple() {
|
|
const app = useAppStore()
|
|
|
|
async function toggleDarkFrom(e: MouseEvent) {
|
|
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
if (typeof document.startViewTransition !== 'function' || reduced) {
|
|
app.toggleDark()
|
|
return
|
|
}
|
|
if (rippling) return
|
|
rippling = true
|
|
const { x, y } = originOf(e)
|
|
// 回调内等 nextTick:html.dark 由 store watcher 刷新,须在快照回调结束前落地。
|
|
// theme-switching 禁掉页面自身的颜色 transition:::view-transition-new(root)
|
|
// 是新 DOM 的实时替身,组件残留的渐变会在扩散圆内继续播放,观感即闪烁。
|
|
const transition = document.startViewTransition(async () => {
|
|
document.documentElement.classList.add('theme-switching')
|
|
app.toggleDark()
|
|
await nextTick()
|
|
})
|
|
try {
|
|
await transition.ready
|
|
const radius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y))
|
|
document.documentElement.animate(
|
|
{ clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${radius}px at ${x}px ${y}px)`] },
|
|
{ duration: RIPPLE_DURATION, easing: 'ease-in-out', pseudoElement: '::view-transition-new(root)' },
|
|
)
|
|
} finally {
|
|
void transition.finished.finally(() => {
|
|
document.documentElement.classList.remove('theme-switching')
|
|
rippling = false
|
|
})
|
|
}
|
|
}
|
|
|
|
return { toggleDarkFrom }
|
|
}
|
|
|
|
/** 扩散圆心:优先触发元素几何中心(键盘触发 clientX/Y 为 0 也稳),兜底点击坐标 */
|
|
function originOf(e: MouseEvent): { x: number; y: number } {
|
|
const el = e.currentTarget
|
|
if (el instanceof Element) {
|
|
const rect = el.getBoundingClientRect()
|
|
if (rect.width || rect.height) {
|
|
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
|
|
}
|
|
}
|
|
return { x: e.clientX, y: e.clientY }
|
|
}
|