mirror of
https://github.com/Such-Software/such.software.git
synced 2026-10-17 05:32:05 +02:00
Splash: remove non-composited glow rings (fix mobile NO_LCP); cross-fade to content; nudge tracer target
- Drop the two animated glow rings: stroke-dashoffset is non-composited, so they repainted every frame next to the logo and prevented Lighthouse from settling on an LCP (mobile NO_LCP). The static box + pulsing glow remain. - Splash now cross-fades into the content: on enter it becomes a fixed overlay and fades opacity to 0 over the revealed page, instead of a hard cut. - Nebula tracer target moved up-and-left (computerX-1, computerY-1.5) so the streamers converge on the monitor screen, not below-right of it. - Removed the now-unused splash-* keyframes.
This commit is contained in:
@@ -227,16 +227,4 @@
|
|||||||
transition-duration: 0.01ms !important;
|
transition-duration: 0.01ms !important;
|
||||||
scroll-behavior: auto !important;
|
scroll-behavior: auto !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Splash hero idle animations as CSS (browser/compositor driven) instead of
|
|
||||||
framer-motion, so they cost no JS main-thread time. The reduced-motion block
|
|
||||||
above neutralizes them. */
|
|
||||||
@keyframes splash-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
|
|
||||||
@keyframes splash-breath { 0%, 100% { opacity: 0.9; } 50% { opacity: 1; } }
|
|
||||||
@keyframes splash-ring-a { from { stroke-dashoffset: 0; } to { stroke-dashoffset: -1; } }
|
|
||||||
@keyframes splash-ring-b { from { stroke-dashoffset: 0; } to { stroke-dashoffset: 1; } }
|
|
||||||
.splash-float { animation: splash-float 6s ease-in-out infinite; }
|
|
||||||
.splash-breath { animation: splash-breath 5s ease-in-out infinite; }
|
|
||||||
.splash-ring-a { animation: splash-ring-a 5s linear infinite; }
|
|
||||||
.splash-ring-b { animation: splash-ring-b 7s linear infinite; }
|
|
||||||
@@ -22,6 +22,10 @@ export default function Home() {
|
|||||||
// header, bottom nav, and background take over. Scrolling back up lands on the hero,
|
// header, bottom nav, and background take over. Scrolling back up lands on the hero,
|
||||||
// never the splash again.
|
// never the splash again.
|
||||||
const [entered, setEntered] = useState(false);
|
const [entered, setEntered] = useState(false);
|
||||||
|
// `splashUp` keeps the splash mounted through its fade-out; `leaving` triggers the
|
||||||
|
// cross-fade overlay (button path only).
|
||||||
|
const [splashUp, setSplashUp] = useState(true);
|
||||||
|
const [leaving, setLeaving] = useState(false);
|
||||||
const splashRef = useRef<HTMLElement>(null);
|
const splashRef = useRef<HTMLElement>(null);
|
||||||
const restoreScroll = useRef(0);
|
const restoreScroll = useRef(0);
|
||||||
|
|
||||||
@@ -31,6 +35,7 @@ export default function Home() {
|
|||||||
useIsoLayoutEffect(() => {
|
useIsoLayoutEffect(() => {
|
||||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
||||||
setEntered(true);
|
setEntered(true);
|
||||||
|
setSplashUp(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -49,6 +54,7 @@ export default function Home() {
|
|||||||
if (!entry.isIntersecting) {
|
if (!entry.isIntersecting) {
|
||||||
restoreScroll.current = Math.max(0, window.scrollY - el.offsetHeight);
|
restoreScroll.current = Math.max(0, window.scrollY - el.offsetHeight);
|
||||||
setEntered(true);
|
setEntered(true);
|
||||||
|
setSplashUp(false); // scrolled past: just unmount, no overlay fade needed
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ threshold: 0 },
|
{ threshold: 0 },
|
||||||
@@ -85,6 +91,10 @@ export default function Home() {
|
|||||||
const enterFromButton = () => {
|
const enterFromButton = () => {
|
||||||
restoreScroll.current = 0;
|
restoreScroll.current = 0;
|
||||||
setEntered(true);
|
setEntered(true);
|
||||||
|
setLeaving(true);
|
||||||
|
// Keep the splash mounted briefly as a fixed, fading overlay so it cross-fades
|
||||||
|
// into the revealed content instead of hard-cutting.
|
||||||
|
setTimeout(() => setSplashUp(false), 800);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -92,7 +102,7 @@ export default function Home() {
|
|||||||
<DynamicBackground nebulaPosition={nebulaPosition} visible={entered} />
|
<DynamicBackground nebulaPosition={nebulaPosition} visible={entered} />
|
||||||
<Header floating visible={entered} />
|
<Header floating visible={entered} />
|
||||||
|
|
||||||
{!entered && <HeroSplash sectionRef={splashRef} onEnter={enterFromButton} />}
|
{splashUp && <HeroSplash sectionRef={splashRef} onEnter={enterFromButton} leaving={leaving} />}
|
||||||
|
|
||||||
<div 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">
|
<div 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">
|
||||||
<h1
|
<h1
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { motion, useReducedMotion } from "framer-motion";
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
import { ChevronDown, LayoutGrid, Smartphone, Wrench, Info } from "lucide-react";
|
import { ChevronDown, LayoutGrid, Smartphone, Wrench, Info } from "lucide-react";
|
||||||
import { useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
const navButtons = [
|
const navButtons = [
|
||||||
{ label: "Portfolio", href: "/products", Icon: LayoutGrid, corner: "left-4 top-6 sm:left-6 sm:top-24 lg:left-8 lg:top-32" },
|
{ label: "Portfolio", href: "/products", Icon: LayoutGrid, corner: "left-4 top-6 sm:left-6 sm:top-24 lg:left-8 lg:top-32" },
|
||||||
@@ -13,7 +13,7 @@ const navButtons = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
/** The "SUCH" glyph mark (2x2 letter grid) with a Cherenkov-blue treatment. */
|
/** The "SUCH" glyph mark (2x2 letter grid) with a Cherenkov-blue treatment. */
|
||||||
function SuchMark({ animate }: { animate: boolean }) {
|
function SuchMark() {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 200 200"
|
viewBox="0 0 200 200"
|
||||||
@@ -30,25 +30,10 @@ function SuchMark({ animate }: { animate: boolean }) {
|
|||||||
</feMerge>
|
</feMerge>
|
||||||
</filter>
|
</filter>
|
||||||
</defs>
|
</defs>
|
||||||
{/* steady bounding box, Cherenkov cyan */}
|
{/* 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) */}
|
||||||
<rect x="14" y="14" width="172" height="172" rx="16" fill="none" stroke="#22d3ee" strokeWidth="3.5" filter="url(#splash-glow)" opacity="0.45" />
|
<rect x="14" y="14" width="172" height="172" rx="16" fill="none" stroke="#22d3ee" strokeWidth="3.5" filter="url(#splash-glow)" opacity="0.45" />
|
||||||
{/* two Cherenkov glow segments circling the ring in opposite directions */}
|
|
||||||
{animate && (
|
|
||||||
<>
|
|
||||||
<rect
|
|
||||||
x="14" y="14" width="172" height="172" rx="16"
|
|
||||||
fill="none" stroke="#a5f3fc" strokeWidth="4" strokeLinecap="round" filter="url(#splash-glow)"
|
|
||||||
pathLength={1} strokeDasharray="0.16 0.84"
|
|
||||||
className="splash-ring-a"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="14" y="14" width="172" height="172" rx="16"
|
|
||||||
fill="none" stroke="#67e8f9" strokeWidth="2.5" strokeLinecap="round" filter="url(#splash-glow)"
|
|
||||||
pathLength={1} strokeDasharray="0.06 0.94" opacity="0.7"
|
|
||||||
className="splash-ring-b"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{/* letters: static so they're a stable LCP candidate */}
|
{/* letters: static so they're a stable LCP candidate */}
|
||||||
<g>
|
<g>
|
||||||
{/* S */}
|
{/* S */}
|
||||||
@@ -64,13 +49,21 @@ function SuchMark({ animate }: { animate: boolean }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HeroSplash({ onEnter, sectionRef }: { onEnter: () => void; sectionRef?: any }) {
|
export function HeroSplash({ onEnter, sectionRef, leaving }: { onEnter: () => void; sectionRef?: any; leaving?: boolean }) {
|
||||||
const prefersReduced = useReducedMotion();
|
const prefersReduced = useReducedMotion();
|
||||||
const animate = !prefersReduced;
|
|
||||||
const [entering, setEntering] = useState(false);
|
const [entering, setEntering] = useState(false);
|
||||||
|
const [fadeOut, setFadeOut] = useState(false);
|
||||||
const dispRef = useRef<SVGFEDisplacementMapElement>(null);
|
const dispRef = useRef<SVGFEDisplacementMapElement>(null);
|
||||||
const turbRef = useRef<SVGFETurbulenceElement>(null);
|
const turbRef = useRef<SVGFETurbulenceElement>(null);
|
||||||
|
|
||||||
|
// When the page asks the splash to leave, it first becomes a fixed overlay; one
|
||||||
|
// frame later we drop opacity so it cross-fades into the revealed content.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!leaving) return;
|
||||||
|
const id = requestAnimationFrame(() => setFadeOut(true));
|
||||||
|
return () => cancelAnimationFrame(id);
|
||||||
|
}, [leaving]);
|
||||||
|
|
||||||
const handleEnter = () => {
|
const handleEnter = () => {
|
||||||
if (entering) return;
|
if (entering) return;
|
||||||
if (prefersReduced) {
|
if (prefersReduced) {
|
||||||
@@ -101,7 +94,11 @@ export function HeroSplash({ onEnter, sectionRef }: { onEnter: () => void; secti
|
|||||||
<section
|
<section
|
||||||
ref={sectionRef}
|
ref={sectionRef}
|
||||||
aria-label="Such Software"
|
aria-label="Such Software"
|
||||||
className="relative z-10 flex min-h-[100svh] w-full max-w-7xl flex-col items-center justify-center overflow-hidden px-4 py-12 text-center"
|
className={
|
||||||
|
leaving
|
||||||
|
? `fixed inset-0 z-50 flex flex-col items-center justify-center overflow-hidden px-4 py-12 text-center pointer-events-none transition-opacity duration-700 ${fadeOut ? "opacity-0" : "opacity-100"}`
|
||||||
|
: "relative z-10 flex min-h-[100svh] w-full max-w-7xl flex-col items-center justify-center overflow-hidden px-4 py-12 text-center"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{/* Click-to-enter: soft Cherenkov wash. The ripple itself is the SVG water
|
{/* Click-to-enter: soft Cherenkov wash. The ripple itself is the SVG water
|
||||||
filter refracting the logo (driven by handleEnter's rAF loop). */}
|
filter refracting the logo (driven by handleEnter's rAF loop). */}
|
||||||
@@ -139,10 +136,10 @@ export function HeroSplash({ onEnter, sectionRef }: { onEnter: () => void; secti
|
|||||||
<motion.div
|
<motion.div
|
||||||
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 ? { scale: [1, 1.06, 1.14] } : undefined}
|
||||||
transition={entering ? { duration: 1.2, ease: "easeInOut", times: [0, 0.7, 1] } : undefined}
|
transition={entering ? { duration: 1.2, ease: "easeInOut" } : undefined}
|
||||||
>
|
>
|
||||||
<SuchMark animate={animate} />
|
<SuchMark />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -92,8 +92,10 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
|
|||||||
// Position calculations
|
// Position calculations
|
||||||
const computerX = position?.x ?? 18;
|
const computerX = position?.x ?? 18;
|
||||||
const computerY = position?.y ?? 18;
|
const computerY = position?.y ?? 18;
|
||||||
const targetX = computerX + 2.5;
|
// Aim the streamers at the monitor's screen: slightly up-and-left of the icon's
|
||||||
const targetY = computerY + 2.5;
|
// geometric center. Tweak these two offsets to move the convergence point.
|
||||||
|
const targetX = computerX - 1;
|
||||||
|
const targetY = computerY - 1.5;
|
||||||
|
|
||||||
// Check for reduced motion preference
|
// Check for reduced motion preference
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user