Files
personal-site-bunext/src/components/general/page-hero.tsx
T
tbenandClaude Opus 5.5 d93550ed29 Add Three.js page graphics and GSAP site-wide motion system
- 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]>
2026-11-03 11:57:05 +01:00

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>
);
}