mirror of
https://github.com/Such-Software/such.software.git
synced 2026-10-17 05:32:05 +02:00
Mobile nebula: keep left monitor + parallax, glide-and-fade with the hero
A phone has no left gutter like desktop, and the nebula was position:fixed, so scrolling slid the text up into the pinned monitor (overlap + jumps). Keep the monitor to the left of the title and keep the parallax, but on mobile the field glides up and out with the hero (slower-than-scroll lag for depth) and fades as it leaves, so it never crosses the content below. Only the monitor/streamer layer fades; the base gradient stays. Extra top room (pt-44) keeps it clear of the header at rest. Desktop unchanged. Dials: yRangeMobile (parallax), heroOpacity range (fade), pt-44 (top room).
This commit is contained in:
@@ -16,7 +16,7 @@ export default function Home() {
|
|||||||
<HomeShell>
|
<HomeShell>
|
||||||
<div
|
<div
|
||||||
id="main-content"
|
id="main-content"
|
||||||
className="z-10 w-full max-w-7xl mx-auto flex flex-col items-center pt-32 pb-10 px-4 text-center scroll-mt-28"
|
className="z-10 w-full max-w-7xl mx-auto flex flex-col items-center pt-44 sm:pt-32 pb-10 px-4 text-center scroll-mt-28"
|
||||||
>
|
>
|
||||||
<h1
|
<h1
|
||||||
id="hero-title"
|
id="hero-title"
|
||||||
|
|||||||
@@ -88,11 +88,20 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
|
|||||||
typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches,
|
typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Parallax Scroll hooks (called unconditionally)
|
// Scroll-driven motion (hooks called unconditionally).
|
||||||
const { scrollY } = useScroll();
|
const { scrollY } = useScroll();
|
||||||
|
// Desktop: gentle downward parallax. The monitor lives in the empty left gutter, so
|
||||||
|
// drifting down stays clear of text.
|
||||||
const yRange = useTransform(scrollY, [0, 1000], [0, 250]);
|
const yRange = useTransform(scrollY, [0, 1000], [0, 250]);
|
||||||
|
// Mobile: there is no gutter, so instead of pinning the field to the screen we let it
|
||||||
|
// glide up and out with the hero (slower than scroll, which gives the same depth) and
|
||||||
|
// fade, so it never crosses the content below. PARALLAX/FADE ranges are easy dials.
|
||||||
|
const yRangeMobile = useTransform(scrollY, [0, 1000], [0, -650]);
|
||||||
|
const parallaxY = isMobile ? yRangeMobile : yRange;
|
||||||
|
const heroOpacity = useTransform(scrollY, [0, 450], [1, 0]);
|
||||||
|
|
||||||
// Position calculations
|
// Monitor position: just to the left of the hero title, on every screen. The extra
|
||||||
|
// top room the hero gets on mobile (see page.tsx) keeps this clear of the header.
|
||||||
const computerX = position?.x ?? 18;
|
const computerX = position?.x ?? 18;
|
||||||
const computerY = position?.y ?? 18;
|
const computerY = position?.y ?? 18;
|
||||||
// Aim the streamers at the monitor's screen. +2.5 hit low-and-right; -1/-1.5 hit
|
// Aim the streamers at the monitor's screen. +2.5 hit low-and-right; -1/-1.5 hit
|
||||||
@@ -111,9 +120,8 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
|
|||||||
return () => mediaQuery.removeEventListener('change', handler);
|
return () => mediaQuery.removeEventListener('change', handler);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// On small screens the monitor and streamers have nowhere to sit without crossing
|
// Track the mobile breakpoint so the monitor can be re-anchored and the parallax
|
||||||
// the hero text, and the scroll parallax drags them over it. Use the static glow
|
// swapped for a fade.
|
||||||
// there instead.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const mq = window.matchMedia('(max-width: 767px)');
|
const mq = window.matchMedia('(max-width: 767px)');
|
||||||
setIsMobile(mq.matches);
|
setIsMobile(mq.matches);
|
||||||
@@ -176,9 +184,8 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
|
|||||||
background: `radial-gradient(circle at center, ${glowColor} 0%, transparent 70%)`,
|
background: `radial-gradient(circle at center, ${glowColor} 0%, transparent 70%)`,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Static background for reduced-motion users and small screens (still Cherenkov-tinted,
|
// Return static background for reduced motion users (still tinted Cherenkov blue, no motion).
|
||||||
// with no monitor or streamers to overlap the hero text).
|
if (prefersReducedMotion) {
|
||||||
if (prefersReducedMotion || isMobile) {
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"absolute inset-0 overflow-hidden pointer-events-none z-0",
|
"absolute inset-0 overflow-hidden pointer-events-none z-0",
|
||||||
@@ -198,7 +205,7 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
|
|||||||
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)}>
|
||||||
|
|
||||||
{/* Base Cherenkov gradient wash */}
|
{/* Base Cherenkov gradient wash (always on; only the active layer below fades) */}
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"absolute inset-0",
|
"absolute inset-0",
|
||||||
themeMode === 'light'
|
themeMode === 'light'
|
||||||
@@ -206,10 +213,14 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
|
|||||||
: "bg-gradient-to-br from-slate-950 via-slate-900/80 to-[#0a1622]"
|
: "bg-gradient-to-br from-slate-950 via-slate-900/80 to-[#0a1622]"
|
||||||
)} />
|
)} />
|
||||||
|
|
||||||
|
{/* Monitor + streamers + glow. On mobile this layer glides up and fades as the
|
||||||
|
hero scrolls away, so it never crosses the content below. */}
|
||||||
|
<motion.div className="absolute inset-0" style={{ opacity: isMobile ? heroOpacity : undefined }}>
|
||||||
|
|
||||||
{/* Slowly-pulsing Cherenkov "reactor pool" bloom */}
|
{/* Slowly-pulsing Cherenkov "reactor pool" bloom */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute h-[70vmax] w-[70vmax] -translate-x-1/2 -translate-y-1/2"
|
className="absolute h-[70vmax] w-[70vmax] -translate-x-1/2 -translate-y-1/2"
|
||||||
style={{ ...glowStyle, left: `${computerX}%`, top: `${computerY}%`, y: yRange }}
|
style={{ ...glowStyle, left: `${computerX}%`, top: `${computerY}%`, y: parallaxY }}
|
||||||
initial={{ opacity: 0, scale: 0.85 }}
|
initial={{ opacity: 0, scale: 0.85 }}
|
||||||
animate={{ opacity: [0.55, 0.9, 0.55], scale: [0.95, 1.05, 0.95] }}
|
animate={{ opacity: [0.55, 0.9, 0.55], scale: [0.95, 1.05, 0.95] }}
|
||||||
transition={{ duration: 11, ease: "easeInOut", repeat: Infinity }}
|
transition={{ duration: 11, ease: "easeInOut", repeat: Infinity }}
|
||||||
@@ -226,7 +237,7 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
|
|||||||
style={{
|
style={{
|
||||||
left: `${computerX}%`,
|
left: `${computerX}%`,
|
||||||
top: `${computerY}%`,
|
top: `${computerY}%`,
|
||||||
y: yRange
|
y: parallaxY
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<motion.div animate={monitorControls}>
|
<motion.div animate={monitorControls}>
|
||||||
@@ -244,7 +255,7 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
|
|||||||
viewBox="0 0 100 100"
|
viewBox="0 0 100 100"
|
||||||
preserveAspectRatio="none"
|
preserveAspectRatio="none"
|
||||||
className="w-full h-full opacity-50 dark:opacity-70 absolute inset-0"
|
className="w-full h-full opacity-50 dark:opacity-70 absolute inset-0"
|
||||||
style={{ y: yRange }}
|
style={{ y: parallaxY }}
|
||||||
>
|
>
|
||||||
{paths.map((d: string, i: number) => (
|
{paths.map((d: string, i: number) => (
|
||||||
<StreamLine
|
<StreamLine
|
||||||
@@ -257,6 +268,7 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</motion.svg>
|
</motion.svg>
|
||||||
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user