fecbfd1990
refactor: 优化代码健壮性和类型安全 style: 更新字体样式和全局CSS fix: 修复IntersectionObserver潜在空引用问题 chore: 更新依赖和ESLint配置 build: 更新构建ID和路由配置
584 lines
15 KiB
TypeScript
584 lines
15 KiB
TypeScript
'use client';
|
|
|
|
import { motion } from 'framer-motion';
|
|
import { useMemo, useState, useEffect } from 'react';
|
|
|
|
interface InkDropProps {
|
|
size?: number;
|
|
opacity?: number;
|
|
color?: string;
|
|
blur?: number;
|
|
delay?: number;
|
|
duration?: number;
|
|
className?: string;
|
|
}
|
|
|
|
export function InkDrop({
|
|
size = 20,
|
|
opacity = 0.15,
|
|
color = '#1C1C1C',
|
|
blur = 0,
|
|
delay = 0,
|
|
duration: _duration = 8,
|
|
className = ''
|
|
}: InkDropProps) {
|
|
return (
|
|
<motion.div
|
|
className={`absolute rounded-full ${className}`}
|
|
style={{
|
|
width: size,
|
|
height: size,
|
|
backgroundColor: color,
|
|
opacity,
|
|
filter: blur > 0 ? `blur(${blur}px)` : 'none',
|
|
}}
|
|
initial={{ scale: 0, opacity: 0 }}
|
|
animate={{
|
|
scale: [0.8, 1.2, 1],
|
|
opacity: [0, opacity, opacity],
|
|
}}
|
|
transition={{
|
|
duration: 1.5,
|
|
delay,
|
|
ease: [0.16, 1, 0.3, 1],
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
interface InkSplashProps {
|
|
size?: number;
|
|
color?: string;
|
|
opacity?: number;
|
|
delay?: number;
|
|
className?: string;
|
|
}
|
|
|
|
export function InkSplash({
|
|
size = 60,
|
|
color = '#C41E3A',
|
|
opacity = 0.2,
|
|
delay = 0,
|
|
className = ''
|
|
}: InkSplashProps) {
|
|
return (
|
|
<motion.svg
|
|
viewBox="0 0 100 100"
|
|
width={size}
|
|
height={size}
|
|
className={`absolute ${className}`}
|
|
initial={{ opacity: 0, scale: 0 }}
|
|
animate={{ opacity, scale: 1 }}
|
|
transition={{ duration: 1.2, delay, ease: [0.16, 1, 0.3, 1] }}
|
|
>
|
|
<motion.path
|
|
d="M50 10 Q30 25 35 50 Q30 75 50 90 Q70 75 65 50 Q70 25 50 10"
|
|
fill={color}
|
|
initial={{ pathLength: 0, opacity: 0 }}
|
|
animate={{ pathLength: 1, opacity: 1 }}
|
|
transition={{ duration: 2, delay: delay + 0.3, ease: [0.16, 1, 0.3, 1] }}
|
|
/>
|
|
</motion.svg>
|
|
);
|
|
}
|
|
|
|
interface SealStampProps {
|
|
size?: number;
|
|
color?: string;
|
|
opacity?: number;
|
|
delay?: number;
|
|
className?: string;
|
|
}
|
|
|
|
export function SealStamp({
|
|
size = 40,
|
|
color = '#C41E3A',
|
|
opacity = 0.15,
|
|
delay = 0,
|
|
className = ''
|
|
}: SealStampProps) {
|
|
return (
|
|
<motion.div
|
|
className={`absolute ${className}`}
|
|
style={{
|
|
width: size,
|
|
height: size,
|
|
border: `2px solid ${color}`,
|
|
borderRadius: '4px',
|
|
opacity: 0,
|
|
transform: 'rotate(-8deg)',
|
|
}}
|
|
initial={{ opacity: 0, scale: 1.5, rotate: -20 }}
|
|
animate={{ opacity, scale: 1, rotate: -8 }}
|
|
transition={{
|
|
type: 'spring',
|
|
stiffness: 200,
|
|
damping: 15,
|
|
delay,
|
|
}}
|
|
>
|
|
<motion.div
|
|
className="absolute inset-1 border border-current"
|
|
style={{ borderColor: color, opacity: 0.5 }}
|
|
initial={{ scale: 0.8 }}
|
|
animate={{ scale: 1 }}
|
|
transition={{ duration: 0.5, delay: delay + 0.3 }}
|
|
/>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
interface InkStainProps {
|
|
size?: number;
|
|
color?: string;
|
|
opacity?: number;
|
|
blur?: number;
|
|
delay?: number;
|
|
className?: string;
|
|
}
|
|
|
|
export function InkStain({
|
|
size = 100,
|
|
color = '#1C1C1C',
|
|
opacity = 0.05,
|
|
blur = 20,
|
|
delay = 0,
|
|
className = ''
|
|
}: InkStainProps) {
|
|
return (
|
|
<motion.div
|
|
className={`absolute ${className}`}
|
|
style={{
|
|
width: size,
|
|
height: size * 0.8,
|
|
backgroundColor: color,
|
|
borderRadius: '50% 40% 60% 45%',
|
|
opacity: 0,
|
|
filter: `blur(${blur}px)`,
|
|
}}
|
|
initial={{ opacity: 0, scale: 0.5 }}
|
|
animate={{ opacity, scale: 1 }}
|
|
transition={{ duration: 2, delay, ease: [0.16, 1, 0.3, 1] }}
|
|
/>
|
|
);
|
|
}
|
|
|
|
interface InkLineProps {
|
|
width?: number;
|
|
height?: number;
|
|
color?: string;
|
|
opacity?: number;
|
|
delay?: number;
|
|
className?: string;
|
|
}
|
|
|
|
export function InkLine({
|
|
width = 200,
|
|
height = 2,
|
|
color = '#1C1C1C',
|
|
opacity = 0.1,
|
|
delay = 0,
|
|
className = ''
|
|
}: InkLineProps) {
|
|
return (
|
|
<motion.div
|
|
className={`absolute ${className}`}
|
|
style={{
|
|
width,
|
|
height,
|
|
backgroundColor: color,
|
|
opacity: 0,
|
|
borderRadius: height / 2,
|
|
}}
|
|
initial={{ opacity: 0, scaleX: 0 }}
|
|
animate={{ opacity, scaleX: 1 }}
|
|
transition={{ duration: 1.5, delay, ease: [0.16, 1, 0.3, 1] }}
|
|
/>
|
|
);
|
|
}
|
|
|
|
interface BrushStrokeProps {
|
|
width?: number;
|
|
height?: number;
|
|
color?: string;
|
|
opacity?: number;
|
|
delay?: number;
|
|
className?: string;
|
|
}
|
|
|
|
export function BrushStroke({
|
|
width = 150,
|
|
height = 30,
|
|
color = '#C41E3A',
|
|
opacity = 0.12,
|
|
delay = 0,
|
|
className = ''
|
|
}: BrushStrokeProps) {
|
|
return (
|
|
<motion.svg
|
|
viewBox="0 0 150 30"
|
|
width={width}
|
|
height={height}
|
|
className={`absolute ${className}`}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity }}
|
|
transition={{ duration: 1, delay }}
|
|
>
|
|
<motion.path
|
|
d="M0 15 Q20 5 40 15 Q60 25 80 15 Q100 5 120 15 Q135 20 150 15"
|
|
fill="none"
|
|
stroke={color}
|
|
strokeWidth="3"
|
|
strokeLinecap="round"
|
|
initial={{ pathLength: 0 }}
|
|
animate={{ pathLength: 1 }}
|
|
transition={{ duration: 2, delay: delay + 0.5, ease: [0.16, 1, 0.3, 1] }}
|
|
/>
|
|
</motion.svg>
|
|
);
|
|
}
|
|
|
|
interface FloatingInkProps {
|
|
count?: number;
|
|
className?: string;
|
|
}
|
|
|
|
export function FloatingInk({ count = 15, className = '' }: FloatingInkProps) {
|
|
const [isMounted, setIsMounted] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setIsMounted(true);
|
|
}, []);
|
|
|
|
const elements = useMemo(() => {
|
|
if (!isMounted) {return [];}
|
|
const items = [];
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const type = i % 5;
|
|
const baseDelay = i * 0.15;
|
|
|
|
items.push({
|
|
id: i,
|
|
type,
|
|
delay: baseDelay,
|
|
props: {
|
|
left: `${10 + Math.random() * 80}%`,
|
|
top: `${10 + Math.random() * 80}%`,
|
|
},
|
|
animX: Math.random() * 10 - 5,
|
|
animDuration: 6 + Math.random() * 4,
|
|
size: type === 0 ? 8 + Math.random() * 16 :
|
|
type === 1 ? 4 + Math.random() * 8 :
|
|
type === 2 ? 20 + Math.random() * 30 :
|
|
type === 3 ? 60 + Math.random() * 80 : 80 + Math.random() * 100,
|
|
opacity: type === 0 ? 0.08 + Math.random() * 0.1 :
|
|
type === 1 ? 0.1 + Math.random() * 0.15 :
|
|
type === 2 ? 0.08 + Math.random() * 0.08 :
|
|
type === 3 ? 0.03 + Math.random() * 0.04 : 0.06 + Math.random() * 0.08,
|
|
blur: type === 0 ? Math.random() * 2 : 0,
|
|
height: type === 4 ? 15 + Math.random() * 20 : undefined,
|
|
});
|
|
}
|
|
|
|
return items;
|
|
}, [count, isMounted]);
|
|
|
|
return (
|
|
<div className={`absolute inset-0 pointer-events-none overflow-hidden ${className}`}>
|
|
{elements.map((el) => (
|
|
<motion.div
|
|
key={el.id}
|
|
className="absolute"
|
|
style={el.props}
|
|
animate={{
|
|
y: [0, -20, 0],
|
|
x: [0, el.animX, 0],
|
|
}}
|
|
transition={{
|
|
duration: el.animDuration,
|
|
delay: el.delay,
|
|
repeat: Infinity,
|
|
ease: 'easeInOut',
|
|
}}
|
|
>
|
|
{el.type === 0 && (
|
|
<InkDrop
|
|
size={el.size}
|
|
opacity={el.opacity}
|
|
blur={el.blur}
|
|
delay={el.delay}
|
|
/>
|
|
)}
|
|
{el.type === 1 && (
|
|
<InkDrop
|
|
size={el.size}
|
|
opacity={el.opacity}
|
|
color="#C41E3A"
|
|
delay={el.delay}
|
|
/>
|
|
)}
|
|
{el.type === 2 && (
|
|
<SealStamp
|
|
size={el.size}
|
|
opacity={el.opacity}
|
|
delay={el.delay}
|
|
/>
|
|
)}
|
|
{el.type === 3 && (
|
|
<InkStain
|
|
size={el.size}
|
|
opacity={el.opacity}
|
|
delay={el.delay}
|
|
/>
|
|
)}
|
|
{el.type === 4 && (
|
|
<BrushStroke
|
|
width={el.size}
|
|
height={el.height}
|
|
opacity={el.opacity}
|
|
delay={el.delay}
|
|
/>
|
|
)}
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface InkDecorationProps {
|
|
variant?: 'minimal' | 'balanced' | 'rich';
|
|
className?: string;
|
|
}
|
|
|
|
interface DropPosition {
|
|
left: string;
|
|
top: string;
|
|
size: number;
|
|
opacity: number;
|
|
blur: number;
|
|
isRed: boolean;
|
|
duration: number;
|
|
}
|
|
|
|
interface SplashPosition {
|
|
left: string;
|
|
top: string;
|
|
size: number;
|
|
duration: number;
|
|
}
|
|
|
|
interface SealPosition {
|
|
left: string;
|
|
top: string;
|
|
size: number;
|
|
duration: number;
|
|
}
|
|
|
|
interface StainPosition {
|
|
left: string;
|
|
top: string;
|
|
size: number;
|
|
duration: number;
|
|
}
|
|
|
|
interface StrokePosition {
|
|
left: string;
|
|
top: string;
|
|
width: number;
|
|
duration: number;
|
|
}
|
|
|
|
export function InkDecoration({ variant = 'balanced', className = '' }: InkDecorationProps) {
|
|
const [dropPositions, setDropPositions] = useState<DropPosition[]>([]);
|
|
const [splashPositions, setSplashPositions] = useState<SplashPosition[]>([]);
|
|
const [sealPositions, setSealPositions] = useState<SealPosition[]>([]);
|
|
const [stainPositions, setStainPositions] = useState<StainPosition[]>([]);
|
|
const [strokePositions, setStrokePositions] = useState<StrokePosition[]>([]);
|
|
|
|
const config = {
|
|
minimal: { drops: 3, splashes: 1, seals: 1, stains: 1, strokes: 1 },
|
|
balanced: { drops: 5, splashes: 2, seals: 2, stains: 2, strokes: 2 },
|
|
rich: { drops: 8, splashes: 3, seals: 3, stains: 3, strokes: 3 },
|
|
};
|
|
|
|
const { drops, splashes, seals, stains, strokes } = config[variant];
|
|
|
|
useEffect(() => {
|
|
setDropPositions(Array.from({ length: drops }, (_, i) => ({
|
|
left: `${15 + (i * 70 / drops)}%`,
|
|
top: `${20 + Math.random() * 60}%`,
|
|
size: 6 + Math.random() * 14,
|
|
opacity: 0.06 + Math.random() * 0.1,
|
|
blur: Math.random() * 3,
|
|
isRed: i % 3 === 0,
|
|
duration: 5 + Math.random() * 3,
|
|
})));
|
|
|
|
setSplashPositions(Array.from({ length: splashes }, (_, i) => ({
|
|
left: `${20 + (i * 60 / splashes)}%`,
|
|
top: `${15 + Math.random() * 70}%`,
|
|
size: 40 + Math.random() * 40,
|
|
duration: 7 + Math.random() * 3,
|
|
})));
|
|
|
|
setSealPositions(Array.from({ length: seals }, (_, i) => ({
|
|
left: `${25 + (i * 50 / seals)}%`,
|
|
top: `${25 + Math.random() * 50}%`,
|
|
size: 25 + Math.random() * 25,
|
|
duration: 6 + Math.random() * 2,
|
|
})));
|
|
|
|
setStainPositions(Array.from({ length: stains }, (_, i) => ({
|
|
left: `${10 + (i * 80 / stains)}%`,
|
|
top: `${30 + Math.random() * 40}%`,
|
|
size: 80 + Math.random() * 60,
|
|
duration: 8 + Math.random() * 4,
|
|
})));
|
|
|
|
setStrokePositions(Array.from({ length: strokes }, (_, i) => ({
|
|
left: `${15 + (i * 70 / strokes)}%`,
|
|
top: `${40 + Math.random() * 30}%`,
|
|
width: 100 + Math.random() * 100,
|
|
duration: 6 + Math.random() * 3,
|
|
})));
|
|
}, [drops, splashes, seals, stains, strokes]);
|
|
|
|
return (
|
|
<div className={`absolute inset-0 pointer-events-none overflow-hidden ${className}`}>
|
|
{dropPositions.map((pos, i) => (
|
|
<motion.div
|
|
key={`drop-${i}`}
|
|
className="absolute"
|
|
style={{ left: pos.left, top: pos.top }}
|
|
animate={{
|
|
y: [0, -15, 0],
|
|
scale: [1, 1.1, 1],
|
|
}}
|
|
transition={{
|
|
duration: pos.duration,
|
|
delay: i * 0.2,
|
|
repeat: Infinity,
|
|
ease: 'easeInOut',
|
|
}}
|
|
>
|
|
<InkDrop
|
|
size={pos.size}
|
|
opacity={pos.opacity}
|
|
blur={pos.blur}
|
|
color={pos.isRed ? '#C41E3A' : '#1C1C1C'}
|
|
delay={i * 0.1}
|
|
/>
|
|
</motion.div>
|
|
))}
|
|
|
|
{splashPositions.map((pos, i) => (
|
|
<motion.div
|
|
key={`splash-${i}`}
|
|
className="absolute"
|
|
style={{ left: pos.left, top: pos.top }}
|
|
animate={{
|
|
y: [0, -10, 0],
|
|
rotate: [0, 5, -5, 0],
|
|
}}
|
|
transition={{
|
|
duration: pos.duration,
|
|
delay: i * 0.3,
|
|
repeat: Infinity,
|
|
ease: 'easeInOut',
|
|
}}
|
|
>
|
|
<InkSplash size={pos.size} opacity={0.12} delay={i * 0.15} />
|
|
</motion.div>
|
|
))}
|
|
|
|
{sealPositions.map((pos, i) => (
|
|
<motion.div
|
|
key={`seal-${i}`}
|
|
className="absolute"
|
|
style={{ left: pos.left, top: pos.top }}
|
|
animate={{
|
|
y: [0, -8, 0],
|
|
rotate: [-8, -5, -10, -8],
|
|
}}
|
|
transition={{
|
|
duration: pos.duration,
|
|
delay: i * 0.25,
|
|
repeat: Infinity,
|
|
ease: 'easeInOut',
|
|
}}
|
|
>
|
|
<SealStamp size={pos.size} opacity={0.1} delay={i * 0.2} />
|
|
</motion.div>
|
|
))}
|
|
|
|
{stainPositions.map((pos, i) => (
|
|
<motion.div
|
|
key={`stain-${i}`}
|
|
className="absolute"
|
|
style={{ left: pos.left, top: pos.top }}
|
|
animate={{
|
|
scale: [1, 1.05, 1],
|
|
opacity: [0.04, 0.06, 0.04],
|
|
}}
|
|
transition={{
|
|
duration: pos.duration,
|
|
delay: i * 0.35,
|
|
repeat: Infinity,
|
|
ease: 'easeInOut',
|
|
}}
|
|
>
|
|
<InkStain size={pos.size} opacity={0.05} delay={i * 0.1} />
|
|
</motion.div>
|
|
))}
|
|
|
|
{strokePositions.map((pos, i) => (
|
|
<motion.div
|
|
key={`stroke-${i}`}
|
|
className="absolute"
|
|
style={{ left: pos.left, top: pos.top }}
|
|
animate={{
|
|
x: [0, 10, 0],
|
|
opacity: [0.08, 0.12, 0.08],
|
|
}}
|
|
transition={{
|
|
duration: pos.duration,
|
|
delay: i * 0.3,
|
|
repeat: Infinity,
|
|
ease: 'easeInOut',
|
|
}}
|
|
>
|
|
<BrushStroke width={pos.width} opacity={0.1} delay={i * 0.15} />
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function InkBackground() {
|
|
return (
|
|
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ duration: 2 }}
|
|
className="absolute top-0 left-1/4 w-[500px] h-[500px] bg-[radial-gradient(ellipse_at_center,rgba(28,28,28,0.02)_0%,transparent_70%)]"
|
|
/>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ duration: 2, delay: 0.3 }}
|
|
className="absolute bottom-0 right-1/4 w-[400px] h-[400px] bg-[radial-gradient(ellipse_at_center,rgba(196,30,58,0.03)_0%,transparent_60%)]"
|
|
/>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ duration: 2, delay: 0.6 }}
|
|
className="absolute top-1/3 right-0 w-[300px] h-[300px] bg-[radial-gradient(ellipse_at_center,rgba(28,28,28,0.015)_0%,transparent_50%)]"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|