GRV-Summit-Site/components/brand/RiftTopographyLayer.tsx
kirukib cb404ec079
Some checks failed
Deploy to Cloudflare Workers (OpenNext) / deploy (push) Has been cancelled
Align site colors with GRV brand book palette.
Centralize primary, secondary, tertiary, and neutral tokens and apply them across theme variables and UI components.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 14:45:22 +03:00

214 lines
6.6 KiB
TypeScript

"use client";
import type { RiftPageProfile } from "@/content/rift-page-profiles";
import {
CONTOUR_MAJOR,
CONTOUR_MINOR,
RIFT_CHANNEL_INNER,
RIFT_CHANNEL_OUTER_LEFT,
RIFT_CHANNEL_OUTER_RIGHT,
RIFT_VIEWBOX,
contourOpacity,
getChannelTransform,
} from "@/lib/rift-topography-paths";
import { mixHex } from "@/lib/rift-colors";
import { cn } from "@/lib/utils";
import { BRAND_COLORS } from "@/content/brand-colors";
const GREEN: string = BRAND_COLORS.primary;
const GOLD: string = BRAND_COLORS.secondary;
const BLUE: string = BRAND_COLORS.tertiary;
type Props = {
variant: "hero" | "ambient" | "header";
profile: RiftPageProfile;
className?: string;
scrollProgress?: number;
scrollY?: number;
reduceMotion?: boolean;
/** hero intro state */
introPhase?: "intro" | "settled" | "static";
};
function strokesFromProgress(p: number, accentMode: RiftPageProfile["accentMode"]) {
const t = Math.max(0, Math.min(1, p));
let primary = GREEN;
let accent = GOLD;
if (accentMode === "gold") {
primary = mixHex(GOLD, GREEN, 0.35);
accent = GOLD;
} else if (accentMode === "mixed") {
if (t < 0.33) {
primary = mixHex(GREEN, GOLD, t / 0.33);
accent = mixHex(GOLD, BLUE, t / 0.33);
} else if (t < 0.66) {
primary = mixHex(GOLD, BLUE, (t - 0.33) / 0.33);
accent = mixHex(BLUE, GREEN, (t - 0.33) / 0.33);
} else {
primary = mixHex(BLUE, GREEN, (t - 0.66) / 0.34);
accent = mixHex(GREEN, GOLD, (t - 0.66) / 0.34);
}
}
return { primary, accent };
}
export function RiftTopographyLayer({
variant,
profile,
className,
scrollProgress = 0,
scrollY = 0,
reduceMotion = false,
introPhase = "static",
}: Props) {
const { primary, accent } = strokesFromProgress(scrollProgress, profile.accentMode);
const drawOffset = reduceMotion ? 0 : Math.max(0, 1 - scrollProgress * 1.12 - 0.06);
const majorOp = contourOpacity(profile.contourDensity, "major");
const minorOp = contourOpacity(profile.contourDensity, "minor");
const channelTransform = getChannelTransform(profile.channelBias);
const parallaxY = reduceMotion ? 0 : scrollY * 0.04;
const isHero = variant === "hero";
const sceneClass = cn(
isHero && "rift-hero-scene",
isHero && introPhase === "intro" && "rift-hero-intro",
isHero && introPhase === "settled" && "rift-hero-settled",
isHero && introPhase === "static" && "rift-hero-static",
!isHero && profile.profileClass,
profile.enablePulse && !reduceMotion && "rift-ambient-pulse"
);
const strokeTransition =
"stroke 0.85s ease, opacity 0.85s ease, stroke-dashoffset 0.25s ease-out";
const drawStyle = {
strokeDasharray: 1,
strokeDashoffset: drawOffset,
transition: strokeTransition,
} as const;
const showMinor =
profile.contourDensity !== "low" || profile.id === "partners";
return (
<div
className={cn("absolute inset-0 overflow-hidden", sceneClass, className)}
style={{
opacity: variant === "ambient" ? profile.ambientOpacity : 1,
transform: variant === "ambient" ? `translate3d(0, ${parallaxY}px, 0)` : undefined,
}}
aria-hidden
>
{isHero && (
<>
<div
className="absolute inset-0 bg-gradient-to-b from-[#fbfdfb] via-white to-[#e8f2ec]"
aria-hidden
/>
<div
className="absolute inset-0 opacity-70"
style={{
background:
"radial-gradient(ellipse 55% 45% at 50% 48%, rgba(255,179,0,0.12) 0%, transparent 70%)",
}}
aria-hidden
/>
<div className="rift-crack-line pointer-events-none absolute left-1/2 top-[46%] z-10 h-px w-[min(88%,680px)] -translate-x-1/2" />
<div className="rift-floor-glow pointer-events-none absolute inset-0" />
<div className="rift-channel-open pointer-events-none absolute inset-0" aria-hidden />
</>
)}
<svg
className="rift-line-draw absolute inset-0 h-full w-full"
viewBox={`0 0 ${RIFT_VIEWBOX.w} ${RIFT_VIEWBOX.h}`}
preserveAspectRatio="xMidYMid slice"
>
<g
className="rift-contour-major"
fill="none"
stroke={primary}
strokeLinecap="round"
style={{ transform: channelTransform, transformOrigin: "center" }}
>
{CONTOUR_MAJOR.map((d, i) => (
<path
key={`m-${i}`}
pathLength={1}
d={d}
strokeWidth={0.7 + (i % 2) * 0.2}
opacity={majorOp}
className="rift-contour-path"
style={{ ...drawStyle, animationDelay: isHero ? `${i * 0.35}s` : undefined }}
/>
))}
</g>
{showMinor && (
<g
className="rift-contour-minor"
fill="none"
stroke={primary}
strokeLinecap="round"
opacity={minorOp * 1.1}
style={{ transform: channelTransform, transformOrigin: "center" }}
>
{CONTOUR_MINOR.map((d, i) => (
<path
key={`n-${i}`}
pathLength={1}
d={d}
strokeWidth={0.45}
className="rift-contour-path rift-terrace-line"
style={drawStyle}
/>
))}
</g>
)}
<g
className="rift-channel-group"
fill="none"
strokeLinecap="round"
style={{ transform: channelTransform, transformOrigin: "center" }}
>
<path
pathLength={1}
d={RIFT_CHANNEL_OUTER_LEFT}
stroke={accent}
strokeWidth={1.4}
className="rift-channel-outer rift-channel-left"
opacity={0.55}
style={drawStyle}
/>
<path
pathLength={1}
d={RIFT_CHANNEL_OUTER_RIGHT}
stroke={accent}
strokeWidth={1.4}
className="rift-channel-outer rift-channel-right"
opacity={0.55}
style={drawStyle}
/>
{RIFT_CHANNEL_INNER.map((d, i) => (
<path
key={`c-${i}`}
pathLength={1}
d={d}
stroke={profile.accentMode === "gold" ? GOLD : primary}
strokeWidth={0.55}
opacity={0.35}
className="rift-channel-inner"
style={{ ...drawStyle, animationDelay: `${0.2 + i * 0.15}s` }}
/>
))}
</g>
</svg>
{isHero && (
<div className="rift-contour-morph pointer-events-none absolute inset-0" aria-hidden />
)}
</div>
);
}