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>
99 lines
3.0 KiB
TypeScript
99 lines
3.0 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;
|
|
|
|
import { BRAND_GREEN_SHADES } from "@/content/brand-colors";
|
|
|
|
/** Narrow green range — low contrast between “stones” on section backgrounds */
|
|
const ROCK_TONES = BRAND_GREEN_SHADES;
|
|
const GROUT = "rgba(48, 97, 76, 0.2)";
|
|
|
|
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("#2a6f4a", "#37a47a", Math.min(1, yNorm * 0.45 + 0.25));
|
|
const tone = ROCK_TONES[(index * 11 + Math.floor(cx * 0.55)) % ROCK_TONES.length];
|
|
return mixHex(gradient, tone, 0.38);
|
|
}
|
|
|
|
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-[#37a47a]",
|
|
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-[#2a6f4a]/8 via-transparent to-[#30614c]/10"
|
|
aria-hidden
|
|
/>
|
|
)}
|
|
<div
|
|
className="absolute inset-0 bg-[radial-gradient(ellipse_85%_60%_at_20%_15%,rgba(13,61,38,0.1),transparent_65%)]"
|
|
aria-hidden
|
|
/>
|
|
</div>
|
|
);
|
|
}
|