Some checks are pending
Deploy to Cloudflare Workers (OpenNext) / deploy (push) Waiting to run
109 lines
3.2 KiB
TypeScript
109 lines
3.2 KiB
TypeScript
import { buildVoronoiMesh, type VoronoiCell } from "@/lib/voronoi-mesh";
|
|
import {
|
|
buildFooterMidlineCurve,
|
|
midlineCurveToClipPath,
|
|
midlineCurveToClipPathPercent,
|
|
} from "@/lib/footer-curve-edge";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const VIEW_W = 160;
|
|
const VIEW_H = 100;
|
|
const MIDLINE_Y = VIEW_H * 0.5;
|
|
const CURVE_SEED = 0xa3f21c;
|
|
const MIDLINE_CURVE = buildFooterMidlineCurve(VIEW_W, 20, CURVE_SEED, MIDLINE_Y, 11);
|
|
const FACET_CLIP_PATH = midlineCurveToClipPath(MIDLINE_CURVE, VIEW_W, VIEW_H);
|
|
const FACET_CLIP_CSS = midlineCurveToClipPathPercent(MIDLINE_CURVE, VIEW_W, VIEW_H);
|
|
|
|
const FACET_TONES = [
|
|
"#f5fbf7",
|
|
"#ecf8f0",
|
|
"#dff3e6",
|
|
"#ccebd8",
|
|
"#b5e4c8",
|
|
"#9ed9b4",
|
|
"#84d09f",
|
|
"#6bc98a",
|
|
"#52c878",
|
|
"#4aad6e",
|
|
] as const;
|
|
|
|
const LOW_POLY_MESH = buildVoronoiMesh(64, VIEW_W, VIEW_H, CURVE_SEED, {
|
|
siteGenerator: "poisson",
|
|
shape: "sharp",
|
|
minDist: 6.5,
|
|
});
|
|
|
|
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 facetFill(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 xNorm = cx / VIEW_W;
|
|
const yNorm = Math.max(0, (cy - MIDLINE_Y) / (VIEW_H - MIDLINE_Y));
|
|
const tone = FACET_TONES[(index * 9 + Math.floor(cx * 0.55)) % FACET_TONES.length];
|
|
const wash = mixHex("#ffffff", "#e8f5ec", 0.35);
|
|
const mix = 0.42 + (1 - xNorm) * 0.2 + yNorm * 0.12;
|
|
return mixHex(wash, tone, mix);
|
|
}
|
|
|
|
const FACET_CELLS = LOW_POLY_MESH.map((cell, i) => ({
|
|
d: cell.d,
|
|
fill: facetFill(cell, i),
|
|
}));
|
|
|
|
type Props = {
|
|
className?: string;
|
|
};
|
|
|
|
export function GeometricMessBackground({ className }: Props) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"pointer-events-none absolute inset-0 z-0 overflow-hidden bg-white",
|
|
className
|
|
)}
|
|
aria-hidden
|
|
>
|
|
<div
|
|
className="geometric-mess-layer absolute inset-0"
|
|
style={{ clipPath: FACET_CLIP_CSS }}
|
|
>
|
|
<svg
|
|
className="h-full w-full"
|
|
viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}
|
|
preserveAspectRatio="xMidYMid slice"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<defs>
|
|
<clipPath id="footer-facet-clip" clipPathUnits="userSpaceOnUse">
|
|
<path d={FACET_CLIP_PATH} />
|
|
</clipPath>
|
|
</defs>
|
|
<rect width={VIEW_W} height={VIEW_H} fill="#ffffff" />
|
|
<g clipPath="url(#footer-facet-clip)">
|
|
{FACET_CELLS.map((cell, i) => (
|
|
<path key={i} d={cell.d} fill={cell.fill} stroke="none" />
|
|
))}
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
|
|
{/* White wash above the curve only — keeps facet tops along the midline visible */}
|
|
<div
|
|
className="absolute inset-x-0 top-0 h-[44%] bg-gradient-to-b from-white via-white/75 to-transparent"
|
|
aria-hidden
|
|
/>
|
|
</div>
|
|
);
|
|
}
|