A pure SVG film-grain overlay applied to live editable text. The noise comes from an feTurbulence fractalNoise primitive (baseFrequency 0.85, 4 octaves, stitched) desaturated to mono, then blended over the source graphic with feBlend mode overlay. The amount slider scales the feComponentTransfer slope linearly from 0 to 2, sweeping from invisible speckle to heavy photocopy texture.
Grain
film noise overlay
Source
"use client";
import { useId, useState } from "react";
import ScrambleLink from "../ScrambleLink";
import DraggablePanel from "../DraggablePanel";
import { PanelColor, PanelSelect, PanelText } from "../_kansei/controls";
import type { FontChoice } from "../_kansei/fonts";
const FAMILY: Record<FontChoice, string> = {
display: "var(--font-display)",
sans: "var(--font-geist-sans)",
noto: "var(--font-noto-sans-jp)",
};
const W = 720;
const H = 360;
export default function Grain() {
const [text, setText] = useState("GRAIN");
const [fontSize, setFontSize] = useState(200);
const [font, setFont] = useState<FontChoice>("sans");
const [amount, setAmount] = useState(0.4);
const [inkColor, setInkColor] = useState("#1a1a1a");
const [bgColor, setBgColor] = useState("#f4efe6");
const filterId = `grain-${useId().replace(/:/g, "")}`;
const slope = amount * 2;
const intercept = 0.5 * (1 - slope);
return (
<div className="fixed inset-0 flex items-center justify-center select-none" style={{ background: bgColor }}>
<svg viewBox={`0 0 ${W} ${H}`} className="w-[90vw] max-w-[1000px] h-auto z-[1]" xmlns="http://www.w3.org/2000/svg">
<defs>
{amount > 0.01 && (
<filter id={filterId} x="0%" y="0%" width="100%" height="100%">
<feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves={4} stitchTiles="stitch" result="noise" />
<feColorMatrix in="noise" type="saturate" values="0" result="mono" />
<feComponentTransfer in="mono" result="scaled">
<feFuncR type="linear" slope={slope} intercept={intercept} />
<feFuncG type="linear" slope={slope} intercept={intercept} />
<feFuncB type="linear" slope={slope} intercept={intercept} />
</feComponentTransfer>
<feBlend in="SourceGraphic" in2="scaled" mode="overlay" />
</filter>
)}
</defs>
<g filter={amount > 0.01 ? `url(#${filterId})` : undefined}>
<rect width={W} height={H} fill={bgColor} />
<text
x="50%"
y="55%"
textAnchor="middle"
dominantBaseline="middle"
fill={inkColor}
fontSize={fontSize}
fontWeight={900}
style={{ fontFamily: FAMILY[font], fontStyle: font === "display" ? "italic" : "normal" }}
>
{text}
</text>
</g>
</svg>
<div
className="fixed bottom-8 left-8 pointer-events-none z-10 font-[family-name:var(--font-flux)]"
style={{ fontVariationSettings: "'wght' 300, 'SRIF' 100" }}
>
<p className="text-[12px] tracking-[0.08em] uppercase text-foreground/50">Grain</p>
<p className="text-[12px] tracking-[0.08em] mt-1 text-foreground/30">film noise overlay</p>
</div>
<DraggablePanel
isLight={false}
controls={[
{ label: "Amount", value: amount, set: setAmount, min: 0, max: 1, step: 0.02 },
{ label: "Font size", value: fontSize, set: (v) => setFontSize(Math.round(v)), min: 80, max: 360, step: 1 },
]}
>
<PanelText label="Text" value={text} set={setText} placeholder="GRAIN" isLight={false} />
<PanelSelect label="Font" value={font} options={["display", "sans", "noto"] as const} set={setFont} isLight={false} />
<PanelColor label="Ink" value={inkColor} set={setInkColor} isLight={false} />
<PanelColor label="Background" value={bgColor} set={setBgColor} isLight={false} />
</DraggablePanel>
<div className="fixed top-8 left-8 z-10">
<ScrambleLink
from="NEMAWASHI LAB"
to="← BACK"
href="/lab"
className="text-[14px] tracking-[0.08em] uppercase pointer-events-auto font-[family-name:var(--font-flux)] text-foreground/60"
style={{ fontVariationSettings: "'wght' 600, 'SRIF' 400" }}
/>
</div>
</div>
);
}