GRV-Summit-Site/components/brand/RoundedRockVoronoiBackground.tsx
Kirubel-Kibru-Yaltopia d261148b35
Some checks are pending
Deploy to Cloudflare Workers (OpenNext) / deploy (push) Waiting to run
Enhance footer and hero with brand backgrounds
2026-05-21 20:35:59 +03:00

96 lines
2.9 KiB
TypeScript

import type { CSSProperties } from "react";
import { buildVoronoiMesh, type VoronoiCell } from "@/lib/voronoi-mesh";
import { cn } from "@/lib/utils";
/** Wider mesh plane — pairs with slice scaling so pebbles are not horizontally stretched. */
const MESH_W = 140;
const MESH_H = 100;
const ROCK_TONES = ["#0d3d26", "#1a5c38", "#246b45", "#2f7a52", "#3d9a66", "#52b87a"] as const;
const GROUT = "rgba(8, 38, 22, 0.42)";
const ROCK_MESH = buildVoronoiMesh(42, MESH_W, MESH_H, 0x475256, {
siteGenerator: "poisson",
});
function hexToRgb(hex: string) {
const n = parseInt(hex.slice(1), 16);
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
}
function mixHex(a: string, b: string, t: number) {
const c1 = hexToRgb(a);
const c2 = hexToRgb(b);
const u = (v: number) => Math.round(v).toString(16).padStart(2, "0");
return `#${u(c1.r + (c2.r - c1.r) * t)}${u(c1.g + (c2.g - c1.g) * t)}${u(c1.b + (c2.b - c1.b) * t)}`;
}
function rockFill(cell: VoronoiCell, index: number) {
const cx = cell.points.reduce((s, p) => s + p[0], 0) / cell.points.length;
const cy = cell.points.reduce((s, p) => s + p[1], 0) / cell.points.length;
const yNorm = cy / MESH_H;
const gradient = mixHex("#4aad6e", "#0d3d26", Math.min(1, yNorm * 0.85 + 0.08));
const tone = ROCK_TONES[(index * 11 + Math.floor(cx * 0.55)) % ROCK_TONES.length];
return mixHex(gradient, tone, 0.54);
}
type Props = {
className?: string;
style?: CSSProperties;
/** Slight vertical wash (two-days section). */
gradient?: boolean;
};
export function RoundedRockVoronoiBackground({
className,
style,
gradient = false,
}: Props) {
const cells = ROCK_MESH.map((cell, i) => ({
d: cell.d,
fill: rockFill(cell, i),
}));
return (
<div
className={cn(
"pointer-events-none absolute overflow-hidden bg-[#1a5c38]",
style?.height != null ? "left-0 right-0" : "inset-0",
className
)}
style={style}
aria-hidden
>
<svg
className="absolute left-1/2 top-1/2 h-full w-full min-h-full min-w-full -translate-x-1/2 -translate-y-1/2"
viewBox={`0 0 ${MESH_W} ${MESH_H}`}
preserveAspectRatio="xMidYMid slice"
xmlns="http://www.w3.org/2000/svg"
>
{cells.map((cell, i) => (
<path
key={i}
d={cell.d}
fill={cell.fill}
stroke={GROUT}
strokeWidth={0.5}
strokeLinejoin="round"
strokeLinecap="round"
/>
))}
</svg>
<div className="absolute inset-0 opacity-[0.18] grain" aria-hidden />
{gradient && (
<div
className="absolute inset-0 bg-gradient-to-b from-[#4aad6e]/14 via-transparent to-[#0d3d26]/18"
aria-hidden
/>
)}
<div
className="absolute inset-0 bg-[radial-gradient(ellipse_85%_60%_at_20%_15%,rgba(13,61,38,0.2),transparent_65%)]"
aria-hidden
/>
</div>
);
}