diff --git a/package-lock.json b/package-lock.json index decfb6f..0cbf183 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,13 +13,16 @@ "lucide-react": "^1.6.0", "react": "^19.2.4", "react-dom": "^19.2.4", - "tailwind-merge": "^3.5.0" + "tailwind-merge": "^3.5.0", + "three": "^0.183.2", + "vanta": "^0.5.24" }, "devDependencies": { "@eslint/js": "^9.39.4", "@types/node": "^24.12.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@types/three": "^0.183.1", "@vitejs/plugin-react": "^6.0.1", "autoprefixer": "^10.4.27", "eslint": "^9.39.4", @@ -286,6 +289,13 @@ "node": ">=6.9.0" } }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@emnapi/core": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", @@ -906,6 +916,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "dev": true, + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -961,6 +978,36 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.183.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz", + "integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~1.0.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.57.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", @@ -1282,6 +1329,13 @@ } } }, + "node_modules/@webgpu/types": { + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz", + "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -2040,6 +2094,13 @@ } } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2786,6 +2847,13 @@ "node": ">= 8" } }, + "node_modules/meshoptimizer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz", + "integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==", + "dev": true, + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -3603,6 +3671,12 @@ "node": ">=0.8" } }, + "node_modules/three": { + "version": "0.183.2", + "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz", + "integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3765,6 +3839,12 @@ "dev": true, "license": "MIT" }, + "node_modules/vanta": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/vanta/-/vanta-0.5.24.tgz", + "integrity": "sha512-fvieEbHy1ZS23zrcX+topzqAgA4Uct1enngOEWLFBgs9TtOf6RDFOYatH7KSVdrABzQDMCQ5myQy+nTSZZwLzg==", + "license": "MIT" + }, "node_modules/vite": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.2.tgz", diff --git a/package.json b/package.json index c853253..586529c 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,16 @@ "lucide-react": "^1.6.0", "react": "^19.2.4", "react-dom": "^19.2.4", - "tailwind-merge": "^3.5.0" + "tailwind-merge": "^3.5.0", + "three": "^0.183.2", + "vanta": "^0.5.24" }, "devDependencies": { "@eslint/js": "^9.39.4", "@types/node": "^24.12.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@types/three": "^0.183.1", "@vitejs/plugin-react": "^6.0.1", "autoprefixer": "^10.4.27", "eslint": "^9.39.4", diff --git a/src/App.tsx b/src/App.tsx index 0701caf..3f0adf4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,17 +4,21 @@ import { TechStack } from './components/TechStack'; import { Projects } from './components/Projects'; import { Capabilities } from './components/Capabilities'; import { Contact } from './components/Contact'; +import { AnimatedBackground } from './components/AnimatedBackground'; function App() { return ( -
- - - - - - -
+
+ +
+ + + + + + +
+
); } diff --git a/src/components/AnimatedBackground.tsx b/src/components/AnimatedBackground.tsx new file mode 100644 index 0000000..8644d9a --- /dev/null +++ b/src/components/AnimatedBackground.tsx @@ -0,0 +1,225 @@ +import { useEffect, useRef } from 'react'; + +interface Particle { + x: number; + y: number; + vx: number; + vy: number; + size: number; + opacity: number; + hue: number; + pulse: number; + pulseSpeed: number; +} + +interface WaveBlob { + x: number; + y: number; + radius: number; + hue: number; + speed: number; + angle: number; +} + +// 极简、优雅、现代的颜色搭配 (更暗、更有质感) +const SECTION_THEMES = [ + { hues: [220, 240, 210] }, // Hero: 经典的深蓝/紫罗兰 + { hues: [250, 270, 230] }, // About: 偏神秘的深紫 + { hues: [200, 210, 220] }, // Capabilities: 清透的深墨蓝 + { hues: [190, 200, 180] }, // TechStack: 深邃的青绿/蓝松石 + { hues: [230, 250, 210] }, // Projects: 靛蓝与深紫交织 + { hues: [220, 230, 240] }, // Contact: 回归深海蓝 +]; + +export function AnimatedBackground() { + const canvasRef = useRef(null); + const scrollRef = useRef(0); + const mouseRef = useRef({ x: -999, y: -999 }); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + let W = window.innerWidth; + let H = window.innerHeight; + + const resize = () => { W = window.innerWidth; H = window.innerHeight; canvas.width = W; canvas.height = H; }; + resize(); + window.addEventListener('resize', resize); + window.addEventListener('mousemove', (e) => { mouseRef.current = { x: e.clientX, y: e.clientY }; }); + window.addEventListener('scroll', () => { scrollRef.current = window.scrollY; }, { passive: true }); + + // 光斑 (Auroras) - 更大、更缓慢 + const BLOB_COUNT = 5; + const blobs: WaveBlob[] = Array.from({ length: BLOB_COUNT }, (_, i) => ({ + x: (W / BLOB_COUNT) * i + W / (BLOB_COUNT * 2), + y: Math.random() * H * 0.8 + H * 0.1, + radius: Math.random() * 300 + 200, // 更大的光斑 + hue: SECTION_THEMES[0].hues[i % 3], + speed: Math.random() * 0.0003 + 0.0001, // 移动极度缓慢 + angle: Math.random() * Math.PI * 2, + })); + + // 环境粒子 (萤火虫/星光) - 减少数量,更精细 + const PARTICLE_COUNT = 80; + const particles: Particle[] = Array.from({ length: PARTICLE_COUNT }, () => ({ + x: Math.random() * W, + y: Math.random() * H, + vx: (Math.random() - 0.5) * 0.15, // 速度极慢 + vy: (Math.random() - 0.5) * 0.15, + size: Math.random() * 1.5 + 0.5, + opacity: Math.random() * 0.5 + 0.1, + hue: 220, + pulse: Math.random() * Math.PI * 2, + pulseSpeed: Math.random() * 0.01 + 0.003, + })); + + const lerp = (a: number, b: number, t: number) => a + (b - a) * t; + let currentHues = [...SECTION_THEMES[0].hues, ...SECTION_THEMES[0].hues]; + let t = 0; + + // 平滑滚动的插值 + let smoothScrollTarget = 0; + let animationId: number; + + const draw = () => { + t += 0.004; // 极其缓慢的时间流逝 + + // 增加滑动阻尼感,让颜色渐变极其丝滑,不会因为快速滑动而突兀 + smoothScrollTarget = lerp(smoothScrollTarget, scrollRef.current, 0.02); + + const pageH = Math.max(1, document.documentElement.scrollHeight - window.innerHeight); + const scrollFrac = smoothScrollTarget / pageH; // 取值 0~1 + const rawIdx = Math.max(0, Math.min(scrollFrac * (SECTION_THEMES.length - 1), SECTION_THEMES.length - 1)); + const sectionA = Math.floor(rawIdx); + const sectionB = Math.min(sectionA + 1, SECTION_THEMES.length - 1); + const blend = rawIdx - sectionA; + + for (let i = 0; i < BLOB_COUNT; i++) { + const hA = SECTION_THEMES[sectionA].hues[i % 3]; + const hB = SECTION_THEMES[sectionB].hues[i % 3]; + currentHues[i] = lerp(currentHues[i], lerp(hA, hB, blend), 0.015); + } + + const domHue = lerp(SECTION_THEMES[sectionA].hues[0], SECTION_THEMES[sectionB].hues[0], blend); + + // 深邃底色 + ctx.fillStyle = '#030305'; + ctx.fillRect(0, 0, W, H); + + // ── 绘制环境极光光斑 (Auroras) ── + for (let i = 0; i < blobs.length; i++) { + const blob = blobs[i]; + blob.angle += blob.speed; + + // 光斑的微小位移 + const dx = Math.cos(blob.angle * 0.7) * 40; + const dy = Math.sin(blob.angle) * 30; + + // 鼠标对光斑产生微弱吸引,增加沉浸感 + const mx = mouseRef.current.x - blob.x; + const my = mouseRef.current.y - blob.y; + const dist = Math.sqrt(mx * mx + my * my); + if (dist < 600) { + blob.x += mx * 0.00003; + blob.y += my * 0.00003; + } + + blob.hue = lerp(blob.hue, currentHues[i], 0.03); + + const cx = blob.x + dx; + const cy = blob.y + dy; + const r = blob.radius + Math.sin(blob.angle * 2) * 20; + + const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, r); + // 使用非常非常低的透明度,打造"高级的高斯模糊"感觉 + const alpha = 0.04 + Math.sin(t + blob.angle) * 0.01; + grad.addColorStop(0, `hsla(${blob.hue}, 70%, 50%, ${alpha})`); + grad.addColorStop(0.5, `hsla(${blob.hue + 15}, 60%, 45%, ${alpha * 0.5})`); + grad.addColorStop(1, 'transparent'); + + ctx.beginPath(); + // 压扁椭圆形更像极光 + ctx.ellipse(cx, cy, r * 1.5, r * 0.8, blob.angle * 0.1, 0, Math.PI * 2); + ctx.fillStyle = grad; + ctx.fill(); + } + + // ── 绘制细微星光粒子 (Particles) ── + for (const p of particles) { + p.x += p.vx; p.y += p.vy; p.pulse += p.pulseSpeed; + + // 边界循环 + if (p.x < -10) p.x = W + 10; + if (p.x > W + 10) p.x = -10; + if (p.y < -10) p.y = H + 10; + if (p.y > H + 10) p.y = -10; + + // 鼠标排斥极其微弱 + const mx = mouseRef.current.x; + const my = mouseRef.current.y; + const mdx = p.x - mx, mdy = p.y - my; + const mdist = Math.sqrt(mdx * mdx + mdy * mdy); + if (mdist < 150 && mdist > 0) { + const force = (150 - mdist) / 150; + p.vx += (mdx / mdist) * force * 0.01; + p.vy += (mdy / mdist) * force * 0.01; + } + + // 阻尼,让其缓慢恢复原速 + p.vx *= 0.99; p.vy *= 0.99; + + // 粒子受环境主色调影响 + p.hue = lerp(p.hue, domHue + Math.sin(p.pulse * 0.5) * 15, 0.02); + + const po = p.opacity * (0.5 + 0.5 * Math.sin(p.pulse)); + const ps = p.size; + + // 星光耀斑 + const glow = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, ps * 3); + glow.addColorStop(0, `hsla(${p.hue}, 80%, 75%, ${po * 0.8})`); + glow.addColorStop(1, `hsla(${p.hue}, 80%, 75%, 0)`); + + ctx.beginPath(); ctx.arc(p.x, p.y, ps * 3, 0, Math.PI * 2); + ctx.fillStyle = glow; ctx.fill(); + + // 核心实心点 + ctx.beginPath(); ctx.arc(p.x, p.y, ps * 0.5, 0, Math.PI * 2); + ctx.fillStyle = `hsla(${p.hue}, 90%, 85%, ${po})`; + ctx.fill(); + } + + // ── 鼠标氛围聚光灯 ── + if (mouseRef.current.x > -900) { + const mx = mouseRef.current.x; + const my = mouseRef.current.y; + const spotlight = ctx.createRadialGradient(mx, my, 0, mx, my, 350); + spotlight.addColorStop(0, `hsla(${domHue}, 60%, 60%, 0.025)`); + spotlight.addColorStop(0.5, `hsla(${domHue}, 60%, 50%, 0.01)`); + spotlight.addColorStop(1, 'transparent'); + ctx.beginPath(); + ctx.arc(mx, my, 350, 0, Math.PI * 2); + ctx.fillStyle = spotlight; + ctx.fill(); + } + + animationId = requestAnimationFrame(draw); + }; + + animationId = requestAnimationFrame(draw); + return () => { + cancelAnimationFrame(animationId); + window.removeEventListener('resize', resize); + }; + }, []); + + return ( + + ); +} diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index 82104aa..93a89c2 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -41,11 +41,11 @@ export const Hero = () => { 构建
- 稳健架构
- 极简交互. + 稳健架构
+ 极简交互.
{
{projects.map((project) => ( - +

{project.title}

{project.description}

@@ -80,7 +81,7 @@ export const Projects = () => { 查看源码
- + ))} diff --git a/src/components/ui/TiltCard.tsx b/src/components/ui/TiltCard.tsx new file mode 100644 index 0000000..71e030a --- /dev/null +++ b/src/components/ui/TiltCard.tsx @@ -0,0 +1,136 @@ +import React, { useRef, useState, useCallback } from 'react'; +import { cn } from '../../utils'; + +interface TiltCardProps extends React.HTMLAttributes { + children: React.ReactNode; + glow?: boolean; + tiltMax?: number; +} + +export const TiltCard = React.forwardRef( + ({ className, children, glow = false, tiltMax = 14, style, ...props }, _ref) => { + const cardRef = useRef(null); + const frameRef = useRef(0); + + const [tilt, setTilt] = useState({ rotX: 0, rotY: 0, scale: 1 }); + const [glare, setGlare] = useState({ x: 50, y: 50, opacity: 0 }); + // normalized -1..1 mouse position for the border gradient + const [border, setBorder] = useState({ x: 0.5, y: 0.5, opacity: 0 }); + + const handleMouseMove = useCallback((e: React.MouseEvent) => { + const card = cardRef.current; + if (!card) return; + const rect = card.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + const nx = x / rect.width; // 0..1 + const ny = y / rect.height; + + cancelAnimationFrame(frameRef.current); + frameRef.current = requestAnimationFrame(() => { + setTilt({ + rotX: -(ny - 0.5) * tiltMax * 2, + rotY: (nx - 0.5) * tiltMax * 2, + scale: 1.03, + }); + setGlare({ x: nx * 100, y: ny * 100, opacity: 0.2 }); + setBorder({ x: nx, y: ny, opacity: 1 }); + }); + }, [tiltMax]); + + const handleMouseLeave = useCallback(() => { + cancelAnimationFrame(frameRef.current); + setTilt({ rotX: 0, rotY: 0, scale: 1 }); + setGlare(g => ({ ...g, opacity: 0 })); + setBorder(b => ({ ...b, opacity: 0 })); + }, []); + + const isResting = tilt.rotX === 0 && tilt.rotY === 0; + + // Dynamic border: a conic gradient anchored to mouse position + const borderAngle = Math.atan2(border.y - 0.5, border.x - 0.5) * (180 / Math.PI) + 90; + + return ( +
+ {/* Animated neon border */} +
+ + {/* Card body */} +
+ + {/* Glare highlight */} +
+ + {/* Laser shimmer sweep on hover */} +
+
+
+ + {/* Content — slight Z lift for depth */} +
+ {children} +
+
+ ); + } +); + +TiltCard.displayName = 'TiltCard'; diff --git a/src/index.css b/src/index.css index faf1c68..77052a1 100644 --- a/src/index.css +++ b/src/index.css @@ -34,6 +34,23 @@ @apply bg-clip-text text-transparent bg-gradient-to-r from-blue-400 via-indigo-400 to-purple-400; } + /* Animated shimmer text */ + .text-shimmer { + background: linear-gradient( + 90deg, + #60a5fa 0%, + #a78bfa 25%, + #22d3ee 50%, + #a78bfa 75%, + #60a5fa 100% + ); + background-size: 200% auto; + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + animation: text-shimmer-move 4s linear infinite; + } + .bg-grid-pattern { background-image: linear-gradient(to right, rgba(255,255,255,0.03) 1px, transparent 1px), @@ -41,7 +58,7 @@ background-size: 50px 50px; } - /* Utility for glow effects behind cards */ + /* Glow behind cards */ .glow-effect::before { content: ''; @apply absolute inset-0 -z-10 bg-primary/20 blur-2xl rounded-full opacity-0 transition-opacity duration-300; @@ -49,4 +66,69 @@ .glow-effect:hover::before { @apply opacity-100; } + + /* Neon text glow */ + .text-glow { + text-shadow: + 0 0 20px rgba(99, 130, 246, 0.5), + 0 0 60px rgba(99, 130, 246, 0.25), + 0 0 100px rgba(99, 130, 246, 0.1); + } + + /* Pulsing ring */ + .pulse-ring { + position: relative; + } + .pulse-ring::after { + content: ''; + @apply absolute inset-0 rounded-full; + border: 1px solid rgba(99, 130, 246, 0.5); + animation: pulse-expand 2s ease-out infinite; + } +} + +/* ── Keyframes ── */ + +@keyframes text-shimmer-move { + 0% { background-position: 0% center; } + 100% { background-position: 200% center; } +} + +@keyframes shimmer-sweep { + 0% { transform: translateX(-100%) skewX(-12deg); } + 100% { transform: translateX(200%) skewX(-12deg); } +} + +@keyframes pulse-expand { + 0% { transform: scale(1); opacity: 0.7; } + 100% { transform: scale(2); opacity: 0; } +} + +@keyframes blob { + 0% { transform: translate(0px, 0px) scale(1); } + 33% { transform: translate(30px, -50px) scale(1.1); } + 66% { transform: translate(-20px, 20px) scale(0.9); } + 100% { transform: translate(0px, 0px) scale(1); } +} + +@keyframes fadeIn { + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +@keyframes fadeUp { + 0% { opacity: 0; transform: translateY(20px); } + 100% { opacity: 1; transform: translateY(0); } +} + +@keyframes float { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-10px); } +} + +/* Scan line across elements */ +@keyframes scan-line { + 0% { top: -2px; opacity: 0.8; } + 80% { opacity: 0.6; } + 100% { top: 100%; opacity: 0; } }