fecbfd1990
refactor: 优化代码健壮性和类型安全 style: 更新字体样式和全局CSS fix: 修复IntersectionObserver潜在空引用问题 chore: 更新依赖和ESLint配置 build: 更新构建ID和路由配置
179 lines
4.7 KiB
TypeScript
179 lines
4.7 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef, useCallback, useState } from 'react';
|
|
|
|
interface Particle {
|
|
x: number;
|
|
y: number;
|
|
targetX: number;
|
|
targetY: number;
|
|
vx: number;
|
|
vy: number;
|
|
size: number;
|
|
opacity: number;
|
|
color: string;
|
|
life: number;
|
|
maxLife: number;
|
|
stage: 'idle' | 'dispersing' | 'reforming';
|
|
}
|
|
|
|
interface SealAnimationEnhancedProps {
|
|
width?: number;
|
|
height?: number;
|
|
particleCount?: number;
|
|
colors?: string[];
|
|
sealText?: string;
|
|
animationStages?: boolean;
|
|
onStageChange?: (stage: string) => void;
|
|
className?: string;
|
|
}
|
|
|
|
export function SealAnimationEnhanced({
|
|
width = 300,
|
|
height = 300,
|
|
particleCount = 150,
|
|
colors = ['#C41E3A', '#D4A574', '#8B4513'],
|
|
sealText: _sealText = '睿新',
|
|
animationStages = true,
|
|
onStageChange,
|
|
className = '',
|
|
}: SealAnimationEnhancedProps) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
const particlesRef = useRef<Particle[]>([]);
|
|
const animationRef = useRef<number | null>(null);
|
|
const [_currentStage, setCurrentStage] = useState<'idle' | 'dispersing' | 'reforming'>('idle');
|
|
const stageTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
const createSealShape = useCallback((width: number, height: number) => {
|
|
const centerX = width / 2;
|
|
const centerY = height / 2;
|
|
const sealSize = Math.min(width, height) * 0.35;
|
|
const particles: { x: number; y: number }[] = [];
|
|
|
|
for (let i = 0; i < particleCount; i++) {
|
|
const angle = (i / particleCount) * Math.PI * 2;
|
|
const radius = sealSize * (0.8 + Math.random() * 0.4);
|
|
particles.push({
|
|
x: centerX + Math.cos(angle) * radius * (Math.random() > 0.5 ? 1 : 0.8),
|
|
y: centerY + Math.sin(angle) * radius * (Math.random() > 0.5 ? 1 : 0.8),
|
|
});
|
|
}
|
|
|
|
return particles;
|
|
}, [particleCount]);
|
|
|
|
const createParticle = useCallback(
|
|
(x: number, y: number, targetX: number, targetY: number): Particle => {
|
|
const color = colors[Math.floor(Math.random() * colors.length)] ?? '#C41E3A';
|
|
const size = 2 + Math.random() * 3;
|
|
const maxLife = 200 + Math.random() * 100;
|
|
|
|
return {
|
|
x,
|
|
y,
|
|
targetX,
|
|
targetY,
|
|
vx: (Math.random() - 0.5) * 2,
|
|
vy: (Math.random() - 0.5) * 2,
|
|
size,
|
|
opacity: 0.6 + Math.random() * 0.4,
|
|
color,
|
|
life: 0,
|
|
maxLife,
|
|
stage: 'idle',
|
|
};
|
|
},
|
|
[colors]
|
|
);
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) {return;}
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) {return;}
|
|
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
|
|
const sealPositions = createSealShape(width, height);
|
|
particlesRef.current = sealPositions.map((pos) =>
|
|
createParticle(pos.x, pos.y, pos.x, pos.y)
|
|
);
|
|
|
|
if (animationStages) {
|
|
stageTimerRef.current = setTimeout(() => {
|
|
setCurrentStage('dispersing');
|
|
onStageChange?.('dispersing');
|
|
|
|
particlesRef.current.forEach(p => {
|
|
p.vx = (Math.random() - 0.5) * 4;
|
|
p.vy = (Math.random() - 0.5) * 4;
|
|
p.stage = 'dispersing';
|
|
});
|
|
|
|
setTimeout(() => {
|
|
setCurrentStage('reforming');
|
|
onStageChange?.('reforming');
|
|
|
|
particlesRef.current.forEach(p => {
|
|
p.stage = 'reforming';
|
|
});
|
|
|
|
setTimeout(() => {
|
|
setCurrentStage('idle');
|
|
onStageChange?.('idle');
|
|
}, 3000);
|
|
}, 2000);
|
|
}, 3000);
|
|
}
|
|
|
|
const animate = () => {
|
|
ctx.clearRect(0, 0, width, height);
|
|
|
|
particlesRef.current.forEach((particle) => {
|
|
if (particle.stage === 'reforming') {
|
|
const dx = particle.targetX - particle.x;
|
|
const dy = particle.targetY - particle.y;
|
|
particle.vx += dx * 0.02;
|
|
particle.vy += dy * 0.02;
|
|
particle.vx *= 0.95;
|
|
particle.vy *= 0.95;
|
|
}
|
|
|
|
particle.x += particle.vx;
|
|
particle.y += particle.vy;
|
|
particle.life++;
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
|
|
ctx.fillStyle = particle.color;
|
|
ctx.globalAlpha = particle.opacity;
|
|
ctx.fill();
|
|
ctx.globalAlpha = 1;
|
|
});
|
|
|
|
animationRef.current = requestAnimationFrame(animate);
|
|
};
|
|
|
|
animate();
|
|
|
|
return () => {
|
|
if (animationRef.current) {
|
|
cancelAnimationFrame(animationRef.current);
|
|
}
|
|
if (stageTimerRef.current) {
|
|
clearTimeout(stageTimerRef.current);
|
|
}
|
|
};
|
|
}, [width, height, createSealShape, createParticle, animationStages, onStageChange]);
|
|
|
|
return (
|
|
<canvas
|
|
ref={canvasRef}
|
|
className={className}
|
|
style={{ width, height }}
|
|
/>
|
|
);
|
|
}
|