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 = 7;
|
||
const GRID_ROWS = 7;
|
||
|
||
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>
|
||
);
|
||
}
|