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