feat: Refactor animation alignment & visual tweaks

- **Visuals**:
  - Implemented 'Data Ingress' animation with lines starting from edges and converging to a fixed point.
  - Added centralized 'Monitor' icon as the convergence target.
  - Refactored 'NebulaField' to use percentage-based coordinates for perfect alignment on resize.
  - Smoothed animation timing and easing.
- **Micro-interactions**:
  - Increased transparency of Bento Grid cards (from 90% to 70%) for better visibility of background elements.
  - Added backdrop-blur to cards.
- **Fixes**:
  - Fixed 'bg-clip-text' bug cutting off descenders in the main headline.
This commit is contained in:
jwinterm
2026-01-15 16:09:34 -05:00
parent 717a3be1c8
commit 4a51d8a334
3 changed files with 62 additions and 24 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ export default function Home() {
</header>
<div className="z-10 w-full max-w-7xl mx-auto flex flex-col items-center pt-20 pb-10 px-4 text-center">
<h2 className="text-5xl md:text-7xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-blue-500 to-emerald-500">
<h2 className="text-5xl md:text-7xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-blue-500 to-emerald-500 pb-2">
Digital Craftsmanship
</h2>
<p className="max-w-2xl text-xl text-muted-foreground mb-8">
@@ -5,17 +5,17 @@ export const PortfolioGrid = () => {
return (
<section className="grid grid-cols-1 md:grid-cols-4 auto-rows-[300px] gap-4 max-w-7xl mx-auto p-4 z-10 relative">
{/* Hero Project: r/CryptoCurrency */}
<Card className="md:col-span-2 md:row-span-2 relative group overflow-hidden border-0 bg-slate-900">
<Card className="md:col-span-2 md:row-span-2 relative group overflow-hidden border-0 bg-slate-900/70 backdrop-blur-sm">
<div className="absolute inset-0 z-10 bg-gradient-to-t from-black/80 to-transparent p-6 flex flex-col justify-end">
<h3 className="text-3xl font-bold text-white">r/CryptoCurrency</h3>
<p className="text-gray-200">Governance dashboard for 10M+ members. Real-time DAO metrics.</p>
</div>
{/* Placeholder image since we don't have the assets yet */}
<div className="absolute inset-0 bg-slate-800" />
<div className="absolute inset-0 bg-slate-800/50" />
</Card>
{/* Secondary: Occupy Wallets */}
<Card className="md:col-span-2 relative group overflow-hidden bg-zinc-100">
<Card className="md:col-span-2 relative group overflow-hidden bg-zinc-100/70 backdrop-blur-sm">
<div className="absolute inset-0 z-10 p-6 flex flex-col justify-center items-center text-center">
<h3 className="text-2xl font-bold text-zinc-900">Occupy Wallets</h3>
<p className="text-zinc-600">High-fidelity gallery for fiscal graffiti.</p>
@@ -23,7 +23,7 @@ export const PortfolioGrid = () => {
</Card>
{/* Tertiary: Monerogue & Wowne.ro (Grouped) */}
<Card className="md:col-span-1 relative group overflow-hidden bg-orange-50/90 dark:bg-orange-950/90 border-orange-500/20">
<Card className="md:col-span-1 relative group overflow-hidden bg-orange-50/70 dark:bg-orange-950/70 border-orange-500/20 backdrop-blur-sm">
<div className="p-6">
<h3 className="text-xl font-bold text-orange-700 dark:text-orange-400">Privacy Labs</h3>
<ul className="mt-2 space-y-2 text-sm">
@@ -34,7 +34,7 @@ export const PortfolioGrid = () => {
</Card>
{/* Quaternary: MoonPlace */}
<Card className="md:col-span-1 relative group bg-indigo-50/90 dark:bg-indigo-950/90 border-indigo-500/20">
<Card className="md:col-span-1 relative group bg-indigo-50/70 dark:bg-indigo-950/70 border-indigo-500/20 backdrop-blur-sm">
<div className="p-6">
<h3 className="text-xl font-bold text-indigo-700 dark:text-indigo-400">MoonPlace</h3>
<p className="text-sm mt-2">Collaborative pixel canvas engine.</p>
+56 -18
View File
@@ -3,6 +3,7 @@
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
import { cn } from "../lib/utils";
import { Monitor } from "lucide-react";
interface NebulaProps {
className?: string;
@@ -10,51 +11,88 @@ interface NebulaProps {
themeMode?: 'light' | 'dark'; // customized prop
}
export const NebulaField = ({ className, density = 15, themeMode = 'dark' }: NebulaProps) => {
export const NebulaField = ({ className, density = 20, themeMode = 'dark' }: NebulaProps) => {
const [paths, setPaths] = useState<string[]>([]);
const [mounted, setMounted] = useState(false);
// Configuration for convergence point (percent of container)
const targetX = 25; // moved right
const targetY = 20; // moved up
// Procedural generation: Create organic bezier curves
useEffect(() => {
setMounted(true);
const newPaths = Array.from({ length: density }).map(() => {
// Random control points
const startX = Math.random() * 100;
const startY = Math.random() * 100;
const cp1X = startX + (Math.random() - 0.5) * 50;
const cp1Y = startY + (Math.random() - 0.5) * 50;
const endX = Math.random() * 100;
const endY = Math.random() * 100;
// Determine random edge start point
const edge = Math.floor(Math.random() * 4); // 0: top, 1: right, 2: bottom, 3: left
let startX = 50, startY = 50;
return `M ${startX} ${startY} Q ${cp1X} ${cp1Y} ${endX} ${endY}`;
// Push start point well outside viewbox to ensure full coverage
switch(edge) {
case 0: startX = Math.random() * 100; startY = -20; break; // Top
case 1: startX = 120; startY = Math.random() * 100; break; // Right
case 2: startX = Math.random() * 100; startY = 120; break; // Bottom
case 3: startX = -20; startY = Math.random() * 100; break; // Left
}
// End at target point with small variance
const endX = targetX + (Math.random() - 0.5) * 5;
const endY = targetY + (Math.random() - 0.5) * 5;
// Control point for curve
const cpX = (startX + endX) / 2 + (Math.random() - 0.5) * 50;
const cpY = (startY + endY) / 2 + (Math.random() - 0.5) * 50;
return `M ${startX} ${startY} Q ${cpX} ${cpY} ${endX} ${endY}`;
});
setPaths(newPaths);
}, [density]);
}, [density, targetX, targetY]);
if (!mounted) return null;
return (
<div className={cn("absolute inset-0 overflow-hidden pointer-events-none z-0", className)}>
{/* "Computer" Icon positioned via inline styles to match physics target exactly */}
<div
className="absolute -translate-x-1/2 -translate-y-1/2 z-10 opacity-20 dark:opacity-40 animate-pulse"
style={{ left: `${targetX}%`, top: `${targetY}%` }}
>
<Monitor
size={64}
className={cn(
"transition-colors duration-1000",
themeMode === 'light' ? "text-slate-400" : "text-emerald-500"
)}
/>
</div>
<svg
viewBox="0 0 100 100"
preserveAspectRatio="none"
className="w-full h-full opacity-40 dark:opacity-60"
className="w-full h-full opacity-60 dark:opacity-80 absolute inset-0"
>
{paths.map((d, i) => (
<motion.path
key={i}
d={d}
stroke="currentColor"
strokeWidth={themeMode === 'light' ? 0.1 : 0.3}
strokeWidth={themeMode === 'light' ? 0.1 : 0.2}
fill="none"
initial={{ pathLength: 0, opacity: 0 }}
animate={{
pathLength: [0, 1, 0],
opacity: [0, 0.8, 0]
pathLength: [0, 1], // Draw from start to end
opacity: [0, 1, 0] // Fade in, then out at the end
}}
transition={{
duration: 20 + Math.random() * 10, // Async timing
duration: 5 + Math.random() * 5, // Faster, more data-stream like (was 20s)
repeat: Infinity,
ease: "linear",
ease: "easeInOut",
delay: Math.random() * 5
}}
className="text-slate-400 dark:text-emerald-400"
className={cn(
"transition-colors duration-500",
themeMode === 'light' ? "text-slate-400" : "text-emerald-500"
)}
/>
))}
</svg>