176 lines
4.7 KiB
JavaScript
176 lines
4.7 KiB
JavaScript
|
|
/**
|
|||
|
|
* 主题切换动画工具函数
|
|||
|
|
* 使用 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)
|
|||
|
|
}
|
|||
|
|
|