104 lines
3.5 KiB
TypeScript
104 lines
3.5 KiB
TypeScript
|
|
'use client';
|
|||
|
|
|
|||
|
|
import { useEffect, useState, useMemo } from 'react';
|
|||
|
|
|
|||
|
|
interface ShatterTransitionProps {
|
|||
|
|
isActive: boolean;
|
|||
|
|
onComplete: () => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 增加网格密度以获得更细腻的破碎感
|
|||
|
|
const GRID_COLS = 10;
|
|||
|
|
const GRID_ROWS = 10;
|
|||
|
|
|
|||
|
|
export default function ShatterTransition({ isActive, onComplete }: ShatterTransitionProps) {
|
|||
|
|
const [isAnimating, setIsAnimating] = useState(false);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (isActive) {
|
|||
|
|
setIsAnimating(true);
|
|||
|
|
// 总动画时间 = 最大延迟 + 单个动画时长
|
|||
|
|
// 这里预留充足时间确保最后一片落完
|
|||
|
|
const timer = setTimeout(() => {
|
|||
|
|
setIsAnimating(false);
|
|||
|
|
onComplete();
|
|||
|
|
}, 1500);
|
|||
|
|
return () => clearTimeout(timer);
|
|||
|
|
} else {
|
|||
|
|
setIsAnimating(false);
|
|||
|
|
}
|
|||
|
|
}, [isActive, onComplete]);
|
|||
|
|
|
|||
|
|
// 预先计算碎片数据,避免重渲染时的性能抖动
|
|||
|
|
const fragments = useMemo(() => {
|
|||
|
|
return Array.from({ length: GRID_COLS * GRID_ROWS }, (_, i) => {
|
|||
|
|
const col = i % GRID_COLS;
|
|||
|
|
const row = Math.floor(i / GRID_COLS);
|
|||
|
|
|
|||
|
|
// 核心算法:计算距离左上角的距离 (欧几里得距离)
|
|||
|
|
// 距离越远,delay 越大
|
|||
|
|
const distance = Math.sqrt(col * col + row * row);
|
|||
|
|
const maxDistance = Math.sqrt(GRID_COLS * GRID_COLS + GRID_ROWS * GRID_ROWS);
|
|||
|
|
const normalizedDistance = distance / maxDistance;
|
|||
|
|
|
|||
|
|
// 爆炸参数
|
|||
|
|
const randomX = (Math.random() - 0.5) * 200; // X轴偏移
|
|||
|
|
const randomY = Math.random() * 300 + 100; // Y轴主要向下掉落
|
|||
|
|
const randomRotateX = (Math.random() - 0.5) * 720; // 剧烈翻滚
|
|||
|
|
const randomRotateY = (Math.random() - 0.5) * 720;
|
|||
|
|
const randomScale = 0.5 + Math.random() * 0.5; // 碎片大小不一
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
id: i,
|
|||
|
|
col,
|
|||
|
|
row,
|
|||
|
|
// 延迟时间:基准延迟 + 随机微扰 (让波浪不那么死板)
|
|||
|
|
delay: normalizedDistance * 600 + (Math.random() * 100),
|
|||
|
|
randomX,
|
|||
|
|
randomY,
|
|||
|
|
randomRotateX,
|
|||
|
|
randomRotateY,
|
|||
|
|
randomScale
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
if (!isActive && !isAnimating) return null;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
className="fixed inset-0 z-[9999] pointer-events-none overflow-hidden"
|
|||
|
|
style={{ perspective: '1200px' }} // 开启3D透视,这很重要!
|
|||
|
|
>
|
|||
|
|
<div className="relative w-full h-full bg-transparent">
|
|||
|
|
{fragments.map((frag) => (
|
|||
|
|
<div
|
|||
|
|
key={frag.id}
|
|||
|
|
className="absolute shatter-fragment will-change-transform"
|
|||
|
|
style={{
|
|||
|
|
left: `${(frag.col / GRID_COLS) * 100}%`,
|
|||
|
|
top: `${(frag.row / GRID_ROWS) * 100}%`,
|
|||
|
|
width: `${100 / GRID_COLS + 0.1}%`, // +0.1% 防止渲染缝隙
|
|||
|
|
height: `${100 / GRID_ROWS + 0.1}%`,
|
|||
|
|
|
|||
|
|
// 动画变量传入 CSS
|
|||
|
|
'--delay': `${frag.delay}ms`,
|
|||
|
|
'--tx': `${frag.randomX}px`,
|
|||
|
|
'--ty': `${frag.randomY}px`,
|
|||
|
|
'--rx': `${frag.randomRotateX}deg`,
|
|||
|
|
'--ry': `${frag.randomRotateY}deg`,
|
|||
|
|
'--s': `${frag.randomScale}`,
|
|||
|
|
} as React.CSSProperties}
|
|||
|
|
>
|
|||
|
|
{/* 内部容器负责材质,外部容器负责位置,分离关注点 */}
|
|||
|
|
<div className="w-full h-full glass-shard bg-white backdrop-blur-lg border-[0.5px] border-white/30 shadow-md" />
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 可选:背景遮罩,防止碎片飞走后直接漏出底部内容,根据需求调整 */}
|
|||
|
|
<div className="absolute inset-0 bg-white -z-10 animate-fade-out" />
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|