feat: add enhanced seal animation component with stages

This commit is contained in:
张翔
2026-02-13 14:01:01 +08:00
parent 9f1853da95
commit fd5a155434
@@ -0,0 +1,178 @@
'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 = '睿新',
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)];
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, i) =>
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 }}
/>
);
}