Skip to content

Commit 7cf4685

Browse files
committed
feat: scroll‑driven Crossfade
1 parent f51b7d1 commit 7cf4685

File tree

2 files changed

+96
-7
lines changed

2 files changed

+96
-7
lines changed

src/app/page.tsx

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,18 @@ export default function Home() {
2020
<div className="min-h-screen flex flex-col font-sans">
2121
<Navbar />
2222
<Hero />
23-
<Platforms />
24-
<Providers />
25-
<Plugins />
26-
<Community />
23+
<StickyFadeSection zIndex={10} heightVh={220} fadeStart={0.60} fadeEnd={0.92}>
24+
<Platforms />
25+
</StickyFadeSection>
26+
<StickyFadeSection zIndex={20} heightVh={220} fadeStart={0.60} fadeEnd={0.92}>
27+
<Providers />
28+
</StickyFadeSection>
29+
<StickyFadeSection zIndex={30} heightVh={200} fadeStart={0.58} fadeEnd={0.9}>
30+
<Plugins />
31+
</StickyFadeSection>
32+
<StickyFadeSection zIndex={40} heightVh={200} fadeStart={0.58} fadeEnd={0.9}>
33+
<Community />
34+
</StickyFadeSection>
2735
<MoreThings />
2836
<GetStarted />
2937
<SiteFooter />
@@ -53,6 +61,67 @@ function useScrollY() {
5361
return scrollY;
5462
}
5563

64+
// Compute a 0..1 fade progress for a tall wrapper with a sticky child
65+
function useSectionFade(
66+
wrapperRef: { current: HTMLElement | null },
67+
fadeStart: number = 0.55,
68+
fadeEnd: number = 0.9
69+
) {
70+
const [opacity, setOpacity] = useState(1);
71+
useEffect(() => {
72+
if (typeof window === "undefined") return;
73+
let raf = 0 as number | 0;
74+
const update = () => {
75+
const el = wrapperRef.current as HTMLElement | null;
76+
if (!el) return setOpacity(1);
77+
const rect = el.getBoundingClientRect();
78+
const total = Math.max(1, el.offsetHeight);
79+
const scrolled = Math.min(Math.max(0, -rect.top), total);
80+
const progress = scrolled / total; // 0..1
81+
const t = Math.min(1, Math.max(0, (progress - fadeStart) / Math.max(0.0001, (fadeEnd - fadeStart))));
82+
const eased = 1 - (t * t * (3 - 2 * t)); // smoothstep ease-out for fade
83+
setOpacity(eased);
84+
};
85+
const onScroll = () => {
86+
if (!raf) raf = requestAnimationFrame(() => { update(); raf = 0 as number | 0; }) as unknown as number;
87+
};
88+
const onResize = () => { update(); };
89+
update();
90+
window.addEventListener("scroll", onScroll, { passive: true });
91+
window.addEventListener("resize", onResize, { passive: true });
92+
return () => {
93+
if (raf) cancelAnimationFrame(raf);
94+
window.removeEventListener("scroll", onScroll);
95+
window.removeEventListener("resize", onResize);
96+
};
97+
}, [wrapperRef, fadeStart, fadeEnd]);
98+
return opacity;
99+
}
100+
101+
function StickyFadeSection({
102+
children,
103+
zIndex,
104+
heightVh = 200,
105+
fadeStart = 0.6,
106+
fadeEnd = 0.9,
107+
}: {
108+
children: React.ReactNode;
109+
zIndex: number;
110+
heightVh?: number;
111+
fadeStart?: number;
112+
fadeEnd?: number;
113+
}) {
114+
const wrapperRef = useRef<HTMLElement | null>(null);
115+
const opacity = useSectionFade(wrapperRef, fadeStart, fadeEnd);
116+
return (
117+
<section ref={wrapperRef} className="relative" style={{ height: `${heightVh}vh` }}>
118+
<div className="sticky top-16" style={{ zIndex, opacity }}>
119+
{children}
120+
</div>
121+
</section>
122+
);
123+
}
124+
56125
function Navbar() {
57126
const [openLang, setOpenLang] = useState(false);
58127
const [openMenu, setOpenMenu] = useState(false);
@@ -152,7 +221,7 @@ function Navbar() {
152221
<ChevronDownIcon aria-hidden className={`w-4 h-4 transition-transform duration-200 ${openLang ? 'rotate-180' : ''}`} />
153222
</button>
154223
{openLang && (
155-
<ul className="absolute right-0 mt-5 w-28 rounded-lg border border-ui bg-background shadow-lg origin-top-right animate-dropdown">
224+
<ul className="absolute right-0 mt-3 w-28 rounded-lg border border-ui bg-background shadow-lg origin-top-right animate-dropdown">
156225
<li className="px-3 py-2 hover:bg-black/[.04] dark:hover:bg-white/[.06] cursor-pointer" onClick={() => { setLocale("zh-CN"); setOpenLang(false); }}>简体中文</li>
157226
<li className="px-3 py-2 hover:bg-black/[.04] dark:hover:bg-white/[.06] cursor-pointer" onClick={() => { setLocale("en-US"); setOpenLang(false); }}>English</li>
158227
<li className="px-3 py-2 hover:bg-black/[.04] dark:hover:bg-white/[.06] cursor-pointer" onClick={() => { setLocale("ja-JP"); setOpenLang(false); }}>日本語</li>
@@ -407,6 +476,8 @@ function Platforms() {
407476

408477
function Providers() {
409478
const { t } = useI18n();
479+
const scrollY = useScrollY();
480+
const provParallax = { transform: `translateY(${scrollY * 0.03}px)`, willChange: "transform" } as React.CSSProperties;
410481
const items = [
411482
{ name: "OpenAI", src: "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg" },
412483
{ name: "xAI", src: "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/xai.svg" },
@@ -429,7 +500,11 @@ function Providers() {
429500
{ name: "FastGPT", src: "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/fastgpt-color.svg" },
430501
];
431502
return (
432-
<section className="min-h-[calc(100vh-64px)] flex items-center py-12 sm:py-16">
503+
<section className="relative overflow-hidden min-h-[calc(100vh-64px)] flex items-center py-12 sm:py-16" style={{ backgroundColor: "var(--section-surface)" }}>
504+
<div aria-hidden className="pointer-events-none absolute inset-0 -z-10" style={provParallax}>
505+
<div className="absolute -top-8 -left-10 w-64 h-64 rounded-full blur-3xl opacity-15 brand-bg animate-orb-pulse" />
506+
<div className="absolute bottom-[-40px] right-1/4 w-72 h-72 rounded-full blur-3xl opacity-10 animate-orb-pulse" style={{ backgroundColor: "#22d3ee" }} />
507+
</div>
433508
<div className="mx-auto max-w-6xl px-4 sm:px-6">
434509
<Reveal as="h2" className="text-center text-3xl sm:text-4xl font-semibold tracking-tight mb-6 sm:mb-8 gradient-title" delay={0}>{t("models.title")}</Reveal>
435510
<Reveal as="p" className="text-center mt-2 text-sm opacity-80 mb-10 sm:mb-12" delay={150}>{t("models.subtitle")}</Reveal>
@@ -583,6 +658,8 @@ function Plugins() {
583658

584659
function Community() {
585660
const { t } = useI18n();
661+
const scrollY = useScrollY();
662+
const commParallax = { transform: `translateY(${scrollY * 0.02}px)`, willChange: "transform" } as React.CSSProperties;
586663
const [stats, setStats] = useState<{ stars: number; forks: number; contributors: number; plugins: number }>({ stars: 0, forks: 0, contributors: 0, plugins: 0 });
587664
useEffect(() => {
588665
fetch("/api/plugins", { cache: "no-store" })
@@ -611,7 +688,11 @@ function Community() {
611688
);
612689

613690
return (
614-
<section className="py-12 sm:py-16">
691+
<section className="relative overflow-hidden py-12 sm:py-16" style={{ backgroundColor: "var(--section-surface)" }}>
692+
<div aria-hidden className="pointer-events-none absolute inset-0 -z-10" style={commParallax}>
693+
<div className="absolute top-[-20px] right-[-40px] w-60 h-60 rounded-full blur-3xl opacity-10 brand-bg animate-orb-pulse" />
694+
<div className="absolute bottom-[-50px] left-1/5 w-72 h-72 rounded-full blur-3xl opacity-10" style={{ backgroundColor: "#a78bfa" }} />
695+
</div>
615696
<div className="mx-auto max-w-6xl px-4 sm:px-6">
616697
<Reveal as="h2" className="text-center text-3xl sm:text-4xl font-semibold tracking-tight gradient-title" delay={0}>{t("community.title")}</Reveal>
617698
<Reveal as="p" className="text-center mt-2 mb-10 text-sm opacity-80" delay={150}>{t("community.subtitle")}</Reveal>

src/assets/globals.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ html.dark, :root[data-theme="dark"] {
104104
--footer-bg: #0f1115;
105105
}
106106

107+
/* Section surface background (used by Providers / Community) */
108+
:root {
109+
--section-surface: rgba(0,0,0,0.02);
110+
}
111+
html.dark, :root[data-theme="dark"] {
112+
--section-surface: rgba(54, 54, 54, 0.318);
113+
}
114+
107115
body {
108116
background: var(--background);
109117
color: var(--foreground);

0 commit comments

Comments
 (0)