From c85383eff20b8c4cb4950f0c357aa448c1cea80c Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 6 Jun 2026 18:05:33 -0400 Subject: [PATCH] Mobile LCP: render splash logo as (valid LCP element); faster blended transition; recenter tracer - Inline is not an LCP candidate; on mobile the tagline is hidden so the logo was the only large element -> NO_LCP. Serve the mark as /images/branding/splash-such.svg and render it via (a valid LCP candidate, painted from SSR HTML). - Transition: start the cross-fade on click (shimmer plays as the splash fades into the content) and shorten durations 1.2s -> 0.8s; snappier and blended. - Tracer target back to the midpoint (computerX+0.5, computerY+0.5). --- .../public/images/branding/splash-such.svg | 18 ++++++ .../src/components/landing/hero-splash.tsx | 61 ++++++------------- packages/ui/src/procedural/nebula-field.tsx | 9 +-- 3 files changed, 41 insertions(+), 47 deletions(-) create mode 100644 apps/web/public/images/branding/splash-such.svg diff --git a/apps/web/public/images/branding/splash-such.svg b/apps/web/public/images/branding/splash-such.svg new file mode 100644 index 0000000..23e2126 --- /dev/null +++ b/apps/web/public/images/branding/splash-such.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/components/landing/hero-splash.tsx b/apps/web/src/components/landing/hero-splash.tsx index 7156009..155347a 100644 --- a/apps/web/src/components/landing/hero-splash.tsx +++ b/apps/web/src/components/landing/hero-splash.tsx @@ -12,42 +12,10 @@ const navButtons = [ { label: "About", href: "/about", Icon: Info, corner: "right-4 bottom-6 sm:right-6 sm:bottom-24 lg:right-8 lg:bottom-32" }, ]; -/** The "SUCH" glyph mark (2x2 letter grid) with a Cherenkov-blue treatment. */ -function SuchMark() { - return ( - - - - - - - - - - - {/* steady bounding box, Cherenkov cyan (static: the circling rings were - removed because stroke-dashoffset is non-composited and destabilized - mobile LCP; the pulsing glow behind the logo carries the motion) */} - - {/* letters: static so they're a stable LCP candidate */} - - {/* S */} - - {/* U */} - - {/* C */} - - {/* H */} - - - - ); -} +// The "SUCH" mark is served as /images/branding/splash-such.svg and rendered as an +// below: an is a valid Largest-Contentful-Paint candidate, whereas an +// inline is not, which is what was producing NO_LCP on mobile (where the +// tagline text is hidden, leaving the logo as the only large element). export function HeroSplash({ onEnter, sectionRef, leaving }: { onEnter: () => void; sectionRef?: any; leaving?: boolean }) { const prefersReduced = useReducedMotion(); @@ -71,9 +39,13 @@ export function HeroSplash({ onEnter, sectionRef, leaving }: { onEnter: () => vo return; } setEntering(true); - // Real water refraction: ramp the displacement scale up and back down while - // the turbulence drifts, so the logo ripples like a disturbed liquid surface. - const DURATION = 1200; + // Begin the cross-fade immediately so the water shimmer plays ON the splash as + // it fades into the revealed content (snappier and more blended than shimmer- + // then-cut). + onEnter(); + // Water refraction: ramp the displacement scale up and back down while the + // turbulence drifts, so the logo ripples like a disturbed liquid surface. + const DURATION = 800; const MAX_SCALE = 40; const start = performance.now(); const tick = (now: number) => { @@ -85,7 +57,6 @@ export function HeroSplash({ onEnter, sectionRef, leaving }: { onEnter: () => vo turbRef.current.setAttribute("baseFrequency", `${f.toFixed(4)} ${(f * 1.3).toFixed(4)}`); } if (t < 1) requestAnimationFrame(tick); - else onEnter(); }; requestAnimationFrame(tick); }; @@ -109,7 +80,7 @@ export function HeroSplash({ onEnter, sectionRef, leaving }: { onEnter: () => vo style={{ background: "radial-gradient(circle at center, rgba(34,211,238,0.28), transparent 65%)" }} initial={{ opacity: 0 }} animate={{ opacity: [0, 0.9, 0] }} - transition={{ duration: 1.2, ease: "easeInOut" }} + transition={{ duration: 0.8, ease: "easeInOut" }} /> )} @@ -137,9 +108,13 @@ export function HeroSplash({ onEnter, sectionRef, leaving }: { onEnter: () => vo style={{ filter: entering ? "url(#splash-water)" : undefined }} initial={{ opacity: 1, scale: 1 }} animate={entering ? { scale: [1, 1.06, 1.14] } : undefined} - transition={entering ? { duration: 1.2, ease: "easeInOut" } : undefined} + transition={entering ? { duration: 0.8, ease: "easeInOut" } : undefined} > - + Such Software diff --git a/packages/ui/src/procedural/nebula-field.tsx b/packages/ui/src/procedural/nebula-field.tsx index 312090c..6380521 100644 --- a/packages/ui/src/procedural/nebula-field.tsx +++ b/packages/ui/src/procedural/nebula-field.tsx @@ -92,10 +92,11 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi // Position calculations const computerX = position?.x ?? 18; const computerY = position?.y ?? 18; - // Aim the streamers at the monitor's screen: slightly up-and-left of the icon's - // geometric center. Tweak these two offsets to move the convergence point. - const targetX = computerX - 1; - const targetY = computerY - 1.5; + // Aim the streamers at the monitor's screen. +2.5 hit low-and-right; -1/-1.5 hit + // high-and-left, so this is the midpoint. Tweak these two offsets to move the + // convergence point. + const targetX = computerX + 0.5; + const targetY = computerY + 0.5; // Check for reduced motion preference useEffect(() => {