company_components_web/src/utils/themeTransition.js

176 lines
4.7 KiB
JavaScript
Raw Normal View History

2026-02-03 15:09:50 +08:00
/**
* 主题切换动画工具函数
* 使用 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)
}