更新显示效果

This commit is contained in:
yoyuzh
2026-03-26 00:46:29 +08:00
parent 4934b64a09
commit ddfffc1ebe
8 changed files with 548 additions and 17 deletions

82
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -4,10 +4,13 @@ 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 (
<main className="w-full bg-background text-slate-100 min-h-screen font-sans selection:bg-primary/30 selection:text-white pb-10">
<div className="relative w-full min-h-screen">
<AnimatedBackground />
<main className="relative z-10 w-full text-slate-100 min-h-screen font-sans selection:bg-primary/30 selection:text-white pb-10">
<Hero />
<About />
<Capabilities />
@@ -15,6 +18,7 @@ function App() {
<Projects />
<Contact />
</main>
</div>
);
}

View File

@@ -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<HTMLCanvasElement>(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 (
<canvas
ref={canvasRef}
className="fixed inset-0 w-full h-full pointer-events-none"
style={{ zIndex: 0 }}
/>
);
}

View File

@@ -41,11 +41,11 @@ export const Hero = () => {
<motion.h1
variants={itemVariants}
className="text-5xl md:text-7xl lg:text-8xl font-semibold tracking-tight text-white mb-6 leading-[1.1]"
className="text-5xl md:text-7xl lg:text-8xl font-semibold tracking-tight text-white mb-6 leading-[1.1] text-glow"
>
<br className="hidden md:block" />
<span className="text-gradient"></span> <br className="hidden md:block" />
<span className="text-gradient"></span>.
<span className="text-shimmer"></span> <br className="hidden md:block" />
<span className="text-shimmer"></span>.
</motion.h1>
<motion.p

View File

@@ -1,8 +1,9 @@
import { Section } from './ui/Section';
import { Card } from './ui/Card';
import { TiltCard } from './ui/TiltCard';
import { ExternalLink, Code } from 'lucide-react';
import { Button } from './ui/Button';
const projects = [
{
title: '分布式云存储系统',
@@ -48,7 +49,7 @@ export const Projects = () => {
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full max-w-5xl mx-auto">
{projects.map((project) => (
<Card key={project.title} glow className="flex flex-col h-full">
<TiltCard key={project.title} glow className="flex flex-col h-full">
<h3 className="text-2xl font-semibold text-white mb-2">{project.title}</h3>
<p className="text-slate-400 text-sm mb-6 flex-grow">{project.description}</p>
@@ -80,7 +81,7 @@ export const Projects = () => {
<Code size={16} />
</Button>
</div>
</Card>
</TiltCard>
))}
</div>
</Section>

View File

@@ -0,0 +1,136 @@
import React, { useRef, useState, useCallback } from 'react';
import { cn } from '../../utils';
interface TiltCardProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
glow?: boolean;
tiltMax?: number;
}
export const TiltCard = React.forwardRef<HTMLDivElement, TiltCardProps>(
({ className, children, glow = false, tiltMax = 14, style, ...props }, _ref) => {
const cardRef = useRef<HTMLDivElement>(null);
const frameRef = useRef<number>(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<HTMLDivElement>) => {
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 (
<div
ref={cardRef}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
style={{
...style,
transform: `perspective(900px) rotateX(${tilt.rotX}deg) rotateY(${tilt.rotY}deg) scale3d(${tilt.scale},${tilt.scale},${tilt.scale})`,
transition: isResting
? 'transform 0.55s cubic-bezier(.03,.98,.52,.99), box-shadow 0.55s ease'
: 'transform 0.08s linear',
willChange: 'transform',
transformStyle: 'preserve-3d',
// Glow box shadow that intensifies on hover
boxShadow: border.opacity
? `0 0 40px -8px hsla(${220 + border.x * 40},80%,60%,0.35), 0 20px 80px -20px hsla(260,80%,50%,0.2)`
: '0 0 0 0 transparent',
}}
className={cn(
'relative rounded-3xl overflow-hidden group',
className
)}
{...props}
>
{/* Animated neon border */}
<div
className="absolute inset-0 rounded-3xl transition-opacity duration-300 pointer-events-none"
style={{
opacity: border.opacity * 0.9,
padding: '1px',
background: `conic-gradient(
from ${borderAngle}deg at ${border.x * 100}% ${border.y * 100}%,
hsla(220,90%,65%,0) 0deg,
hsla(220,90%,65%,0.9) 60deg,
hsla(270,90%,70%,0.6) 120deg,
hsla(190,90%,65%,0.8) 180deg,
hsla(220,90%,65%,0) 240deg
)`,
WebkitMask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
WebkitMaskComposite: 'xor',
maskComposite: 'exclude',
}}
/>
{/* Card body */}
<div className={cn(
'absolute inset-[1px] rounded-3xl',
'bg-surface/50 backdrop-blur-lg border border-white/5 group-hover:border-white/10 transition-colors duration-300',
glow && 'glow-effect',
)} />
{/* Glare highlight */}
<div
className="absolute inset-0 rounded-3xl pointer-events-none transition-opacity duration-300"
style={{
opacity: glare.opacity,
background: `radial-gradient(circle at ${glare.x}% ${glare.y}%, rgba(255,255,255,0.18) 0%, transparent 65%)`,
}}
/>
{/* Laser shimmer sweep on hover */}
<div className="absolute inset-0 rounded-3xl pointer-events-none overflow-hidden">
<div
className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500"
style={{
background: `linear-gradient(105deg, transparent 30%, rgba(255,255,255,0.06) 50%, transparent 70%)`,
animation: 'shimmer-sweep 2s ease-in-out infinite',
}}
/>
</div>
{/* Content — slight Z lift for depth */}
<div
className="relative p-6 md:p-8"
style={{ transform: 'translateZ(20px)', transformStyle: 'preserve-3d' }}
>
{children}
</div>
</div>
);
}
);
TiltCard.displayName = 'TiltCard';

View File

@@ -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; }
}