feat: add enhanced seal animation component with stages
This commit is contained in:
@@ -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 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user