feat: 添加预览效果页面并优化交互效果
refactor: 优化代码健壮性和类型安全 style: 更新字体样式和全局CSS fix: 修复IntersectionObserver潜在空引用问题 chore: 更新依赖和ESLint配置 build: 更新构建ID和路由配置
This commit is contained in:
@@ -134,7 +134,7 @@ export default function AboutPage() {
|
||||
<div className="font-semibold text-black text-sm md:text-base">{milestone.date}</div>
|
||||
</div>
|
||||
<div className="flex-1 pb-6 border-l-2 border-gray-200 pl-6 relative">
|
||||
<div className="absolute -left-[9px] top-1 w-4 h-4 bg-black rounded-full"></div>
|
||||
<div className="absolute -left-[9px] top-1 w-4 h-4 bg-black rounded-full" />
|
||||
<h3 className="font-semibold text-black mb-1">{milestone.title}</h3>
|
||||
<p className="text-gray-600 text-sm leading-relaxed">{milestone.description}</p>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ArrowLeft, Building2, CheckCircle2, TrendingUp, Users, Target } from 'lucide-react';
|
||||
import { CASES, COMPANY_INFO } from '@/lib/constants';
|
||||
import { CASES } from '@/lib/constants';
|
||||
import type { StaticImageData } from 'next/image';
|
||||
|
||||
interface CaseResult {
|
||||
@@ -36,7 +35,7 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
if (entry?.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
},
|
||||
@@ -116,7 +115,7 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold text-[#171717] mb-4">解决方案</h2>
|
||||
<div className="space-y-4">
|
||||
{caseItem.tags.map((tag, index) => (
|
||||
{caseItem.tags.map((tag) => (
|
||||
<div key={tag} className="flex items-start gap-3">
|
||||
<div className="w-6 h-6 bg-[#C41E3A] rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<CheckCircle2 className="w-4 h-4 text-white" />
|
||||
|
||||
@@ -13,10 +13,9 @@ export default function ContactPage() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
async function handleSubmit(formData: FormData) {
|
||||
async function handleSubmit(_formData: FormData) {
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Simulate form submission
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
setIsSubmitting(false);
|
||||
|
||||
+21
-1
@@ -1,9 +1,20 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@font-face {
|
||||
font-family: 'Aoyagi Reisho';
|
||||
src: url('/fonts/AoyagiReisho.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-stretch: normal;
|
||||
unicode-range: U+4E00-9FFF, U+3400-4DBF, U+20000-2A6DF, U+2A700-2B73F, U+2B740-2B81F, U+2B820-2CEAF, U+F900-FAFF, U+2F800-2FA1F;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--font-chinese: var(--font-noto-sans-sc);
|
||||
--font-calligraphy: 'Aoyagi Reisho', var(--font-long-cang), 'Long Cang', var(--font-ma-shan-zheng), 'Ma Shan Zheng', 'ZCOOL XiaoWei', 'STKaiti', 'KaiTi', serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
@@ -315,7 +326,16 @@
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
|
||||
/* 青柳隶书体 - 与 Logo 保持一致 */
|
||||
.font-calligraphy {
|
||||
font-family: 'Aoyagi Reisho', var(--font-long-cang), 'Long Cang', var(--font-ma-shan-zheng), 'Ma Shan Zheng', 'ZCOOL XiaoWei', 'STKaiti', 'KaiTi', serif !important;
|
||||
font-weight: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
/* 发光效果 */
|
||||
.bg-glow-red {
|
||||
background: radial-gradient(circle at center, var(--color-accent-red-glow) 0%, transparent 70%);
|
||||
|
||||
+26
-2
@@ -1,5 +1,5 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { Geist, Geist_Mono, Noto_Sans_SC } from "next/font/google";
|
||||
import { Geist, Geist_Mono, Noto_Sans_SC, Ma_Shan_Zheng, Long_Cang } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "@/contexts/theme-context";
|
||||
import { WebVitals } from "@/components/analytics/web-vitals";
|
||||
@@ -27,6 +27,22 @@ const notoSansSC = Noto_Sans_SC({
|
||||
preload: true,
|
||||
});
|
||||
|
||||
const maShanZheng = Ma_Shan_Zheng({
|
||||
weight: "400",
|
||||
variable: "--font-ma-shan-zheng",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
preload: true,
|
||||
});
|
||||
|
||||
const longCang = Long_Cang({
|
||||
weight: "400",
|
||||
variable: "--font-long-cang",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
preload: true,
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: "四川睿新致远科技有限公司 - 企业数字化转型服务商",
|
||||
@@ -98,6 +114,14 @@ export default function RootLayout({
|
||||
<head>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/favicon.svg" />
|
||||
{/* 预加载龙藏体,确保与 Logo 一致 */}
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://fonts.gstatic.com/s/longcang/v21/LYjAdGP8kkgoTec8zkRgqHAtXN-dRp6ohF_hzzTtOcBgYoCKmPpHHEBiM6LIGv3EnKLjtw.0.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<OrganizationSchema />
|
||||
<WebsiteSchema />
|
||||
<script
|
||||
@@ -114,7 +138,7 @@ export default function RootLayout({
|
||||
/>
|
||||
</head>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} ${notoSansSC.variable} font-sans antialiased`}
|
||||
className={`${geistSans.variable} ${geistMono.variable} ${notoSansSC.variable} ${maShanZheng.variable} ${longCang.variable} font-sans antialiased`}
|
||||
style={{ fontFamily: "'Noto Sans SC', 'Geist', -apple-system, BlinkMacSystemFont, sans-serif" }}
|
||||
>
|
||||
<WebVitals />
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { MeshGradient } from '@/components/effects/mesh-gradient';
|
||||
import { TechGridFlow } from '@/components/effects/tech-grid-flow';
|
||||
import { DataParticleFlow } from '@/components/effects/data-particle-flow';
|
||||
import { GeometricAbstract } from '@/components/effects/geometric-abstract';
|
||||
import { InkTechFusion } from '@/components/effects/ink-tech-fusion';
|
||||
|
||||
type EffectType = 'mesh' | 'tech-grid' | 'data-particle' | 'geometric' | 'ink-tech' | 'combined';
|
||||
type ParticleShape = 'circle' | 'square' | 'triangle' | 'diamond' | 'star' | 'mixed';
|
||||
type ParticleEffect = 'default' | 'pulse' | 'glow' | 'trail';
|
||||
|
||||
interface EffectConfig {
|
||||
id: EffectType;
|
||||
name: string;
|
||||
description: string;
|
||||
features: string[];
|
||||
recommended: boolean;
|
||||
}
|
||||
|
||||
const effects: EffectConfig[] = [
|
||||
{
|
||||
id: 'mesh',
|
||||
name: 'MeshGradient',
|
||||
description: '多层渐变叠加,优雅专业',
|
||||
features: ['GPU 加速', '4 种主题', '完全可访问'],
|
||||
recommended: false,
|
||||
},
|
||||
{
|
||||
id: 'tech-grid',
|
||||
name: 'TechGridFlow',
|
||||
description: '科技网格流,数字化连接',
|
||||
features: ['网格线条', '发光效果', '3 种密度'],
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
id: 'data-particle',
|
||||
name: 'DataParticleFlow',
|
||||
description: '数据粒子流,信息流动',
|
||||
features: ['粒子动画', '点阵背景', '可自定义数量'],
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
id: 'geometric',
|
||||
name: 'GeometricAbstract',
|
||||
description: '几何抽象,现代美学',
|
||||
features: ['多种形状', '旋转动画', '3 种复杂度'],
|
||||
recommended: false,
|
||||
},
|
||||
{
|
||||
id: 'ink-tech',
|
||||
name: 'InkTechFusion',
|
||||
description: '水墨科技融合,传承创新',
|
||||
features: ['水墨效果', '科技线条', '双色渐变'],
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
id: 'combined',
|
||||
name: '组合方案',
|
||||
description: 'TechGridFlow + InkTechFusion',
|
||||
features: ['科技感', '文化底蕴', '完美契合'],
|
||||
recommended: true,
|
||||
},
|
||||
];
|
||||
|
||||
const particleShapes: { id: ParticleShape; name: string; icon: string }[] = [
|
||||
{ id: 'circle', name: '圆形', icon: '●' },
|
||||
{ id: 'square', name: '方形', icon: '■' },
|
||||
{ id: 'triangle', name: '三角形', icon: '▲' },
|
||||
{ id: 'diamond', name: '菱形', icon: '◆' },
|
||||
{ id: 'star', name: '星形', icon: '★' },
|
||||
{ id: 'mixed', name: '混合', icon: '✦' },
|
||||
];
|
||||
|
||||
const particleEffects: { id: ParticleEffect; name: string; description: string }[] = [
|
||||
{ id: 'default', name: '默认', description: '标准动画效果' },
|
||||
{ id: 'pulse', name: '脉冲', description: '呼吸式缩放' },
|
||||
{ id: 'glow', name: '发光', description: '增强发光效果' },
|
||||
{ id: 'trail', name: '轨迹', description: '长距离移动轨迹' },
|
||||
];
|
||||
|
||||
export default function EffectsPreviewPage() {
|
||||
const [selectedEffect, setSelectedEffect] = useState<EffectType>('data-particle');
|
||||
const [particleShape, setParticleShape] = useState<ParticleShape>('circle');
|
||||
const [particleEffect, setParticleEffect] = useState<ParticleEffect>('default');
|
||||
const [particleIntensity, setParticleIntensity] = useState<'subtle' | 'normal' | 'prominent'>('normal');
|
||||
|
||||
const renderEffect = () => {
|
||||
switch (selectedEffect) {
|
||||
case 'mesh':
|
||||
return <MeshGradient variant="elegant" />;
|
||||
case 'tech-grid':
|
||||
return <TechGridFlow variant="default" color="#C41E3A" />;
|
||||
case 'data-particle':
|
||||
return (
|
||||
<DataParticleFlow
|
||||
particleCount={60}
|
||||
color="#C41E3A"
|
||||
intensity={particleIntensity}
|
||||
shape={particleShape}
|
||||
effect={particleEffect}
|
||||
/>
|
||||
);
|
||||
case 'geometric':
|
||||
return <GeometricAbstract variant="minimal" color="#C41E3A" />;
|
||||
case 'ink-tech':
|
||||
return <InkTechFusion variant="subtle" primaryColor="#C41E3A" secondaryColor="#1C1C1C" />;
|
||||
case 'combined':
|
||||
return (
|
||||
<>
|
||||
<InkTechFusion variant="subtle" primaryColor="#C41E3A" secondaryColor="#1C1C1C" />
|
||||
<TechGridFlow variant="sparse" color="#C41E3A" />
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#FAFAFA]">
|
||||
<div className="fixed top-0 left-0 right-0 z-50 bg-white/95 backdrop-blur-sm border-b border-[#E5E5E5] shadow-sm">
|
||||
<div className="container-wide py-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-[#1C1C1C]">Hero Section 效果预览</h1>
|
||||
<p className="text-sm text-[#718096]">企业数字化转型服务商专属设计方案</p>
|
||||
</div>
|
||||
<a
|
||||
href="/"
|
||||
className="px-4 py-2 rounded-lg bg-[#F5F5F5] text-[#1C1C1C] hover:bg-[#E5E5E5] transition-colors text-sm font-medium"
|
||||
>
|
||||
返回首页
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 overflow-x-auto pb-2">
|
||||
{effects.map((effect) => (
|
||||
<motion.button
|
||||
key={effect.id}
|
||||
onClick={() => setSelectedEffect(effect.id)}
|
||||
className={`relative px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-all ${
|
||||
selectedEffect === effect.id
|
||||
? 'bg-[#C41E3A] text-white'
|
||||
: 'bg-[#F5F5F5] text-[#1C1C1C] hover:bg-[#E5E5E5]'
|
||||
}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{effect.name}
|
||||
{effect.recommended && (
|
||||
<span className="absolute -top-1 -right-1 px-1.5 py-0.5 text-[10px] bg-[#16A34A] text-white rounded-full">
|
||||
推荐
|
||||
</span>
|
||||
)}
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedEffect === 'data-particle' && (
|
||||
<div className="mt-4 space-y-3 border-t border-[#E5E5E5] pt-3">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-[#718096] mb-2 block">粒子形状</label>
|
||||
<div className="flex gap-2">
|
||||
{particleShapes.map((shape) => (
|
||||
<button
|
||||
key={shape.id}
|
||||
onClick={() => setParticleShape(shape.id)}
|
||||
className={`px-3 py-1.5 rounded text-sm font-medium transition-all ${
|
||||
particleShape === shape.id
|
||||
? 'bg-[#C41E3A] text-white'
|
||||
: 'bg-[#F5F5F5] text-[#1C1C1C] hover:bg-[#E5E5E5]'
|
||||
}`}
|
||||
>
|
||||
<span className="mr-1">{shape.icon}</span>
|
||||
{shape.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-medium text-[#718096] mb-2 block">动画效果</label>
|
||||
<div className="flex gap-2">
|
||||
{particleEffects.map((effect) => (
|
||||
<button
|
||||
key={effect.id}
|
||||
onClick={() => setParticleEffect(effect.id)}
|
||||
className={`px-3 py-1.5 rounded text-sm font-medium transition-all ${
|
||||
particleEffect === effect.id
|
||||
? 'bg-[#C41E3A] text-white'
|
||||
: 'bg-[#F5F5F5] text-[#1C1C1C] hover:bg-[#E5E5E5]'
|
||||
}`}
|
||||
>
|
||||
{effect.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-medium text-[#718096] mb-2 block">强度</label>
|
||||
<div className="flex gap-2">
|
||||
{(['subtle', 'normal', 'prominent'] as const).map((intensity) => (
|
||||
<button
|
||||
key={intensity}
|
||||
onClick={() => setParticleIntensity(intensity)}
|
||||
className={`px-3 py-1.5 rounded text-sm font-medium transition-all ${
|
||||
particleIntensity === intensity
|
||||
? 'bg-[#C41E3A] text-white'
|
||||
: 'bg-[#F5F5F5] text-[#1C1C1C] hover:bg-[#E5E5E5]'
|
||||
}`}
|
||||
>
|
||||
{intensity === 'subtle' ? '柔和' : intensity === 'normal' ? '正常' : '突出'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-32">
|
||||
<div className="relative h-screen">
|
||||
{renderEffect()}
|
||||
|
||||
<div className="relative z-10 h-full flex items-center justify-center">
|
||||
<div className="text-center px-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="mb-6"
|
||||
>
|
||||
<span className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full border border-[#1C1C1C]/20 bg-white/80 backdrop-blur-sm text-[#1C1C1C] text-sm font-medium">
|
||||
智连未来 · 与客户共同成长
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
className="text-5xl sm:text-6xl lg:text-7xl font-bold tracking-tight mb-6 text-[#1A1A2E]"
|
||||
>
|
||||
睿新致远
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
className="text-xl sm:text-2xl text-[#C41E3A] mb-4 font-medium"
|
||||
>
|
||||
企业数字化转型服务商
|
||||
</motion.p>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
className="text-lg text-[#718096] mb-10 max-w-2xl mx-auto"
|
||||
>
|
||||
深耕数字化转型,融合科技创新与东方智慧,为企业赋能未来
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
className="flex gap-4 justify-center"
|
||||
>
|
||||
<button className="px-8 py-3 rounded-lg bg-[#C41E3A] text-white font-medium hover:bg-[#A01830] transition-colors">
|
||||
立即咨询
|
||||
</button>
|
||||
<button className="px-8 py-3 rounded-lg border border-[#1C1C1C] text-[#1C1C1C] font-medium hover:bg-[#F5F5F5] transition-colors">
|
||||
了解更多
|
||||
</button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container-wide py-16">
|
||||
<h2 className="text-2xl font-bold text-[#1C1C1C] mb-8">方案详情</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{effects.map((effect) => (
|
||||
<motion.div
|
||||
key={effect.id}
|
||||
className={`p-6 rounded-xl border-2 transition-all cursor-pointer ${
|
||||
selectedEffect === effect.id
|
||||
? 'border-[#C41E3A] bg-[#FEF2F4]'
|
||||
: 'border-[#E5E5E5] bg-white hover:border-[#D4D4D4]'
|
||||
}`}
|
||||
onClick={() => setSelectedEffect(effect.id)}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<h3 className="text-lg font-semibold text-[#1C1C1C]">{effect.name}</h3>
|
||||
{effect.recommended && (
|
||||
<span className="px-2 py-1 text-xs bg-[#16A34A] text-white rounded-full">
|
||||
推荐
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-[#718096] mb-4">{effect.description}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{effect.features.map((feature, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 text-xs bg-[#F5F5F5] text-[#5C5C5C] rounded"
|
||||
>
|
||||
{feature}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedEffect === 'data-particle' && (
|
||||
<div className="mt-8 p-6 rounded-xl bg-gradient-to-r from-[#FEF2F4] to-[#FFF0F3] border border-[#FFE8EC]">
|
||||
<h3 className="text-lg font-semibold text-[#1C1C1C] mb-4">🎨 DataParticleFlow 自定义选项</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<h4 className="font-medium text-[#1C1C1C] mb-2">粒子形状</h4>
|
||||
<ul className="space-y-1 text-sm text-[#5C5C5C]">
|
||||
<li>● <strong>圆形</strong> - 经典柔和</li>
|
||||
<li>■ <strong>方形</strong> - 现代科技</li>
|
||||
<li>▲ <strong>三角形</strong> - 动感锐利</li>
|
||||
<li>◆ <strong>菱形</strong> - 精致优雅</li>
|
||||
<li>★ <strong>星形</strong> - 独特醒目</li>
|
||||
<li>✦ <strong>混合</strong> - 丰富多样</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-[#1C1C1C] mb-2">动画效果</h4>
|
||||
<ul className="space-y-1 text-sm text-[#5C5C5C]">
|
||||
<li>• <strong>默认</strong> - 标准动画效果</li>
|
||||
<li>• <strong>脉冲</strong> - 呼吸式缩放</li>
|
||||
<li>• <strong>发光</strong> - 增强发光效果</li>
|
||||
<li>• <strong>轨迹</strong> - 长距离移动轨迹</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-[#1C1C1C] mb-2">强度级别</h4>
|
||||
<ul className="space-y-1 text-sm text-[#5C5C5C]">
|
||||
<li>• <strong>柔和</strong> - 微妙优雅</li>
|
||||
<li>• <strong>正常</strong> - 平衡适中</li>
|
||||
<li>• <strong>突出</strong> - 醒目明显</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8 p-6 rounded-xl bg-[#F5F5F5] border border-[#E5E5E5]">
|
||||
<h3 className="text-lg font-semibold text-[#1C1C1C] mb-3">📊 性能指标</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-[#C41E3A]">60fps</div>
|
||||
<div className="text-sm text-[#718096]">稳定帧率</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-[#C41E3A]">GPU</div>
|
||||
<div className="text-sm text-[#718096]">硬件加速</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-[#C41E3A]">WCAG</div>
|
||||
<div className="text-sm text-[#718096]">可访问性</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-[#C41E3A]">100%</div>
|
||||
<div className="text-sm text-[#718096]">性能优化</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -5,6 +5,7 @@ import { Menu, X } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { COMPANY_INFO, NAVIGATION } from '@/lib/constants';
|
||||
import { useFocusTrap } from '@/hooks/use-focus-trap';
|
||||
|
||||
export function Header() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -12,6 +13,7 @@ export function Header() {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const isScrollingRef = useRef(false);
|
||||
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const focusTrapRef = useFocusTrap<HTMLDivElement>(isOpen);
|
||||
|
||||
const handleNavClick = useCallback((e: React.MouseEvent<HTMLAnchorElement>, href: string) => {
|
||||
e.preventDefault();
|
||||
@@ -43,6 +45,26 @@ export function Header() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent, href?: string) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (href) {
|
||||
const targetId = href.replace('#', '');
|
||||
const element = document.getElementById(targetId);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
setActiveSection(targetId);
|
||||
setIsOpen(false);
|
||||
}
|
||||
} else {
|
||||
setIsOpen(!isOpen);
|
||||
}
|
||||
}
|
||||
if (e.key === 'Escape' && isOpen) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 20);
|
||||
@@ -55,12 +77,15 @@ export function Header() {
|
||||
const scrollPosition = window.scrollY + 100;
|
||||
|
||||
for (let i = sections.length - 1; i >= 0; i--) {
|
||||
const section = document.getElementById(sections[i]);
|
||||
const sectionId = sections[i];
|
||||
if (!sectionId) {continue;}
|
||||
|
||||
const section = document.getElementById(sectionId);
|
||||
if (section) {
|
||||
const sectionTop = section.offsetTop;
|
||||
const sectionBottom = sectionTop + section.offsetHeight;
|
||||
if (scrollPosition >= sectionTop && scrollPosition < sectionBottom) {
|
||||
setActiveSection(sections[i]);
|
||||
setActiveSection(sectionId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -95,7 +120,8 @@ export function Header() {
|
||||
<a
|
||||
href="#home"
|
||||
onClick={(e) => handleNavClick(e, '#home')}
|
||||
className="flex items-center group"
|
||||
onKeyDown={(e) => handleKeyDown(e, '#home')}
|
||||
className="flex items-center group focus:outline-none focus:ring-2 focus:ring-[#C41E3A] focus:ring-offset-2 rounded-sm"
|
||||
>
|
||||
<img
|
||||
src="/logo.svg"
|
||||
@@ -110,9 +136,11 @@ export function Header() {
|
||||
key={item.id}
|
||||
href={item.href}
|
||||
onClick={(e) => handleNavClick(e, item.href)}
|
||||
onKeyDown={(e) => handleKeyDown(e, item.href)}
|
||||
className={`
|
||||
relative px-3 py-1.5 text-sm font-medium
|
||||
transition-all duration-300
|
||||
focus:outline-none focus:ring-2 focus:ring-[#C41E3A] focus:ring-offset-2 rounded-sm
|
||||
${activeSection === item.id.replace('#', '')
|
||||
? 'text-[#1C1C1C]'
|
||||
: 'text-[#3D3D3D] hover:text-[#1C1C1C]'
|
||||
@@ -147,8 +175,9 @@ export function Header() {
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="md:hidden p-2 -mr-2 text-[#3D3D3D] hover:text-[#1C1C1C] transition-colors"
|
||||
className="md:hidden p-2 -mr-2 text-[#3D3D3D] hover:text-[#1C1C1C] transition-colors focus:outline-none focus:ring-2 focus:ring-[#C41E3A] focus:ring-offset-2 rounded-sm"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
onKeyDown={(e) => handleKeyDown(e)}
|
||||
aria-expanded={isOpen}
|
||||
aria-controls="mobile-menu"
|
||||
aria-label={isOpen ? '关闭菜单' : '打开菜单'}
|
||||
@@ -162,6 +191,7 @@ export function Header() {
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
ref={focusTrapRef}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
@@ -170,6 +200,7 @@ export function Header() {
|
||||
<div
|
||||
className="absolute inset-0 bg-black/20 backdrop-blur-sm"
|
||||
onClick={() => setIsOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ y: -20, opacity: 0 }}
|
||||
@@ -187,6 +218,7 @@ export function Header() {
|
||||
key={item.id}
|
||||
href={item.href}
|
||||
onClick={(e) => handleNavClick(e, item.href)}
|
||||
onKeyDown={(e) => handleKeyDown(e, item.href)}
|
||||
initial={{ x: -20, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
@@ -194,6 +226,7 @@ export function Header() {
|
||||
block px-4 py-3 text-base font-medium
|
||||
transition-all duration-300
|
||||
border-l-2
|
||||
focus:outline-none focus:ring-2 focus:ring-[#C41E3A] focus:ring-inset
|
||||
${activeSection === item.id.replace('#', '')
|
||||
? 'text-[#1C1C1C] border-[#C41E3A] bg-[#FEF2F4]'
|
||||
: 'text-[#3D3D3D] border-transparent hover:text-[#1C1C1C] hover:bg-[#F5F5F5]'
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
|
||||
import { Menu, X } from 'lucide-react';
|
||||
import { NAVIGATION } from '@/lib/constants';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useFocusTrap } from '@/hooks/use-focus-trap';
|
||||
|
||||
interface MobileMenuProps {
|
||||
className?: string;
|
||||
@@ -11,6 +12,7 @@ interface MobileMenuProps {
|
||||
|
||||
export function MobileMenu({ className }: MobileMenuProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const focusTrapRef = useFocusTrap<HTMLDivElement>(isOpen);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
@@ -32,12 +34,29 @@ export function MobileMenu({ className }: MobileMenuProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent, href?: string) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
if (href) {
|
||||
handleNavClick(href);
|
||||
} else {
|
||||
setIsOpen(!isOpen);
|
||||
}
|
||||
}
|
||||
if (event.key === 'Escape' && isOpen) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('lg:hidden', className)}>
|
||||
<div className={cn('lg:hidden', className)} ref={focusTrapRef}>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="p-2 rounded-md hover:bg-[#F5F5F5] transition-colors"
|
||||
onKeyDown={(e) => handleKeyDown(e)}
|
||||
className="p-2 rounded-md hover:bg-[#F5F5F5] transition-colors focus:outline-none focus:ring-2 focus:ring-[#C41E3A] focus:ring-offset-2"
|
||||
aria-label={isOpen ? '关闭菜单' : '打开菜单'}
|
||||
aria-expanded={isOpen}
|
||||
aria-controls="mobile-menu-panel"
|
||||
>
|
||||
{isOpen ? (
|
||||
<X className="w-6 h-6 text-[#171717]" />
|
||||
@@ -51,16 +70,23 @@ export function MobileMenu({ className }: MobileMenuProps) {
|
||||
<div
|
||||
className="fixed inset-0 bg-black/20 backdrop-blur-sm z-40"
|
||||
onClick={() => setIsOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<nav className="fixed top-16 left-0 right-0 bg-white border-b border-[#E5E5E5] z-50 shadow-lg">
|
||||
<nav
|
||||
id="mobile-menu-panel"
|
||||
className="fixed top-16 left-0 right-0 bg-white border-b border-[#E5E5E5] z-50 shadow-lg"
|
||||
role="navigation"
|
||||
aria-label="移动端导航"
|
||||
>
|
||||
<div className="container-wide py-4">
|
||||
<ul className="space-y-1">
|
||||
<ul className="space-y-1" role="list">
|
||||
{NAVIGATION.map((item) => (
|
||||
<li key={item.id}>
|
||||
<button
|
||||
onClick={() => handleNavClick(item.href)}
|
||||
className="block w-full text-left px-4 py-3 text-[#171717] hover:bg-[#FEF2F4] hover:text-[#C41E3A] rounded-md transition-colors"
|
||||
onKeyDown={(e) => handleKeyDown(e, item.href)}
|
||||
className="block w-full text-left px-4 py-3 text-[#171717] hover:bg-[#FEF2F4] hover:text-[#C41E3A] rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-[#C41E3A] focus:ring-inset"
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
|
||||
@@ -13,7 +13,7 @@ export function CasesSection() {
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
if (entry?.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -46,7 +46,7 @@ export function ContactSection() {
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
if (entry?.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { RippleButton, SealButton } from '@/components/ui/ripple-button';
|
||||
import { SplitText, GradientText, MagneticButton, BlurReveal, CounterWithEffect } from '@/lib/animations';
|
||||
import { GradientText, MagneticButton, BlurReveal, CounterWithEffect } from '@/lib/animations';
|
||||
import { InkBackground } from '@/components/ui/ink-decoration';
|
||||
import { GradientFlow } from '@/components/effects/gradient-flow';
|
||||
import { DataParticleFlow } from '@/components/effects/data-particle-flow';
|
||||
import { SubtleDots } from '@/components/effects/subtle-dots';
|
||||
import { COMPANY_INFO, STATS } from '@/lib/constants';
|
||||
import { ArrowRight, Shield, Zap, Award } from 'lucide-react';
|
||||
@@ -24,7 +24,7 @@ export function HeroSection() {
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
if (entry?.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
},
|
||||
@@ -40,11 +40,11 @@ export function HeroSection() {
|
||||
|
||||
useEffect(() => {
|
||||
const statsEl = document.getElementById('stats-section');
|
||||
if (!statsEl) return;
|
||||
if (!statsEl) {return;}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
if (entry?.isIntersecting) {
|
||||
setStatsVisible(true);
|
||||
}
|
||||
},
|
||||
@@ -62,6 +62,13 @@ export function HeroSection() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>, id: string) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
handleScrollTo(id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
id="home"
|
||||
@@ -70,9 +77,12 @@ export function HeroSection() {
|
||||
className="relative min-h-screen flex items-center pt-16 overflow-hidden bg-linear-to-b from-[#FAFAFA] to-white"
|
||||
>
|
||||
<InkBackground />
|
||||
<GradientFlow
|
||||
colors={['#FAFAFA', '#FFE8EC', '#FFF0F3', '#F5F5F5', '#FFD6DD']}
|
||||
duration={15}
|
||||
<DataParticleFlow
|
||||
particleCount={60}
|
||||
color="#C41E3A"
|
||||
intensity="subtle"
|
||||
shape="square"
|
||||
effect="pulse"
|
||||
/>
|
||||
<SubtleDots color="#C41E3A" count={8} />
|
||||
|
||||
@@ -94,9 +104,15 @@ export function HeroSection() {
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
className="text-5xl sm:text-6xl lg:text-7xl font-bold tracking-tight mb-6"
|
||||
className="text-5xl sm:text-6xl lg:text-7xl tracking-tight mb-6 font-calligraphy"
|
||||
style={{
|
||||
fontWeight: 'normal',
|
||||
WebkitFontSmoothing: 'antialiased',
|
||||
MozOsxFontSmoothing: 'grayscale',
|
||||
textRendering: 'optimizeLegibility'
|
||||
}}
|
||||
>
|
||||
<SplitText text={COMPANY_INFO.shortName} className="text-[#1A1A2E]" delay={0.2} />
|
||||
{COMPANY_INFO.shortName}
|
||||
</motion.h1>
|
||||
|
||||
<BlurReveal delay={0.3}>
|
||||
@@ -109,7 +125,7 @@ export function HeroSection() {
|
||||
|
||||
<BlurReveal delay={0.4}>
|
||||
<p className="text-lg text-[#718096] mb-10 max-w-2xl mx-auto leading-relaxed">
|
||||
融合金融科技专业品质与中国传统美学,为您打造卓越的数字体验
|
||||
深耕数字化转型,融合科技创新与东方智慧,为企业赋能未来
|
||||
</p>
|
||||
</BlurReveal>
|
||||
|
||||
@@ -123,6 +139,7 @@ export function HeroSection() {
|
||||
<SealButton
|
||||
size="lg"
|
||||
onClick={() => handleScrollTo('contact')}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'contact')}
|
||||
className="min-w-45"
|
||||
>
|
||||
立即咨询
|
||||
@@ -134,6 +151,7 @@ export function HeroSection() {
|
||||
size="lg"
|
||||
variant="outline"
|
||||
onClick={() => handleScrollTo('about')}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'about')}
|
||||
className="min-w-45"
|
||||
>
|
||||
了解更多
|
||||
|
||||
@@ -58,7 +58,7 @@ export function InsightsSection() {
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
if (entry?.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -50,7 +50,7 @@ export function TestimonialsSection() {
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
if (entry?.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -44,7 +44,7 @@ export function InkCard({
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
|
||||
if (!cardRef.current) return;
|
||||
if (!cardRef.current) {return;}
|
||||
const rect = cardRef.current.getBoundingClientRect();
|
||||
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
||||
@@ -214,7 +214,7 @@ export function TiltCard({
|
||||
const [rotateY, setRotateY] = useState(0);
|
||||
|
||||
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
|
||||
if (!cardRef.current) return;
|
||||
if (!cardRef.current) {return;}
|
||||
const rect = cardRef.current.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
@@ -269,7 +269,7 @@ export function GlowCard({
|
||||
const [glowPosition, setGlowPosition] = useState({ x: 50, y: 50 });
|
||||
|
||||
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
|
||||
if (!cardRef.current) return;
|
||||
if (!cardRef.current) {return;}
|
||||
const rect = cardRef.current.getBoundingClientRect();
|
||||
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
||||
|
||||
@@ -26,7 +26,7 @@ export function AnimatedNumber({
|
||||
const hasAnimated = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInView || hasAnimated.current) return;
|
||||
if (!isInView || hasAnimated.current) {return;}
|
||||
|
||||
hasAnimated.current = true;
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export function InkDrop({
|
||||
color = '#1C1C1C',
|
||||
blur = 0,
|
||||
delay = 0,
|
||||
duration = 8,
|
||||
duration: _duration = 8,
|
||||
className = ''
|
||||
}: InkDropProps) {
|
||||
return (
|
||||
@@ -251,7 +251,7 @@ export function FloatingInk({ count = 15, className = '' }: FloatingInkProps) {
|
||||
}, []);
|
||||
|
||||
const elements = useMemo(() => {
|
||||
if (!isMounted) return [];
|
||||
if (!isMounted) {return [];}
|
||||
const items = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
@@ -351,12 +351,50 @@ interface InkDecorationProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function InkDecoration({ variant = 'balanced', className = '' }: InkDecorationProps) {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
interface DropPosition {
|
||||
left: string;
|
||||
top: string;
|
||||
size: number;
|
||||
opacity: number;
|
||||
blur: number;
|
||||
isRed: boolean;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
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 },
|
||||
@@ -366,53 +404,45 @@ export function InkDecoration({ variant = 'balanced', className = '' }: InkDecor
|
||||
|
||||
const { drops, splashes, seals, stains, strokes } = config[variant];
|
||||
|
||||
const dropPositions = useMemo(() => {
|
||||
if (!isMounted) return [];
|
||||
return Array.from({ length: drops }, (_, i) => ({
|
||||
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,
|
||||
}));
|
||||
}, [drops, isMounted]);
|
||||
|
||||
const splashPositions = useMemo(() => {
|
||||
if (!isMounted) return [];
|
||||
return Array.from({ length: splashes }, (_, i) => ({
|
||||
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,
|
||||
}));
|
||||
}, [splashes, isMounted]);
|
||||
|
||||
const sealPositions = useMemo(() => {
|
||||
if (!isMounted) return [];
|
||||
return Array.from({ length: seals }, (_, i) => ({
|
||||
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,
|
||||
}));
|
||||
}, [seals, isMounted]);
|
||||
|
||||
const stainPositions = useMemo(() => {
|
||||
if (!isMounted) return [];
|
||||
return Array.from({ length: stains }, (_, i) => ({
|
||||
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,
|
||||
}));
|
||||
}, [stains, isMounted]);
|
||||
|
||||
const strokePositions = useMemo(() => {
|
||||
if (!isMounted) return [];
|
||||
return Array.from({ length: strokes }, (_, i) => ({
|
||||
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,
|
||||
}));
|
||||
}, [strokes, isMounted]);
|
||||
duration: 6 + Math.random() * 3,
|
||||
})));
|
||||
}, [drops, splashes, seals, stains, strokes]);
|
||||
|
||||
return (
|
||||
<div className={`absolute inset-0 pointer-events-none overflow-hidden ${className}`}>
|
||||
@@ -426,7 +456,7 @@ export function InkDecoration({ variant = 'balanced', className = '' }: InkDecor
|
||||
scale: [1, 1.1, 1],
|
||||
}}
|
||||
transition={{
|
||||
duration: 5 + Math.random() * 3,
|
||||
duration: pos.duration,
|
||||
delay: i * 0.2,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
@@ -452,7 +482,7 @@ export function InkDecoration({ variant = 'balanced', className = '' }: InkDecor
|
||||
rotate: [0, 5, -5, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 7 + Math.random() * 3,
|
||||
duration: pos.duration,
|
||||
delay: i * 0.3,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
@@ -472,7 +502,7 @@ export function InkDecoration({ variant = 'balanced', className = '' }: InkDecor
|
||||
rotate: [-8, -5, -10, -8],
|
||||
}}
|
||||
transition={{
|
||||
duration: 6 + Math.random() * 2,
|
||||
duration: pos.duration,
|
||||
delay: i * 0.25,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
@@ -492,7 +522,7 @@ export function InkDecoration({ variant = 'balanced', className = '' }: InkDecor
|
||||
opacity: [0.04, 0.06, 0.04],
|
||||
}}
|
||||
transition={{
|
||||
duration: 8 + Math.random() * 4,
|
||||
duration: pos.duration,
|
||||
delay: i * 0.35,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
@@ -512,7 +542,7 @@ export function InkDecoration({ variant = 'balanced', className = '' }: InkDecor
|
||||
opacity: [0.08, 0.12, 0.08],
|
||||
}}
|
||||
transition={{
|
||||
duration: 6 + Math.random() * 3,
|
||||
duration: pos.duration,
|
||||
delay: i * 0.3,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
|
||||
@@ -24,10 +24,10 @@ export function ParticleBackground({
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
if (!canvas) {return;}
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
if (!ctx) {return;}
|
||||
|
||||
let animationFrameId: number;
|
||||
let particles: Particle[] = [];
|
||||
@@ -58,8 +58,8 @@ export function ParticleBackground({
|
||||
particle.x += particle.vx;
|
||||
particle.y += particle.vy;
|
||||
|
||||
if (particle.x < 0 || particle.x > canvas.width) particle.vx *= -1;
|
||||
if (particle.y < 0 || particle.y > canvas.height) particle.vy *= -1;
|
||||
if (particle.x < 0 || particle.x > canvas.width) {particle.vx *= -1;}
|
||||
if (particle.y < 0 || particle.y > canvas.height) {particle.vy *= -1;}
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
|
||||
@@ -67,7 +67,7 @@ export function ParticleBackground({
|
||||
ctx.fill();
|
||||
|
||||
particles.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);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion, AnimatePresence, type HTMLMotionProps } from 'framer-motion';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
@@ -51,6 +51,7 @@ export interface RippleButtonProps
|
||||
rippleDuration?: number;
|
||||
children?: React.ReactNode;
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLButtonElement>) => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
@@ -60,7 +61,7 @@ const RippleButton = React.forwardRef<HTMLButtonElement, RippleButtonProps>(
|
||||
const [ripples, setRipples] = React.useState<Ripple[]>([]);
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (disabled) return;
|
||||
if (disabled) {return;}
|
||||
|
||||
const button = e.currentTarget;
|
||||
const rect = button.getBoundingClientRect();
|
||||
@@ -78,7 +79,7 @@ const RippleButton = React.forwardRef<HTMLButtonElement, RippleButtonProps>(
|
||||
};
|
||||
|
||||
const getRippleColor = () => {
|
||||
if (rippleColor) return rippleColor;
|
||||
if (rippleColor) {return rippleColor;}
|
||||
if (variant === 'outline' || variant === 'ghost' || variant === 'link') {
|
||||
return 'rgba(196, 30, 58, 0.2)';
|
||||
}
|
||||
@@ -123,6 +124,7 @@ RippleButton.displayName = 'RippleButton';
|
||||
export interface SealButtonProps extends VariantProps<typeof rippleButtonVariants> {
|
||||
children?: React.ReactNode;
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLButtonElement>) => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
@@ -133,7 +135,7 @@ const SealButton = React.forwardRef<HTMLButtonElement, SealButtonProps>(
|
||||
const [showInk, setShowInk] = React.useState(false);
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (disabled) return;
|
||||
if (disabled) {return;}
|
||||
setIsPressed(true);
|
||||
setShowInk(true);
|
||||
setTimeout(() => setIsPressed(false), 600);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { motion, useScroll, useTransform, useSpring, type MotionValue, type Variants, type HTMLMotionProps } from 'framer-motion';
|
||||
import { motion, useScroll, useTransform, useSpring, type Variants, type HTMLMotionProps } from 'framer-motion';
|
||||
import { useRef, type ReactNode } from 'react';
|
||||
|
||||
export const scrollRevealVariants: Variants = {
|
||||
@@ -80,7 +80,7 @@ export function ScrollReveal({
|
||||
children,
|
||||
className = '',
|
||||
delay = 0,
|
||||
variants = scrollRevealVariants,
|
||||
variants: _variants = scrollRevealVariants,
|
||||
once = true,
|
||||
threshold = 0.1,
|
||||
...props
|
||||
@@ -230,18 +230,23 @@ export function FadeOnScroll({
|
||||
offset: ['start end', 'center center'],
|
||||
});
|
||||
|
||||
const transformUp = useTransform(scrollYProgress, [0, 1], [distance, 0]);
|
||||
const transformDown = useTransform(scrollYProgress, [0, 1], [-distance, 0]);
|
||||
const transformLeft = useTransform(scrollYProgress, [0, 1], [distance, 0]);
|
||||
const transformRight = useTransform(scrollYProgress, [0, 1], [-distance, 0]);
|
||||
|
||||
const getTransform = () => {
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
return useTransform(scrollYProgress, [0, 1], [distance, 0]);
|
||||
return transformUp;
|
||||
case 'down':
|
||||
return useTransform(scrollYProgress, [0, 1], [-distance, 0]);
|
||||
return transformDown;
|
||||
case 'left':
|
||||
return useTransform(scrollYProgress, [0, 1], [distance, 0]);
|
||||
return transformLeft;
|
||||
case 'right':
|
||||
return useTransform(scrollYProgress, [0, 1], [-distance, 0]);
|
||||
return transformRight;
|
||||
default:
|
||||
return useTransform(scrollYProgress, [0, 1], [distance, 0]);
|
||||
return transformUp;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -414,7 +419,7 @@ interface ScrollTriggeredCounterProps {
|
||||
|
||||
export function ScrollTriggeredCounter({
|
||||
end,
|
||||
duration = 2,
|
||||
duration: _duration = 2,
|
||||
prefix = '',
|
||||
suffix = '',
|
||||
className = '',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect, type ReactNode } from 'react';
|
||||
import { useState, useRef, type ReactNode } from 'react';
|
||||
|
||||
interface TouchSwipeProps {
|
||||
children: ReactNode;
|
||||
@@ -23,15 +23,21 @@ export function TouchSwipe({
|
||||
|
||||
const handleTouchStart = (e: React.TouchEvent) => {
|
||||
setTouchEnd(null);
|
||||
setTouchStart(e.targetTouches[0].clientX);
|
||||
const touch = e.targetTouches[0];
|
||||
if (touch) {
|
||||
setTouchStart(touch.clientX);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: React.TouchEvent) => {
|
||||
setTouchEnd(e.targetTouches[0].clientX);
|
||||
const touch = e.targetTouches[0];
|
||||
if (touch) {
|
||||
setTouchEnd(touch.clientX);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
if (!touchStart || !touchEnd) return;
|
||||
if (!touchStart || !touchEnd) {return;}
|
||||
|
||||
const distance = touchStart - touchEnd;
|
||||
const isLeftSwipe = distance > threshold;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
|
||||
export function useFocusTrap<T extends HTMLElement>(isActive: boolean) {
|
||||
const containerRef = useRef<T>(null);
|
||||
const previousActiveElement = useRef<HTMLElement | null>(null);
|
||||
|
||||
const getFocusableElements = useCallback(() => {
|
||||
if (!containerRef.current) return [];
|
||||
|
||||
const elements = containerRef.current.querySelectorAll<HTMLElement>(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
|
||||
return Array.from(elements).filter(
|
||||
(el) => !el.hasAttribute('disabled') && el.offsetParent !== null
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (!isActive || !containerRef.current) return;
|
||||
|
||||
if (event.key === 'Tab') {
|
||||
const focusableElements = getFocusableElements();
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
|
||||
if (event.shiftKey) {
|
||||
if (document.activeElement === firstElement) {
|
||||
event.preventDefault();
|
||||
lastElement?.focus();
|
||||
}
|
||||
} else {
|
||||
if (document.activeElement === lastElement) {
|
||||
event.preventDefault();
|
||||
firstElement?.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
previousActiveElement.current?.focus();
|
||||
}
|
||||
},
|
||||
[isActive, getFocusableElements]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) {
|
||||
previousActiveElement.current = document.activeElement as HTMLElement;
|
||||
|
||||
const focusableElements = getFocusableElements();
|
||||
focusableElements[0]?.focus();
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
document.body.style.overflow = 'unset';
|
||||
};
|
||||
}, [isActive, handleKeyDown, getFocusableElements]);
|
||||
|
||||
return containerRef;
|
||||
}
|
||||
@@ -19,13 +19,15 @@ export function useIntersectionObserver<T extends Element>(
|
||||
|
||||
useEffect(() => {
|
||||
const element = elementRef.current;
|
||||
if (!element) return;
|
||||
if (!element) {return;}
|
||||
|
||||
if (freezeOnceVisible && isIntersecting) return;
|
||||
if (freezeOnceVisible && isIntersecting) {return;}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
setIsIntersecting(entry.isIntersecting);
|
||||
if (entry) {
|
||||
setIsIntersecting(entry.isIntersecting);
|
||||
}
|
||||
},
|
||||
{ threshold, root, rootMargin }
|
||||
);
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
function getMediaQuerySnapshot(query: string): boolean {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
return window.matchMedia(query).matches;
|
||||
}
|
||||
|
||||
function getMediaQueryServerSnapshot(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function subscribeToMediaQuery(query: string, callback: () => void): () => void {
|
||||
if (typeof window === 'undefined') {
|
||||
return () => {};
|
||||
}
|
||||
const media = window.matchMedia(query);
|
||||
media.addEventListener('change', callback);
|
||||
return () => {
|
||||
media.removeEventListener('change', callback);
|
||||
};
|
||||
}
|
||||
|
||||
export function useMediaQuery(query: string): boolean {
|
||||
const [matches, setMatches] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia(query);
|
||||
|
||||
if (media.matches !== matches) {
|
||||
setMatches(media.matches);
|
||||
}
|
||||
|
||||
const listener = (event: MediaQueryListEvent) => {
|
||||
setMatches(event.matches);
|
||||
};
|
||||
|
||||
media.addEventListener('change', listener);
|
||||
|
||||
return () => {
|
||||
media.removeEventListener('change', listener);
|
||||
};
|
||||
}, [matches, query]);
|
||||
|
||||
const matches = useSyncExternalStore(
|
||||
(callback) => subscribeToMediaQuery(query, callback),
|
||||
() => getMediaQuerySnapshot(query),
|
||||
getMediaQueryServerSnapshot
|
||||
);
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ export function useScrollReveal({
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element) return;
|
||||
if (!element) {return;}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
if (entry?.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
if (triggerOnce) {
|
||||
observer.unobserve(element);
|
||||
@@ -44,7 +44,7 @@ interface UseScrollProgressOptions {
|
||||
threshold?: number;
|
||||
}
|
||||
|
||||
export function useScrollProgress({ threshold = 0 }: UseScrollProgressOptions = {}) {
|
||||
export function useScrollProgress({ threshold: _threshold = 0 }: UseScrollProgressOptions = {}) {
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -74,13 +74,13 @@ export function useParallax({ speed = 0.5 }: UseParallaxOptions = {}) {
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (!ref.current) return;
|
||||
|
||||
if (!ref.current) {return;}
|
||||
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
const scrolled = window.pageYOffset;
|
||||
const elementTop = rect.top + scrolled;
|
||||
const parallaxOffset = (scrolled - elementTop) * speed;
|
||||
|
||||
|
||||
setOffset(parallaxOffset);
|
||||
};
|
||||
|
||||
|
||||
@@ -342,7 +342,7 @@ export function CountUp({ end, duration = 2000, delay = 0, prefix = '', suffix =
|
||||
const hasAnimated = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInView || hasAnimated.current) return;
|
||||
if (!isInView || hasAnimated.current) {return;}
|
||||
|
||||
hasAnimated.current = true;
|
||||
|
||||
@@ -398,12 +398,12 @@ interface TypewriterProps {
|
||||
|
||||
export function Typewriter({ text, delay = 0, speed = 50, className = '', cursorClassName = '' }: TypewriterProps) {
|
||||
const [displayText, setDisplayText] = useState('');
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const [_isTyping, setIsTyping] = useState(false);
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const isInView = useInView(ref, { once: true });
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInView) return;
|
||||
if (!isInView) {return;}
|
||||
|
||||
const startTyping = setTimeout(() => {
|
||||
setIsTyping(true);
|
||||
@@ -605,13 +605,13 @@ export function SplitText({ text, className = '', delay = 0, staggerDelay = 0.03
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
className={className}
|
||||
style={{ display: 'inline-block' }}
|
||||
style={{ display: 'inline-block', fontFamily: 'inherit' }}
|
||||
>
|
||||
{text.split('').map((char, index) => (
|
||||
<motion.span
|
||||
key={index}
|
||||
variants={child}
|
||||
style={{ display: 'inline-block' }}
|
||||
style={{ display: 'inline-block', fontFamily: 'inherit' }}
|
||||
>
|
||||
{char === ' ' ? '\u00A0' : char}
|
||||
</motion.span>
|
||||
@@ -659,7 +659,7 @@ export function MagneticButton({ children, className = '', strength = 0.3, onCli
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!ref.current) return;
|
||||
if (!ref.current) {return;}
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
@@ -886,13 +886,13 @@ export function CounterWithEffect({
|
||||
effect = 'bounce'
|
||||
}: CounterWithEffectProps) {
|
||||
const [count, setCount] = useState(0);
|
||||
const [prevCount, setPrevCount] = useState(0);
|
||||
const [_prevCount, setPrevCount] = useState(0);
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const isInView = useInView(ref, { once: true, margin: '-100px' });
|
||||
const hasAnimated = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInView || hasAnimated.current) return;
|
||||
if (!isInView || hasAnimated.current) {return;}
|
||||
hasAnimated.current = true;
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Company Information
|
||||
export const COMPANY_INFO = {
|
||||
name: '四川睿新致远科技有限公司',
|
||||
shortName: '睿新致远',
|
||||
shortName: '睿新致遠',
|
||||
slogan: '专注科技创新,驱动智慧未来',
|
||||
description: '专注于信息技术服务与解决方案,为企业提供全方位的数字化转型支持',
|
||||
founded: '2026',
|
||||
|
||||
+1
-1
@@ -38,5 +38,5 @@ export function escapeHTML(str: string): string {
|
||||
'/': '/',
|
||||
};
|
||||
|
||||
return str.replace(/[&<>"'/]/g, (char) => map[char]);
|
||||
return str.replace(/[&<>"'/]/g, (char) => map[char] ?? char);
|
||||
}
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ export function debounce<T extends (...args: unknown[]) => unknown>(
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: NodeJS.Timeout | null = null
|
||||
return (...args: Parameters<T>) => {
|
||||
if (timeout) clearTimeout(timeout)
|
||||
if (timeout) {clearTimeout(timeout)}
|
||||
timeout = setTimeout(() => func(...args), wait)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user