style: increase nebula visibility, computer size and adjust position

This commit is contained in:
jwinterm
2026-01-16 10:58:49 -05:00
parent c9dbcc2f84
commit f978334beb
4 changed files with 4396 additions and 9 deletions
@@ -0,0 +1,67 @@
import { Card } from "@repo/ui/components/card";
import Image from "next/image";
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/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>
<Image
src="/images/r-cryptocurrency.png"
alt="r/CryptoCurrency Governance Dashboard"
fill
className="object-cover opacity-60 transition-transform duration-500 group-hover:scale-105"
/>
</Card>
{/* Secondary: Occupy Wallets */}
<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 drop-shadow-sm">Occupy Wallets</h3>
<p className="text-zinc-800 font-medium drop-shadow-sm">High-fidelity gallery for fiscal graffiti.</p>
</div>
<Image
src="/images/occupy-wallets.png"
alt="Occupy Wallets Gallery"
fill
className="object-cover opacity-40 transition-transform duration-500 group-hover:scale-105"
/>
</Card>
{/* Tertiary: Monerogue & Wowne.ro (Grouped) */}
<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="absolute inset-0 z-10 p-6 bg-gradient-to-br from-background/90 to-transparent">
<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 text-foreground">
<li className="flex items-center gap-2">Monerogue <span className="text-xs opacity-50">(Game)</span></li>
<li className="flex items-center gap-2">Wowne.ro <span className="text-xs opacity-50">(WASM)</span></li>
</ul>
</div>
<Image
src="/images/privacy-labs.png"
alt="Privacy Labs Projects"
fill
className="object-cover opacity-30 transition-transform duration-500 group-hover:scale-105"
/>
</Card>
{/* Quaternary: MoonPlace */}
<Card className="md:col-span-1 relative group overflow-hidden bg-indigo-50/70 dark:bg-indigo-950/70 border-indigo-500/20 backdrop-blur-sm">
<div className="absolute inset-0 z-10 p-6 bg-gradient-to-br from-background/90 to-transparent">
<h3 className="text-xl font-bold text-indigo-700 dark:text-indigo-400">MoonPlace</h3>
<p className="text-sm mt-2 text-foreground">Collaborative pixel canvas engine.</p>
</div>
<Image
src="/images/moonplace.png"
alt="MoonPlace Pixel Canvas"
fill
className="object-cover opacity-30 transition-transform duration-500 group-hover:scale-105"
/>
</Card>
</section>
);
};
@@ -0,0 +1,151 @@
'use client';
import { motion, useAnimation } from "framer-motion";
import { useEffect, useState, useCallback } from "react";
import { cn } from "../lib/utils";
import { Monitor } from "lucide-react";
interface NebulaProps {
className?: string;
density?: number;
themeMode?: 'light' | 'dark'; // customized prop
}
interface StreamLineProps {
d: string;
themeMode: 'light' | 'dark';
onHit: () => void;
width: number;
}
const StreamLine = ({ d, themeMode, onHit, width }: StreamLineProps) => {
const controls = useAnimation();
useEffect(() => {
let isMounted = true;
const animate = async () => {
// Random initial delay
await new Promise(resolve => setTimeout(resolve, Math.random() * 4000));
while (isMounted) {
const duration = 2 + Math.random() * 4;
controls.set({ pathLength: 0, opacity: 0 });
await controls.start({
pathLength: [0, 1],
opacity: [0, 1, 1, 0],
transition: {
duration,
ease: "easeInOut",
times: [0, 0.1, 0.9, 1]
}
});
if (isMounted) onHit();
await new Promise(resolve => setTimeout(resolve, Math.random() * 2000));
}
};
animate();
return () => { isMounted = false; };
}, [controls, onHit]);
return (
<motion.path
d={d}
stroke="currentColor"
strokeWidth={width}
fill="none"
animate={controls}
className={cn(
"transition-colors duration-500",
themeMode === 'light' ? "text-slate-400" : "text-emerald-500"
)}
/>
);
};
export const NebulaField = ({ className, density = 20, themeMode = 'dark' }: NebulaProps) => {
const [paths, setPaths] = useState<string[]>([]);
const [mounted, setMounted] = useState(false);
const monitorControls = useAnimation();
// Configuration for convergence point (percent of container)
const targetX = 25; // moved right
const targetY = 20; // moved up
useEffect(() => {
setMounted(true);
const newPaths = Array.from({ length: density }).map(() => {
// 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;
// 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]); // Removed targetX/Y dep as constant
const handleHit = useCallback(() => {
monitorControls.start({
scale: [1, 1.15, 1],
filter: ["brightness(1)", "brightness(1.5)", "brightness(1)"],
transition: { duration: 0.3 }
});
}, [monitorControls]);
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 */}
<motion.div
className="absolute -translate-x-1/2 -translate-y-1/2 z-10 opacity-30 dark:opacity-50"
style={{ left: `${targetX}%`, top: `${targetY}%` }}
animate={monitorControls}
>
<Monitor
size={64}
className={cn(
"transition-colors duration-1000",
themeMode === 'light' ? "text-slate-400" : "text-emerald-500"
)}
/>
</motion.div>
<svg
viewBox="0 0 100 100"
preserveAspectRatio="none"
className="w-full h-full opacity-60 dark:opacity-80 absolute inset-0"
>
{paths.map((d, i) => (
<StreamLine
key={i}
d={d}
themeMode={themeMode}
onHit={handleHit}
width={themeMode === 'light' ? 0.1 : 0.2}
/>
))}
</svg>
</div>
);
};
+9 -9
View File
@@ -39,7 +39,7 @@ const StreamLine = ({ d, themeMode, onHit, width }: StreamLineProps) => {
await controls.start({
pathLength: [0, 0.20, 0.20, 0],
pathOffset: [0, 0, 0.75, 0.98], // Avoid overshooting center too much
opacity: [0, 0.3, 0.3, 0], // Muted streamers
opacity: [0, 0.5, 0.5, 0], // More visible streamers
transition: {
duration,
ease: "linear",
@@ -65,7 +65,7 @@ const StreamLine = ({ d, themeMode, onHit, width }: StreamLineProps) => {
animate={controls}
className={cn(
"transition-colors duration-1000",
themeMode === 'light' ? "text-slate-300" : "text-emerald-500/30"
themeMode === 'light' ? "text-slate-400" : "text-emerald-500/50"
)}
/>
);
@@ -90,7 +90,7 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark' }: Nebu
const yRange = useTransform(scrollY, [0, 1000], [0, 250]);
// Configuration for convergence point
const targetX = 10; // Even further left to clear centered H1s
const targetX = 18; // Moved right to clear margins better
const targetY = 18; // High up near the title line
const paths = useMemo(() => {
@@ -122,10 +122,10 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark' }: Nebu
setMonitorColor(colors[Math.floor(Math.random() * colors.length)]);
monitorControls.start({
scale: [1, 1.05, 1], // Very subtle
scale: [1, 1.1, 1], // Slightly more pronounced pulse
filter: [
"brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))",
"brightness(1.1) drop-shadow(0 0 8px currentColor)", // Muted glow
"brightness(1.3) drop-shadow(0 0 12px currentColor)", // Stronger glow
"brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))"
],
transition: { duration: 2.5, ease: "easeOut" }
@@ -141,7 +141,7 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark' }: Nebu
className="absolute -translate-x-1/2 -translate-y-1/2 z-20"
initial={{ opacity: 0, scale: 0.8 }}
animate={{
opacity: themeMode === 'light' ? 0.3 : 0.4,
opacity: themeMode === 'light' ? 0.4 : 0.6, // More visible computer
scale: 1
}}
transition={{ duration: 1.5 }}
@@ -153,7 +153,7 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark' }: Nebu
>
<motion.div animate={monitorControls}>
<Monitor
size={40} // Smaller, more subtle icon
size={56} // Bigger icon as requested
className={cn(
"transition-colors duration-2000",
monitorColor
@@ -165,7 +165,7 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark' }: Nebu
<motion.svg
viewBox="0 0 100 100"
preserveAspectRatio="none"
className="w-full h-full opacity-30 dark:opacity-50 absolute inset-0"
className="w-full h-full opacity-50 dark:opacity-70 absolute inset-0"
style={{ y: yRange }}
>
{paths.map((d: string, i: number) => (
@@ -174,7 +174,7 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark' }: Nebu
d={d}
themeMode={themeMode}
onHit={handleHit}
width={themeMode === 'light' ? 0.06 : 0.1}
width={themeMode === 'light' ? 0.08 : 0.12}
/>
))}
</motion.svg>
File diff suppressed because it is too large Load Diff