Some checks are pending
Deploy to Cloudflare Workers (OpenNext) / deploy (push) Waiting to run
Use mainwhite.svg on white sections with curvy green transitions into flat green bands, improve text and button contrast, and deploy via OpenNext on Cloudflare Workers. Co-authored-by: Cursor <cursoragent@cursor.com>
212 lines
6.5 KiB
TypeScript
212 lines
6.5 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";
|
|
|
|
const GREEN = "#1a5c38";
|
|
const GOLD = "#ffb300";
|
|
const BLUE = "#1f3d7e";
|
|
|
|
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-[#f0f5f2]"
|
|
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>
|
|
);
|
|
}
|