- Home hero: full-height, with an interactive 3D wireframe of my face (MediaPipe landmarks from my-photo.png) rendered as a network graph with packets, scan sweeps and an assemble-on-load entry animation - Inner pages: shared PageHero with a themed scene per page (About helix, Skills geodesic sphere, Work infra stack, Blog signal waves, Contact globe with connection arcs) - Shared Three.js stage (framing, GSAP entry, pointer, off-screen pausing, reduced motion) with lazily loaded per-scene chunks - GSAP motion system: data-reveal / data-reveal-stagger / data-parallax attributes, SplitText headings, divider draw-in, page exit fade, no-JS and reduced-motion fallbacks - Mobile: graphic stacks above the text Co-Authored-By: Claude Opus 5.5 <[email protected]>
86 lines
2.8 KiB
TypeScript
86 lines
2.8 KiB
TypeScript
import React from "react";
|
|
import { twMerge } from "tailwind-merge";
|
|
import H1 from "@/src/components/lib/layout/H1";
|
|
import Section from "@/src/components/lib/layout/Section";
|
|
import Span from "@/src/components/lib/layout/Span";
|
|
import Stack from "@/src/components/lib/layout/Stack";
|
|
import SceneCanvas, { SceneName } from "@/src/components/lib/three/SceneCanvas";
|
|
|
|
type Props = {
|
|
title: React.ReactNode;
|
|
subtitle?: React.ReactNode;
|
|
scene: SceneName;
|
|
/** Small label above the title. */
|
|
eyebrow?: React.ReactNode;
|
|
/** Extra content under the subtitle (CTAs, contact details...). */
|
|
children?: React.ReactNode;
|
|
className?: string;
|
|
};
|
|
|
|
/**
|
|
* Inner-page hero: animated title block on the left, the page's 3D graphic
|
|
* on the right (behind the text, dimmed, on smaller screens).
|
|
*/
|
|
export default function PageHero({
|
|
title,
|
|
subtitle,
|
|
scene,
|
|
eyebrow,
|
|
children,
|
|
className,
|
|
}: Props) {
|
|
return (
|
|
<Section
|
|
className={twMerge(
|
|
"relative overflow-hidden justify-start xl:justify-center",
|
|
"xl:min-h-[clamp(440px,64svh,760px)]",
|
|
"pt-4! xl:pt-20!",
|
|
className,
|
|
)}
|
|
>
|
|
<div
|
|
className={twMerge(
|
|
"relative w-full h-[38svh] min-h-[260px] max-h-[440px] mb-4",
|
|
"xl:absolute xl:inset-y-0 xl:right-0 xl:left-auto",
|
|
"xl:w-1/2 xl:h-auto xl:max-h-none xl:mb-0",
|
|
"xl:[mask-image:linear-gradient(to_right,transparent,black_18%)]",
|
|
)}
|
|
data-parallax="0.4"
|
|
>
|
|
<SceneCanvas scene={scene} className="w-full h-full" />
|
|
</div>
|
|
<Stack className="relative z-10 w-full max-w-full xl:max-w-[50vw] gap-6">
|
|
{eyebrow && (
|
|
<Span
|
|
className="text-sm uppercase tracking-[0.2em] dark:text-emerald-400/80"
|
|
data-reveal="fade"
|
|
>
|
|
{eyebrow}
|
|
</Span>
|
|
)}
|
|
<H1 className="leading-snug m-0!" data-reveal="text">
|
|
{title}
|
|
</H1>
|
|
{subtitle && (
|
|
<Span
|
|
className="text-xl"
|
|
data-reveal="up"
|
|
data-reveal-delay="0.35"
|
|
>
|
|
{subtitle}
|
|
</Span>
|
|
)}
|
|
{children && (
|
|
<div
|
|
className="w-full"
|
|
data-reveal="up"
|
|
data-reveal-delay="0.5"
|
|
>
|
|
{children}
|
|
</div>
|
|
)}
|
|
</Stack>
|
|
</Section>
|
|
);
|
|
}
|