style: mute nebula intensity, fix load visibility, and sync parallax

This commit is contained in:
jwinterm
2026-01-16 10:49:33 -05:00
parent 51cfd7a3c8
commit 29135a3a70
2 changed files with 43 additions and 46 deletions
@@ -6,6 +6,13 @@ import { useEffect, useState } from "react";
export function DynamicBackground() { export function DynamicBackground() {
const { theme } = useTheme(); const { theme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return ( return (
<NebulaField <NebulaField
+36 -46
View File
@@ -24,16 +24,14 @@ const StreamLine = ({ d, themeMode, onHit, width }: StreamLineProps) => {
useEffect(() => { useEffect(() => {
let isMounted = true; let isMounted = true;
const animate = async () => { const animate = async () => {
// Shorter start delay (0-2s) so it feels more reactive on load // Very short initial delay for first streamers
await new Promise(resolve => setTimeout(resolve, Math.random() * 2000)); await new Promise(resolve => setTimeout(resolve, Math.random() * 1000));
while (isMounted) { while (isMounted) {
const duration = 8 + Math.random() * 6; // slightly faster but still slow const duration = 7 + Math.random() * 5; // Faster, snappier
// Ensure starting state is definitely 0
controls.set({ pathLength: 0, pathOffset: 0, opacity: 0 }); controls.set({ pathLength: 0, pathOffset: 0, opacity: 0 });
// Trigger onHit when the tip reaches the end (at 75% of duration)
const hitTimeout = setTimeout(() => { const hitTimeout = setTimeout(() => {
if (isMounted) onHit(); if (isMounted) onHit();
}, duration * 750); }, duration * 750);
@@ -41,7 +39,7 @@ const StreamLine = ({ d, themeMode, onHit, width }: StreamLineProps) => {
await controls.start({ await controls.start({
pathLength: [0, 0.25, 0.25, 0], pathLength: [0, 0.25, 0.25, 0],
pathOffset: [0, 0, 0.75, 1], pathOffset: [0, 0, 0.75, 1],
opacity: [0, 0.5, 0.5, 0], opacity: [0, 0.4, 0.4, 0], // Muted streamers
transition: { transition: {
duration, duration,
ease: "linear", ease: "linear",
@@ -50,9 +48,7 @@ const StreamLine = ({ d, themeMode, onHit, width }: StreamLineProps) => {
}); });
clearTimeout(hitTimeout); clearTimeout(hitTimeout);
await new Promise(resolve => setTimeout(resolve, 500 + Math.random() * 1500));
// Continuous flow with small pause
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000));
} }
}; };
animate(); animate();
@@ -91,7 +87,7 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark' }: Nebu
// Configuration for convergence point // Configuration for convergence point
const targetX = 25; const targetX = 25;
const targetY = 30; // Lowered to hit center of computer better const targetY = 35; // Moved down slightly more
const paths = useMemo(() => { const paths = useMemo(() => {
return Array.from({ length: density }).map(() => { return Array.from({ length: density }).map(() => {
@@ -105,76 +101,70 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark' }: Nebu
case 3: startX = -20; startY = Math.random() * 100; break; case 3: startX = -20; startY = Math.random() * 100; break;
} }
const endX = targetX + (Math.random() - 0.5) * 4; const endX = targetX + (Math.random() - 0.5) * 2; // Tighter hit box
const endY = targetY + (Math.random() - 0.5) * 4; const endY = targetY + (Math.random() - 0.5) * 2; // Tighter hit box
const cpX = (startX + endX) / 2 + (Math.random() - 0.5) * 50; const cpX = (startX + endX) / 2 + (Math.random() - 0.5) * 50;
const cpY = (startY + endY) / 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}`; return `M ${startX} ${startY} Q ${cpX} ${cpY} ${endX} ${endY}`;
}); });
}, [density, themeMode]); // Regenerate on theme switch }, [density, themeMode, targetX, targetY]); // Added dependencies for stability
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
}, []); }, []);
// Initial animation for the computer icon
useEffect(() => {
if (mounted) {
monitorControls.set({ opacity: 0, scale: 0.5 });
monitorControls.start({
opacity: themeMode === 'light' ? 0.4 : 0.6,
scale: 1,
transition: { duration: 2, ease: "easeOut" }
});
}
}, [mounted, themeMode, monitorControls]);
const handleHit = useCallback(() => { const handleHit = useCallback(() => {
const colors = [ const colors = themeMode === 'light'
"text-emerald-500", "text-blue-500", "text-purple-500", ? ["text-slate-400", "text-blue-400", "text-indigo-400", "text-emerald-400", "text-cyan-400"]
"text-indigo-500", "text-cyan-500", "text-rose-500", "text-amber-500" : ["text-emerald-400/60", "text-blue-400/60", "text-purple-400/60", "text-indigo-400/60", "text-cyan-400/60", "text-rose-400/60", "text-amber-400/60"];
];
setMonitorColor(colors[Math.floor(Math.random() * colors.length)]); setMonitorColor(colors[Math.floor(Math.random() * colors.length)]);
monitorControls.start({ monitorControls.start({
scale: [1, 1.2, 1], scale: [1, 1.08, 1], // Less jarring scale
filter: [ filter: [
"brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))", "brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))",
"brightness(1.5) drop-shadow(0 0 20px currentColor)", "brightness(1.2) drop-shadow(0 0 15px currentColor)", // Less brightness
"brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))" "brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))"
], ],
transition: { duration: 4, ease: "easeInOut" } transition: { duration: 3, ease: "easeOut" } // Slower and smoother
}); });
}, [monitorControls]); }, [monitorControls, themeMode]);
if (!mounted) return null;
return ( return (
<div className={cn("absolute inset-0 overflow-hidden pointer-events-none z-0", className)}> <div className={cn("absolute inset-0 overflow-hidden pointer-events-none z-0", className)}>
<motion.div <motion.div
className="absolute -translate-x-1/2 -translate-y-1/2 z-20" 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.5,
scale: 1
}}
transition={{ duration: 1 }}
style={{ style={{
left: `${targetX}%`, left: `${targetX}%`,
top: `${targetY}%`, top: `${targetY}%`,
y: yRange y: yRange
}} }}
animate={monitorControls}
> >
<Monitor <motion.div animate={monitorControls}>
size={64} <Monitor
className={cn( size={64}
"transition-colors duration-2000", className={cn(
monitorColor "transition-colors duration-2000",
)} monitorColor
/> )}
/>
</motion.div>
</motion.div> </motion.div>
<svg <motion.svg
viewBox="0 0 100 100" viewBox="0 0 100 100"
preserveAspectRatio="none" preserveAspectRatio="none"
className="w-full h-full opacity-40 dark:opacity-70 absolute inset-0" className="w-full h-full opacity-40 dark:opacity-70 absolute inset-0"
style={{ y: yRange }} // SVG follows computer parallax
> >
{paths.map((d: string, i: number) => ( {paths.map((d: string, i: number) => (
<StreamLine <StreamLine
@@ -182,10 +172,10 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark' }: Nebu
d={d} d={d}
themeMode={themeMode} themeMode={themeMode}
onHit={handleHit} onHit={handleHit}
width={themeMode === 'light' ? 0.1 : 0.2} width={themeMode === 'light' ? 0.08 : 0.12} // slightly thinner for less jarring feel
/> />
))} ))}
</svg> </motion.svg>
</div> </div>
); );
}; };