Restore tuned nebula; make splash logo static for stable mobile LCP

- Revert the nebula density/parallax change that disturbed the carefully
  tuned streamer convergence (it gave no perf benefit; the nebula is behind
  the splash and not in the load trace).
- Logo is now fully static (no float/breath) so it's a stable LCP candidate
  on mobile; the rings, glow pulse, and staged button fade-ins still carry
  the motion. (Desktop TBT win from the framer->CSS conversion is retained.)
This commit is contained in:
jwinterm
2026-06-06 17:31:43 -04:00
parent a5815e20ee
commit fad3223af3
2 changed files with 16 additions and 11 deletions
@@ -49,8 +49,8 @@ function SuchMark({ animate }: { animate: boolean }) {
/> />
</> </>
)} )}
{/* letters with a gentle breath */} {/* letters: static so they're a stable LCP candidate */}
<g className={animate ? "splash-breath" : undefined}> <g>
{/* S */} {/* S */}
<path d="M 78,36 H 36 V 58 H 78 V 82 H 36" fill="none" stroke="#35c98e" strokeWidth="14" strokeLinecap="round" strokeLinejoin="round" /> <path d="M 78,36 H 36 V 58 H 78 V 82 H 36" fill="none" stroke="#35c98e" strokeWidth="14" strokeLinecap="round" strokeLinejoin="round" />
{/* U */} {/* U */}
@@ -137,7 +137,6 @@ export function HeroSplash({ onEnter, sectionRef }: { onEnter: () => void; secti
style={{ animationDuration: "4.5s" }} style={{ animationDuration: "4.5s" }}
/> />
<motion.div <motion.div
className={!entering && animate ? "splash-float" : undefined}
style={{ filter: entering ? "url(#splash-water)" : undefined }} style={{ filter: entering ? "url(#splash-water)" : undefined }}
initial={{ opacity: 1, scale: 1 }} initial={{ opacity: 1, scale: 1 }}
animate={entering ? { opacity: [1, 1, 0], scale: [1, 1.06, 1.14] } : undefined} animate={entering ? { opacity: [1, 1, 0], scale: [1, 1.06, 1.14] } : undefined}
+14 -8
View File
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { motion, useAnimation } from "framer-motion"; import { motion, useAnimation, useScroll, useTransform } from "framer-motion";
import { useEffect, useState, useCallback, useMemo } from "react"; import { useEffect, useState, useCallback, useMemo } from "react";
import { cn } from "../lib/utils"; import { cn } from "../lib/utils";
import { Monitor } from "lucide-react"; import { Monitor } from "lucide-react";
@@ -79,12 +79,16 @@ const StreamLine = ({ d, themeMode, onHit, width, index }: StreamLineProps) => {
); );
}; };
export const NebulaField = ({ className, density = 4, themeMode = 'dark', position }: NebulaProps) => { export const NebulaField = ({ className, density = 8, themeMode = 'dark', position }: NebulaProps) => {
// === ALL HOOKS MUST BE CALLED BEFORE ANY CONDITIONAL RETURNS === // === ALL HOOKS MUST BE CALLED BEFORE ANY CONDITIONAL RETURNS ===
const monitorControls = useAnimation(); const monitorControls = useAnimation();
const [monitorColor, setMonitorColor] = useState(themeMode === 'light' ? "text-sky-500" : "text-cyan-400/70"); const [monitorColor, setMonitorColor] = useState(themeMode === 'light' ? "text-sky-500" : "text-cyan-400/70");
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false); const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
// Parallax Scroll hooks (called unconditionally)
const { scrollY } = useScroll();
const yRange = useTransform(scrollY, [0, 1000], [0, 250]);
// Position calculations // Position calculations
const computerX = position?.x ?? 18; const computerX = position?.x ?? 18;
const computerY = position?.y ?? 18; const computerY = position?.y ?? 18;
@@ -187,7 +191,7 @@ export const NebulaField = ({ className, density = 4, themeMode = 'dark', positi
{/* 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}%` }} style={{ ...glowStyle, left: `${computerX}%`, top: `${computerY}%`, y: yRange }}
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 }}
@@ -201,9 +205,10 @@ export const NebulaField = ({ className, density = 4, themeMode = 'dark', positi
scale: 1 scale: 1
}} }}
transition={{ duration: 1.5 }} transition={{ duration: 1.5 }}
style={{ style={{
left: `${computerX}%`, left: `${computerX}%`,
top: `${computerY}%` top: `${computerY}%`,
y: yRange
}} }}
> >
<motion.div animate={monitorControls}> <motion.div animate={monitorControls}>
@@ -217,10 +222,11 @@ export const NebulaField = ({ className, density = 4, themeMode = 'dark', positi
</motion.div> </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-50 dark:opacity-70 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) => ( {paths.map((d: string, i: number) => (
<StreamLine <StreamLine
@@ -232,7 +238,7 @@ export const NebulaField = ({ className, density = 4, themeMode = 'dark', positi
index={i} index={i}
/> />
))} ))}
</svg> </motion.svg>
</div> </div>
); );
}; };