import type { ReactNode } from "react"; type NoiseSurfaceProps = { children?: ReactNode; className?: string; /** Wash colour in the top left. */ from?: string; /** Wash colour in the bottom right. */ to?: string; background?: string; /** Grain strength, 0 to 1. */ grain?: number; /** Grain flickers like film. False keeps it still. */ moving?: boolean; }; const GRAIN = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")`; const CSS = ` @keyframes noise-surface-shift { 0% { transform: translate(0, 0); } 10% { transform: translate(-5%, -10%); } 20% { transform: translate(-15%, 5%); } 30% { transform: translate(7%, -25%); } 40% { transform: translate(-5%, 25%); } 50% { transform: translate(-15%, 10%); } 60% { transform: translate(15%, 0); } 70% { transform: translate(0, 15%); } 80% { transform: translate(3%, 35%); } 90% { transform: translate(-10%, 10%); } } .noise-surface-grain { animation: noise-surface-shift 1s steps(1) infinite; } @media (prefers-reduced-motion: reduce) { .noise-surface-grain { animation: none; } } `; /* NoiseSurface: a soft two-colour wash under film grain. CSS only. */ export function NoiseSurface({ children, className = "", from = "#a78bfa", to = "#fb7185", background = "#0b0912", grain = 0.2, moving = true, }: NoiseSurfaceProps) { const wash = [ `radial-gradient(120% 90% at 15% 10%, color-mix(in srgb, ${from} 40%, transparent), transparent 60%)`, `radial-gradient(100% 80% at 90% 100%, color-mix(in srgb, ${to} 35%, transparent), transparent 55%)`, background, ].join(", "); return (
{moving && }
{children}
); }