Some checks failed
Deploy to Cloudflare Workers (OpenNext) / deploy (push) Has been cancelled
Centralize primary, secondary, tertiary, and neutral tokens and apply them across theme variables and UI components. Co-authored-by: Cursor <cursoragent@cursor.com>
214 lines
6.6 KiB
TypeScript
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>
|
|
);
|
|
}
|