feat: 添加预览效果页面并优化交互效果

refactor: 优化代码健壮性和类型安全

style: 更新字体样式和全局CSS

fix: 修复IntersectionObserver潜在空引用问题

chore: 更新依赖和ESLint配置

build: 更新构建ID和路由配置
This commit is contained in:
张翔
2026-02-24 10:24:05 +08:00
parent 64165c4499
commit fecbfd1990
239 changed files with 3403 additions and 5181 deletions
@@ -119,7 +119,7 @@ function FloatingLine({
}
interface FloatingIconProps {
icon: any;
icon?: any;
size?: number;
color?: string;
delay?: number;
@@ -329,7 +329,7 @@ export function AdvancedFloatingEffects({
const iconsList = [Cpu, Shield, Zap, Globe, FileText, TrendingUp, BarChart3, Users];
const elements = useMemo(() => {
if (!isMounted) return [];
if (!isMounted) {return [];}
const items = [];
const width = typeof window !== 'undefined' ? window.innerWidth : 1920;
@@ -420,7 +420,7 @@ export function AdvancedFloatingEffects({
}, [orbs, icons, rings, lines, dots, isMounted, iconsList]);
const getParallaxStyle = (depth: number) => {
if (variant !== 'parallax') return {};
if (variant !== 'parallax') {return {};}
const y = useTransform(scrollY, [0, 500], [0, -depth * 100]);
return { y };
};
@@ -0,0 +1,195 @@
'use client';
import { motion, useReducedMotion } from 'framer-motion';
import { useEffect, useState } from 'react';
interface DataParticleFlowProps {
className?: string;
particleCount?: number;
color?: string;
intensity?: 'subtle' | 'normal' | 'prominent';
shape?: 'circle' | 'square' | 'triangle' | 'diamond' | 'star' | 'mixed';
effect?: 'default' | 'pulse' | 'glow' | 'trail';
}
interface Particle {
id: number;
x: number;
y: number;
size: number;
duration: number;
delay: number;
opacity: number;
moveRange: number;
shape: 'circle' | 'square' | 'triangle' | 'diamond' | 'star';
rotation: number;
}
export function DataParticleFlow({
className = '',
particleCount = 50,
color = '#C41E3A',
intensity = 'normal',
shape = 'circle',
effect = 'default',
}: DataParticleFlowProps) {
const prefersReducedMotion = useReducedMotion();
const [particles, setParticles] = useState<Particle[]>([]);
useEffect(() => {
const intensityConfig = {
subtle: { sizeMin: 3, sizeMax: 8, opacityMin: 0.2, opacityMax: 0.4, moveRange: 80 },
normal: { sizeMin: 6, sizeMax: 16, opacityMin: 0.4, opacityMax: 0.7, moveRange: 150 },
prominent: { sizeMin: 10, sizeMax: 24, opacityMin: 0.5, opacityMax: 0.9, moveRange: 200 },
};
const shapes: Particle['shape'][] = ['circle', 'square', 'triangle', 'diamond', 'star'];
const config = intensityConfig[intensity];
const generated: Particle[] = Array.from({ length: particleCount }, (_, i) => ({
id: i,
x: Math.random() * 100,
y: Math.random() * 100,
size: Math.random() * (config.sizeMax - config.sizeMin) + config.sizeMin,
duration: Math.random() * 12 + 8,
delay: Math.random() * 3,
opacity: Math.random() * (config.opacityMax - config.opacityMin) + config.opacityMin,
moveRange: config.moveRange,
shape: shape === 'mixed' ? (shapes[Math.floor(Math.random() * shapes.length)] ?? 'circle') : shape as Particle['shape'],
rotation: Math.random() * 360,
}));
setParticles(generated);
}, [particleCount, intensity, shape]);
const getShapeStyles = (particle: Particle): React.CSSProperties => {
const baseStyles: React.CSSProperties = {
width: particle.size,
height: particle.size,
left: `${particle.x}%`,
top: `${particle.y}%`,
willChange: prefersReducedMotion ? 'auto' : 'transform, opacity',
};
switch (particle.shape) {
case 'circle':
return {
...baseStyles,
borderRadius: '50%',
background: `radial-gradient(circle, ${color} 0%, ${color}80 40%, transparent 70%)`,
boxShadow: effect === 'glow' ? `0 0 ${particle.size * 2}px ${color}60` : 'none',
};
case 'square':
return {
...baseStyles,
borderRadius: '2px',
background: `linear-gradient(135deg, ${color} 0%, ${color}60 100%)`,
boxShadow: effect === 'glow' ? `0 0 ${particle.size}px ${color}40` : 'none',
};
case 'triangle':
return {
...baseStyles,
width: 0,
height: 0,
background: 'transparent',
borderLeft: `${particle.size / 2}px solid transparent`,
borderRight: `${particle.size / 2}px solid transparent`,
borderBottom: `${particle.size}px solid ${color}`,
filter: effect === 'glow' ? `drop-shadow(0 0 ${particle.size / 2}px ${color}60)` : 'none',
};
case 'diamond':
return {
...baseStyles,
transform: `rotate(45deg)`,
background: `linear-gradient(135deg, ${color} 0%, ${color}60 100%)`,
boxShadow: effect === 'glow' ? `0 0 ${particle.size}px ${color}40` : 'none',
};
case 'star':
return {
...baseStyles,
clipPath: 'polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)',
background: `radial-gradient(circle, ${color} 0%, ${color}80 100%)`,
filter: effect === 'glow' ? `drop-shadow(0 0 ${particle.size / 2}px ${color}60)` : 'none',
};
default:
return baseStyles;
}
};
const getAnimationVariants = (particle: Particle) => {
if (prefersReducedMotion) {
return { scale: 1, opacity: particle.opacity };
}
const baseAnimation = {
scale: [0, 2, 1.5, 2.5, 0],
opacity: [0, particle.opacity, particle.opacity * 0.8, particle.opacity, 0],
y: [0, -particle.moveRange * 0.5, -particle.moveRange, -particle.moveRange * 1.5, -particle.moveRange * 2],
x: [0, particle.moveRange * 0.3, -particle.moveRange * 0.2, particle.moveRange * 0.15, 0],
};
switch (effect) {
case 'pulse':
return {
...baseAnimation,
scale: [0, 1.5, 1, 1.8, 0],
};
case 'glow':
return {
...baseAnimation,
opacity: [0, particle.opacity, particle.opacity * 1.2, particle.opacity, 0],
};
case 'trail':
return {
...baseAnimation,
y: [particle.moveRange * 0.5, -particle.moveRange * 0.5, -particle.moveRange * 1.5, -particle.moveRange * 2.5, -particle.moveRange * 3],
};
default:
return baseAnimation;
}
};
return (
<div className={`absolute inset-0 overflow-hidden ${className}`} aria-hidden="true">
{particles.map((particle) => (
<motion.div
key={particle.id}
className="absolute"
style={getShapeStyles(particle)}
initial={{ scale: 0, opacity: 0 }}
animate={getAnimationVariants(particle)}
transition={{
duration: particle.duration,
repeat: Infinity,
ease: 'easeInOut',
delay: particle.delay,
}}
/>
))}
<svg className="absolute inset-0 w-full h-full opacity-15">
<defs>
<pattern id="dataGrid" width="60" height="60" patternUnits="userSpaceOnUse">
<circle cx="30" cy="30" r="1.5" fill={color} opacity="0.4" />
<circle cx="0" cy="0" r="1" fill={color} opacity="0.2" />
<circle cx="60" cy="0" r="1" fill={color} opacity="0.2" />
<circle cx="0" cy="60" r="1" fill={color} opacity="0.2" />
<circle cx="60" cy="60" r="1" fill={color} opacity="0.2" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#dataGrid)" />
</svg>
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-white/20" />
</div>
);
}
export default DataParticleFlow;
@@ -27,7 +27,7 @@ export function FluidWaveBackground({
const sceneRef = useRef<THREE.Scene | null>(null);
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
const meshRef = useRef<THREE.Mesh | null>(null);
const animationRef = useRef<number>();
const animationRef = useRef<number | undefined>(undefined);
const mouseRef = useRef({ x: 0, y: 0, active: false });
const vertexShader = `
@@ -102,7 +102,7 @@ export function FluidWaveBackground({
`;
useEffect(() => {
if (!containerRef.current) return;
if (!containerRef.current) {return;}
const container = containerRef.current;
const width = container.clientWidth;
@@ -154,14 +154,22 @@ export function FluidWaveBackground({
const animate = (time: number) => {
if (meshRef.current && rendererRef.current && sceneRef.current && cameraRef.current) {
const material = meshRef.current.material as THREE.ShaderMaterial;
material.uniforms.uTime.value = time * speed;
if (material.uniforms.uTime) {
material.uniforms.uTime.value = time * speed;
}
if (mouseRef.current.active) {
material.uniforms.uMouse.value.x = mouseRef.current.x;
material.uniforms.uMouse.value.y = mouseRef.current.y;
material.uniforms.uMouseActive.value = 1.0;
if (material.uniforms.uMouse) {
material.uniforms.uMouse.value.x = mouseRef.current.x;
material.uniforms.uMouse.value.y = mouseRef.current.y;
}
if (material.uniforms.uMouseActive) {
material.uniforms.uMouseActive.value = 1.0;
}
} else {
material.uniforms.uMouseActive.value = 0.0;
if (material.uniforms.uMouseActive) {
material.uniforms.uMouseActive.value = 0.0;
}
}
rendererRef.current.render(sceneRef.current, cameraRef.current);
@@ -170,7 +178,7 @@ export function FluidWaveBackground({
};
const handleMouseMove = (event: MouseEvent) => {
if (!containerRef.current) return;
if (!containerRef.current) {return;}
const rect = containerRef.current.getBoundingClientRect();
mouseRef.current.x = (event.clientX - rect.left) / rect.width;
mouseRef.current.y = 1.0 - (event.clientY - rect.top) / rect.height;
@@ -187,7 +195,7 @@ export function FluidWaveBackground({
animate(0);
const handleResize = () => {
if (!containerRef.current || !cameraRef.current || !rendererRef.current) return;
if (!containerRef.current || !cameraRef.current || !rendererRef.current) {return;}
const newWidth = containerRef.current.clientWidth;
const newHeight = containerRef.current.clientHeight;
@@ -0,0 +1,163 @@
'use client';
import { motion, useReducedMotion } from 'framer-motion';
import { useEffect, useState } from 'react';
interface GeometricAbstractProps {
className?: string;
variant?: 'minimal' | 'complex' | 'dynamic';
color?: string;
}
interface Shape {
id: number;
type: 'circle' | 'square' | 'triangle';
x: number;
y: number;
size: number;
rotation: number;
opacity: number;
duration: number;
delay: number;
}
export function GeometricAbstract({
className = '',
variant = 'minimal',
color = '#C41E3A',
}: GeometricAbstractProps) {
const prefersReducedMotion = useReducedMotion();
const [shapes, setShapes] = useState<Shape[]>([]);
useEffect(() => {
const count = variant === 'complex' ? 15 : variant === 'dynamic' ? 20 : 8;
const generated: Shape[] = Array.from({ length: count }, (_, i) => ({
id: i,
type: ['circle', 'square', 'triangle'][Math.floor(Math.random() * 3)] as Shape['type'],
x: Math.random() * 100,
y: Math.random() * 100,
size: Math.random() * 100 + 50,
rotation: Math.random() * 360,
opacity: Math.random() * 0.08 + 0.02,
duration: Math.random() * 20 + 15,
delay: Math.random() * 3,
}));
setShapes(generated);
}, [variant]);
const renderShape = (shape: Shape) => {
const baseStyle = {
position: 'absolute' as const,
left: `${shape.x}%`,
top: `${shape.y}%`,
width: shape.size,
height: shape.size,
opacity: shape.opacity,
};
switch (shape.type) {
case 'circle':
return (
<motion.div
key={shape.id}
style={{
...baseStyle,
borderRadius: '50%',
border: `1px solid ${color}`,
background: `radial-gradient(circle, ${color}10 0%, transparent 70%)`,
}}
animate={
prefersReducedMotion
? {}
: {
scale: [1, 1.2, 1],
rotate: [0, 180, 360],
opacity: [shape.opacity, shape.opacity * 1.5, shape.opacity],
}
}
transition={{
duration: shape.duration,
repeat: Infinity,
ease: 'easeInOut',
delay: shape.delay,
}}
/>
);
case 'square':
return (
<motion.div
key={shape.id}
style={{
...baseStyle,
border: `1px solid ${color}`,
background: `linear-gradient(135deg, ${color}08 0%, transparent 100%)`,
}}
animate={
prefersReducedMotion
? {}
: {
rotate: [shape.rotation, shape.rotation + 90, shape.rotation],
scale: [1, 1.1, 1],
opacity: [shape.opacity, shape.opacity * 1.3, shape.opacity],
}
}
transition={{
duration: shape.duration,
repeat: Infinity,
ease: 'easeInOut',
delay: shape.delay,
}}
/>
);
case 'triangle':
return (
<motion.div
key={shape.id}
style={{
...baseStyle,
clipPath: 'polygon(50% 0%, 0% 100%, 100% 100%)',
background: `linear-gradient(135deg, ${color}10 0%, transparent 100%)`,
}}
animate={
prefersReducedMotion
? {}
: {
rotate: [0, 120, 240, 360],
scale: [1, 1.15, 1],
}
}
transition={{
duration: shape.duration,
repeat: Infinity,
ease: 'easeInOut',
delay: shape.delay,
}}
/>
);
default:
return null;
}
};
return (
<div className={`absolute inset-0 overflow-hidden ${className}`} aria-hidden="true">
{shapes.map(renderShape)}
<svg className="absolute inset-0 w-full h-full opacity-5">
<defs>
<pattern id="geoGrid" width="60" height="60" patternUnits="userSpaceOnUse">
<path d="M 60 0 L 0 0 0 60" fill="none" stroke={color} strokeWidth="0.5" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#geoGrid)" />
</svg>
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-white/40" />
</div>
);
}
export default GeometricAbstract;
@@ -0,0 +1,70 @@
'use client';
import { motion, useReducedMotion } from 'framer-motion';
interface GradientFlowOptimizedProps {
className?: string;
colors?: string[];
duration?: number;
variant?: 'smooth' | 'dynamic' | 'minimal';
}
export function GradientFlowOptimized({
className = '',
colors = ['#FAFAFA', '#FFE8EC', '#FFF0F3', '#F5F5F5', '#FFD6DD'],
duration = 15,
variant = 'smooth',
}: GradientFlowOptimizedProps) {
const prefersReducedMotion = useReducedMotion();
const gradientStyle = {
background: `linear-gradient(135deg, ${colors.join(', ')})`,
backgroundSize: '400% 400%',
};
const variants = {
smooth: {
backgroundPosition: ['0% 50%', '100% 50%', '0% 50%'],
},
dynamic: {
backgroundPosition: ['0% 0%', '100% 100%', '0% 50%', '100% 0%', '0% 0%'],
},
minimal: {
backgroundPosition: ['0% 50%', '50% 50%', '0% 50%'],
},
};
if (prefersReducedMotion) {
return (
<div
className={`absolute inset-0 ${className}`}
style={{
...gradientStyle,
backgroundPosition: '50% 50%',
}}
aria-hidden="true"
/>
);
}
return (
<div className={`absolute inset-0 overflow-hidden ${className}`} aria-hidden="true">
<motion.div
className="absolute inset-0"
style={{
...gradientStyle,
willChange: 'background-position',
}}
animate={variants[variant]}
transition={{
duration,
repeat: Infinity,
ease: 'linear',
}}
/>
<div className="absolute inset-0 backdrop-blur-[100px] opacity-50" />
</div>
);
}
export default GradientFlowOptimized;
+92
View File
@@ -0,0 +1,92 @@
'use client';
import { motion, useReducedMotion } from 'framer-motion';
import { useEffect, useState } from 'react';
interface GradientOrbsProps {
className?: string;
count?: number;
}
interface Orb {
id: number;
size: number;
x: number;
y: number;
color: string;
duration: number;
delay: number;
}
const colorPalette = [
'rgba(196, 30, 58, 0.15)',
'rgba(255, 232, 236, 0.2)',
'rgba(255, 240, 243, 0.18)',
'rgba(245, 245, 245, 0.15)',
'rgba(255, 214, 221, 0.2)',
'rgba(224, 74, 104, 0.12)',
];
export function GradientOrbs({ className = '', count = 5 }: GradientOrbsProps) {
const prefersReducedMotion = useReducedMotion();
const [orbs, setOrbs] = useState<Orb[]>([]);
useEffect(() => {
const generatedOrbs: Orb[] = Array.from({ length: count }, (_, i) => ({
id: i,
size: Math.random() * 400 + 200,
x: Math.random() * 100,
y: Math.random() * 100,
color: colorPalette[i % colorPalette.length] ?? 'rgba(196, 30, 58, 0.15)',
duration: Math.random() * 20 + 15,
delay: Math.random() * 5,
}));
setOrbs(generatedOrbs);
}, [count]);
return (
<div
className={`absolute inset-0 overflow-hidden ${className}`}
aria-hidden="true"
>
{orbs.map((orb) => (
<motion.div
key={orb.id}
className="absolute rounded-full"
style={{
width: orb.size,
height: orb.size,
left: `${orb.x}%`,
top: `${orb.y}%`,
background: `radial-gradient(circle, ${orb.color} 0%, transparent 70%)`,
willChange: prefersReducedMotion ? 'auto' : 'transform',
filter: 'blur(60px)',
}}
initial={{
x: '-50%',
y: '-50%',
scale: 1,
}}
animate={
prefersReducedMotion
? {}
: {
x: ['-50%', '-40%', '-60%', '-50%'],
y: ['-50%', '-60%', '-40%', '-50%'],
scale: [1, 1.2, 0.9, 1],
}
}
transition={{
duration: orb.duration,
repeat: Infinity,
ease: 'easeInOut',
delay: orb.delay,
}}
/>
))}
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-white/20 pointer-events-none" />
</div>
);
}
export default GradientOrbs;
-78
View File
@@ -1,78 +0,0 @@
'use client';
import { useEffect, useRef } from 'react';
import { gsap } from 'gsap';
interface GSAPAnimationProps {
className?: string;
color?: string;
}
export function GSAPAnimation({
className = '',
color = '#C41E3A'
}: GSAPAnimationProps) {
const containerRef = useRef<HTMLDivElement>(null);
const elementsRef = useRef<HTMLDivElement[]>([]);
useEffect(() => {
if (!containerRef.current) return;
const elements = elementsRef.current;
elements.forEach((el, i) => {
gsap.fromTo(el,
{
opacity: 0,
scale: 0,
rotation: 0
},
{
opacity: 0.15,
scale: 1,
rotation: 360,
duration: 8,
delay: i * 1.5,
repeat: -1,
yoyo: true,
ease: 'power2.inOut'
}
);
});
return () => {
gsap.killTweensOf(elements);
};
}, []);
const shapes = [
{ type: 'circle', size: 100, x: 15, y: 20 },
{ type: 'square', size: 80, x: 75, y: 15 },
{ type: 'circle', size: 60, x: 65, y: 65 },
{ type: 'square', size: 50, x: 20, y: 70 }
];
return (
<div ref={containerRef} className={`absolute inset-0 pointer-events-none ${className}`}>
{shapes.map((shape, index) => (
<div
key={index}
ref={el => {
if (el) elementsRef.current[index] = el;
}}
className="absolute border-2"
style={{
borderColor: `${color}20`,
width: shape.size,
height: shape.size,
left: `${shape.x}%`,
top: `${shape.y}%`,
borderRadius: shape.type === 'circle' ? '50%' : '0'
}}
/>
))}
</div>
);
}
export default GSAPAnimation;
+135
View File
@@ -0,0 +1,135 @@
'use client';
import { motion, useReducedMotion } from 'framer-motion';
import { useEffect, useState } from 'react';
interface InkTechFusionProps {
className?: string;
variant?: 'subtle' | 'prominent' | 'dynamic';
primaryColor?: string;
secondaryColor?: string;
}
interface InkBlob {
id: number;
x: number;
y: number;
size: number;
opacity: number;
duration: number;
delay: number;
color: string;
}
export function InkTechFusion({
className = '',
variant = 'subtle',
primaryColor = '#C41E3A',
secondaryColor = '#1C1C1C',
}: InkTechFusionProps) {
const prefersReducedMotion = useReducedMotion();
const [blobs, setBlobs] = useState<InkBlob[]>([]);
useEffect(() => {
const count = variant === 'prominent' ? 8 : variant === 'dynamic' ? 12 : 5;
const generated: InkBlob[] = Array.from({ length: count }, (_, i) => ({
id: i,
x: Math.random() * 100,
y: Math.random() * 100,
size: Math.random() * 300 + 100,
opacity: Math.random() * 0.06 + 0.02,
duration: Math.random() * 25 + 20,
delay: Math.random() * 5,
color: i % 2 === 0 ? primaryColor : secondaryColor,
}));
setBlobs(generated);
}, [variant, primaryColor, secondaryColor]);
return (
<div className={`absolute inset-0 overflow-hidden ${className}`} aria-hidden="true">
{blobs.map((blob) => (
<motion.div
key={blob.id}
className="absolute"
style={{
left: `${blob.x}%`,
top: `${blob.y}%`,
width: blob.size,
height: blob.size,
background: `radial-gradient(circle, ${blob.color}${Math.round(blob.opacity * 255).toString(16).padStart(2, '0')} 0%, transparent 70%)`,
borderRadius: '50%',
filter: 'blur(40px)',
willChange: prefersReducedMotion ? 'auto' : 'transform',
}}
initial={{ scale: 0.8, opacity: 0 }}
animate={
prefersReducedMotion
? { scale: 1, opacity: blob.opacity }
: {
scale: [0.8, 1.2, 0.9, 1.1, 0.8],
opacity: [0, blob.opacity, blob.opacity * 1.2, blob.opacity, 0],
x: [0, 30, -20, 10, 0],
y: [0, -20, 30, -10, 0],
}
}
transition={{
duration: blob.duration,
repeat: Infinity,
ease: 'easeInOut',
delay: blob.delay,
}}
/>
))}
<svg className="absolute inset-0 w-full h-full opacity-10">
<defs>
<filter id="ink-blur">
<feGaussianBlur in="SourceGraphic" stdDeviation="3" />
</filter>
<pattern id="ink-texture" width="100" height="100" patternUnits="userSpaceOnUse">
<circle cx="50" cy="50" r="1" fill={primaryColor} opacity="0.2" />
<circle cx="25" cy="75" r="0.5" fill={secondaryColor} opacity="0.15" />
<circle cx="75" cy="25" r="0.8" fill={primaryColor} opacity="0.18" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#ink-texture)" filter="url(#ink-blur)" />
</svg>
<svg className="absolute inset-0 w-full h-full opacity-5">
<defs>
<linearGradient id="tech-line-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor={primaryColor} stopOpacity="0" />
<stop offset="50%" stopColor={primaryColor} stopOpacity="0.3" />
<stop offset="100%" stopColor={primaryColor} stopOpacity="0" />
</linearGradient>
</defs>
<motion.line
x1="0%"
y1="30%"
x2="100%"
y2="70%"
stroke="url(#tech-line-gradient)"
strokeWidth="1"
initial={{ pathLength: 0 }}
animate={prefersReducedMotion ? { pathLength: 1 } : { pathLength: [0, 1, 1, 0] }}
transition={{ duration: 15, repeat: Infinity, ease: 'easeInOut' }}
/>
<motion.line
x1="0%"
y1="70%"
x2="100%"
y2="30%"
stroke="url(#tech-line-gradient)"
strokeWidth="1"
initial={{ pathLength: 0 }}
animate={prefersReducedMotion ? { pathLength: 1 } : { pathLength: [0, 1, 1, 0] }}
transition={{ duration: 18, repeat: Infinity, ease: 'easeInOut', delay: 3 }}
/>
</svg>
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-white/50" />
</div>
);
}
export default InkTechFusion;
+77 -84
View File
@@ -1,96 +1,89 @@
'use client';
import { motion } from 'framer-motion';
import { motion, useReducedMotion } from 'framer-motion';
interface MeshGradientProps {
className?: string;
variant?: 'default' | 'warm' | 'cool' | 'elegant';
}
export function MeshGradient({
className = ''
}: MeshGradientProps) {
const gradientVariants = {
default: {
colors: [
'radial-gradient(at 40% 20%, hsla(280,80%,90%,0.3) 0px, transparent 50%)',
'radial-gradient(at 80% 0%, hsla(189,100%,56%,0.2) 0px, transparent 50%)',
'radial-gradient(at 0% 50%, hsla(355,100%,93%,0.3) 0px, transparent 50%)',
'radial-gradient(at 80% 50%, hsla(340,100%,76%,0.2) 0px, transparent 50%)',
'radial-gradient(at 0% 100%, hsla(22,100%,77%,0.3) 0px, transparent 50%)',
'radial-gradient(at 80% 100%, hsla(242,100%,70%,0.2) 0px, transparent 50%)',
'radial-gradient(at 0% 0%, hsla(343,100%,76%,0.2) 0px, transparent 50%)',
],
},
warm: {
colors: [
'radial-gradient(at 40% 20%, hsla(15,90%,85%,0.4) 0px, transparent 50%)',
'radial-gradient(at 80% 0%, hsla(30,100%,80%,0.3) 0px, transparent 50%)',
'radial-gradient(at 0% 50%, hsla(0,100%,94%,0.4) 0px, transparent 50%)',
'radial-gradient(at 80% 50%, hsla(20,100%,85%,0.3) 0px, transparent 50%)',
'radial-gradient(at 0% 100%, hsla(10,100%,90%,0.4) 0px, transparent 50%)',
],
},
cool: {
colors: [
'radial-gradient(at 40% 20%, hsla(200,80%,90%,0.3) 0px, transparent 50%)',
'radial-gradient(at 80% 0%, hsla(220,100%,85%,0.2) 0px, transparent 50%)',
'radial-gradient(at 0% 50%, hsla(180,100%,90%,0.3) 0px, transparent 50%)',
'radial-gradient(at 80% 50%, hsla(240,80%,90%,0.2) 0px, transparent 50%)',
],
},
elegant: {
colors: [
'radial-gradient(at 40% 20%, hsla(0,70%,90%,0.25) 0px, transparent 50%)',
'radial-gradient(at 80% 0%, hsla(0,60%,95%,0.2) 0px, transparent 50%)',
'radial-gradient(at 0% 50%, hsla(350,80%,92%,0.25) 0px, transparent 50%)',
'radial-gradient(at 80% 50%, hsla(0,50%,97%,0.2) 0px, transparent 50%)',
],
},
};
export function MeshGradient({ className = '', variant = 'default' }: MeshGradientProps) {
const prefersReducedMotion = useReducedMotion();
const { colors } = gradientVariants[variant];
return (
<div className={`absolute inset-0 overflow-hidden ${className}`}>
<motion.div
className="absolute w-[800px] h-[800px] rounded-full"
style={{
background: 'radial-gradient(circle, rgba(196,30,58,0.15) 0%, transparent 70%)',
filter: 'blur(60px)'
}}
animate={{
x: [0, 100, 50, 0],
y: [0, 50, 100, 0],
scale: [1, 1.2, 0.9, 1]
}}
transition={{
duration: 20,
repeat: Infinity,
ease: 'easeInOut'
}}
initial={{ left: '10%', top: '20%' }}
/>
<motion.div
className="absolute w-[600px] h-[600px] rounded-full"
style={{
background: 'radial-gradient(circle, rgba(212,165,116,0.12) 0%, transparent 70%)',
filter: 'blur(50px)'
}}
animate={{
x: [0, -80, -40, 0],
y: [0, 80, 40, 0],
scale: [1, 0.9, 1.1, 1]
}}
transition={{
duration: 18,
repeat: Infinity,
ease: 'easeInOut',
delay: 2
}}
initial={{ right: '15%', top: '30%' }}
/>
<motion.div
className="absolute w-[500px] h-[500px] rounded-full"
style={{
background: 'radial-gradient(circle, rgba(139,69,19,0.1) 0%, transparent 70%)',
filter: 'blur(40px)'
}}
animate={{
x: [0, 60, -30, 0],
y: [0, -60, 30, 0],
scale: [1, 1.15, 0.95, 1]
}}
transition={{
duration: 22,
repeat: Infinity,
ease: 'easeInOut',
delay: 4
}}
initial={{ left: '30%', bottom: '20%' }}
/>
<motion.div
className="absolute w-[400px] h-[400px] rounded-full"
style={{
background: 'radial-gradient(circle, rgba(196,30,58,0.08) 0%, transparent 70%)',
filter: 'blur(30px)'
}}
animate={{
x: [0, -50, 80, 0],
y: [0, 40, -50, 0],
scale: [1, 1.1, 0.85, 1]
}}
transition={{
duration: 16,
repeat: Infinity,
ease: 'easeInOut',
delay: 1
}}
initial={{ right: '25%', bottom: '30%' }}
/>
<div
className={`absolute inset-0 overflow-hidden ${className}`}
aria-hidden="true"
>
{colors.map((gradient, index) => (
<motion.div
key={index}
className="absolute inset-0"
style={{
background: gradient,
willChange: prefersReducedMotion ? 'auto' : 'transform, opacity',
}}
animate={
prefersReducedMotion
? {}
: {
scale: [1, 1.1, 1],
opacity: [0.6, 0.8, 0.6],
x: [0, 10, 0],
y: [0, -10, 0],
}
}
transition={{
duration: 20 + index * 2,
repeat: Infinity,
ease: 'easeInOut',
delay: index * 0.5,
}}
/>
))}
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-white/30" />
</div>
);
}
export default MeshGradient;
export default MeshGradient;
@@ -51,13 +51,13 @@ export function MouseInteractiveParticles({
}, []);
useEffect(() => {
if (!isMounted) return;
if (!isMounted) {return;}
const canvas = canvasRef.current;
if (!canvas) return;
if (!canvas) {return;}
const ctx = canvas.getContext('2d');
if (!ctx) return;
if (!ctx) {return;}
let width = window.innerWidth;
let height = window.innerHeight;
@@ -87,7 +87,7 @@ export function MouseInteractiveParticles({
vy: (Math.random() - 0.5) * 0.5,
size: Math.random() * 3 + 1,
opacity: Math.random() * 0.5 + 0.2,
color: colors[Math.floor(Math.random() * colors.length)],
color: colors[Math.floor(Math.random() * colors.length)] ?? '#C41E3A',
life: Math.random() * Math.PI * 2,
});
}
@@ -143,7 +143,7 @@ export function MouseInteractiveParticles({
ctx.fill();
particlesRef.current.forEach((otherParticle, j) => {
if (i === j) return;
if (i === j) {return;}
const dx = particle.x - otherParticle.x;
const dy = particle.y - otherParticle.y;
const distance = Math.sqrt(dx * dx + dy * dy);
@@ -181,7 +181,7 @@ export function MouseInteractiveParticles({
};
}, [isMounted, particleCount, getColors, interactionRadius]);
if (!isMounted) return null;
if (!isMounted) {return null;}
return (
<canvas
+1 -1
View File
@@ -19,7 +19,7 @@ export function ParallaxEffect({
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!containerRef.current) return;
if (!containerRef.current) {return;}
const rect = containerRef.current.getBoundingClientRect();
const centerX = rect.width / 2;
+18 -14
View File
@@ -1,7 +1,7 @@
'use client';
import { useEffect, useRef, useState, useCallback } from 'react';
import { motion, useMotionValue, useTransform } from 'framer-motion';
import { motion, useMotionValue } from 'framer-motion';
interface Particle {
x: number;
@@ -31,7 +31,7 @@ export function ParticleGalaxy({
}: ParticleGalaxyProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const particlesRef = useRef<Particle[]>([]);
const animationRef = useRef<number>();
const animationRef = useRef<number | undefined>(undefined);
const [isVisible, setIsVisible] = useState(false);
const mouseX = useMotionValue(0);
@@ -83,8 +83,8 @@ export function ParticleGalaxy({
x += vx;
y += vy;
if (x < 0 || x > width) vx *= -1;
if (y < 0 || y > height) vy *= -1;
if (x < 0 || x > width) {vx *= -1;}
if (y < 0 || y > height) {vy *= -1;}
x = Math.max(0, Math.min(width, x));
y = Math.max(0, Math.min(height, y));
@@ -108,15 +108,19 @@ export function ParticleGalaxy({
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const p1 = particles[i];
const p2 = particles[j];
if (!p1 || !p2) {continue;}
const dx = p1.x - p2.x;
const dy = p1.y - p2.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < connectionDistance) {
const opacity = (1 - distance / connectionDistance) * 0.3;
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.strokeStyle = `rgba(${lineColor}, ${opacity})`;
ctx.lineWidth = 0.5;
ctx.stroke();
@@ -127,10 +131,10 @@ export function ParticleGalaxy({
const animate = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
if (!canvas) {return;}
const ctx = canvas.getContext('2d');
if (!ctx) return;
if (!ctx) {return;}
drawParticles(ctx, canvas.width, canvas.height);
animationRef.current = requestAnimationFrame(animate);
@@ -138,10 +142,10 @@ export function ParticleGalaxy({
const handleResize = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
if (!canvas) {return;}
const container = canvas.parentElement;
if (!container) return;
if (!container) {return;}
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
@@ -151,10 +155,10 @@ export function ParticleGalaxy({
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
if (!canvas) {return;}
const container = canvas.parentElement;
if (!container) return;
if (!container) {return;}
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
@@ -33,7 +33,7 @@ export function SealAnimationEnhanced({
height = 300,
particleCount = 150,
colors = ['#C41E3A', '#D4A574', '#8B4513'],
sealText = '睿新',
sealText: _sealText = '睿新',
animationStages = true,
onStageChange,
className = '',
@@ -41,7 +41,7 @@ export function SealAnimationEnhanced({
const canvasRef = useRef<HTMLCanvasElement>(null);
const particlesRef = useRef<Particle[]>([]);
const animationRef = useRef<number | null>(null);
const [currentStage, setCurrentStage] = useState<'idle' | 'dispersing' | 'reforming'>('idle');
const [_currentStage, setCurrentStage] = useState<'idle' | 'dispersing' | 'reforming'>('idle');
const stageTimerRef = useRef<NodeJS.Timeout | null>(null);
const createSealShape = useCallback((width: number, height: number) => {
@@ -64,7 +64,7 @@ export function SealAnimationEnhanced({
const createParticle = useCallback(
(x: number, y: number, targetX: number, targetY: number): Particle => {
const color = colors[Math.floor(Math.random() * colors.length)];
const color = colors[Math.floor(Math.random() * colors.length)] ?? '#C41E3A';
const size = 2 + Math.random() * 3;
const maxLife = 200 + Math.random() * 100;
@@ -88,16 +88,16 @@ export function SealAnimationEnhanced({
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
if (!canvas) {return;}
const ctx = canvas.getContext('2d');
if (!ctx) return;
if (!ctx) {return;}
canvas.width = width;
canvas.height = height;
const sealPositions = createSealShape(width, height);
particlesRef.current = sealPositions.map((pos, i) =>
particlesRef.current = sealPositions.map((pos) =>
createParticle(pos.x, pos.y, pos.x, pos.y)
);
+106
View File
@@ -0,0 +1,106 @@
'use client';
import { motion, useReducedMotion } from 'framer-motion';
import { useEffect, useState } from 'react';
interface TechGridFlowProps {
className?: string;
variant?: 'default' | 'dense' | 'sparse';
color?: string;
}
interface GridLine {
id: number;
x1: number;
y1: number;
x2: number;
y2: number;
delay: number;
duration: number;
}
export function TechGridFlow({
className = '',
variant = 'default',
color = '#C41E3A',
}: TechGridFlowProps) {
const prefersReducedMotion = useReducedMotion();
const [lines, setLines] = useState<GridLine[]>([]);
useEffect(() => {
const lineCount = variant === 'dense' ? 30 : variant === 'sparse' ? 10 : 20;
const generatedLines: GridLine[] = [];
for (let i = 0; i < lineCount; i++) {
const isHorizontal = Math.random() > 0.5;
generatedLines.push({
id: i,
x1: isHorizontal ? 0 : Math.random() * 100,
y1: isHorizontal ? Math.random() * 100 : 0,
x2: isHorizontal ? 100 : Math.random() * 100,
y2: isHorizontal ? Math.random() * 100 : 100,
delay: Math.random() * 5,
duration: Math.random() * 10 + 10,
});
}
setLines(generatedLines);
}, [variant]);
return (
<div className={`absolute inset-0 overflow-hidden ${className}`} aria-hidden="true">
<svg
className="absolute inset-0 w-full h-full"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="none"
>
<defs>
<linearGradient id="gridGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor={color} stopOpacity="0" />
<stop offset="50%" stopColor={color} stopOpacity="0.15" />
<stop offset="100%" stopColor={color} stopOpacity="0" />
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="2" result="coloredBlur" />
<feMerge>
<feMergeNode in="coloredBlur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
{lines.map((line) => (
<motion.line
key={line.id}
x1={`${line.x1}%`}
y1={`${line.y1}%`}
x2={`${line.x2}%`}
y2={`${line.y2}%`}
stroke="url(#gridGradient)"
strokeWidth="1"
filter="url(#glow)"
initial={{ pathLength: 0, opacity: 0 }}
animate={
prefersReducedMotion
? { pathLength: 1, opacity: 0.3 }
: {
pathLength: [0, 1, 1, 0],
opacity: [0, 0.3, 0.3, 0],
}
}
transition={{
duration: line.duration,
repeat: Infinity,
ease: 'easeInOut',
delay: line.delay,
}}
/>
))}
</svg>
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-white/20" />
</div>
);
}
export default TechGridFlow;