A two-layer SVG filter chain simulates letterpress printing on textured paper. The inner filter displaces the text outline with fractal turbulence (roughening the edges the way ink spreads into fibres), then blends in a second turbulence layer as a grain texture and quantises the alpha channel through a discrete transfer function (eight steps from 0 to 1) to mimic uneven ink density. A separate whole-page grain filter adds subtle paper noise on top. The result reads like a worn Risograph pull with displacement, grain frequency, and opacity all tuneable.
displacement · grain
Source
"use client";
import { useId, useState } from "react";
import ScrambleLink from "../ScrambleLink";
import DraggablePanel from "../DraggablePanel";
import { PanelColor, PanelSelect, PanelText, PanelTextArea } from "../_kansei/controls";
import { FONT_STYLES, type FontChoice } from "../_kansei/fonts";
export default function Print() {
const [dispScale, setDispScale] = useState(2.0);
const [grainFreq, setGrainFreq] = useState(0.04);
const [grainOpacity, setGrainOpacity] = useState(0.15);
const [fontSize, setFontSize] = useState(64);
const [font, setFont] = useState<FontChoice>("display");
const [textColor, setTextColor] = useState("#1a1814");
const [bgColor, setBgColor] = useState("#efe7d6");
const [heroLine1, setHeroLine1] = useState("Distill the soul.");
const [heroLine2, setHeroLine2] = useState("Define the code.");
const [bodyText, setBodyText] = useState(
"Every design carries a hidden logic — the grain of a decision, the weight of restraint.",
);
const uid = useId().replace(/:/g, "");
const inkId = `ink-paper-${uid}`;
const grainId = `grain-${uid}`;
const fontStyle = FONT_STYLES[font];
return (
<div className="fixed inset-0 flex items-center justify-center select-none" style={{ background: bgColor }}>
<svg width="0" height="0" style={{ position: "absolute", overflow: "hidden" }} aria-hidden>
<defs>
<filter id={inkId} x="-5%" y="-5%" width="110%" height="110%" colorInterpolationFilters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency={0.65} numOctaves={4} seed={2} result="edgeTurbulence" />
<feDisplacementMap in="SourceGraphic" in2="edgeTurbulence" scale={dispScale} xChannelSelector="R" yChannelSelector="G" result="displaced" />
<feTurbulence type="fractalNoise" baseFrequency={grainFreq} numOctaves={6} seed={8} result="paperGrain" />
<feColorMatrix in="paperGrain" type="saturate" values="0" result="greyGrain" />
<feComponentTransfer in="greyGrain" result="fadedGrain">
<feFuncA type="linear" slope={grainOpacity} />
</feComponentTransfer>
<feBlend in="displaced" in2="fadedGrain" mode="multiply" result="withGrain" />
<feComponentTransfer in="withGrain" result="inkDensity">
<feFuncA type="discrete" tableValues="0 0.6 0.75 0.88 0.94 0.97 0.99 1" />
</feComponentTransfer>
<feComposite in="inkDensity" in2="SourceGraphic" operator="in" result="composited" />
</filter>
<filter id={grainId} x="0%" y="0%" width="100%" height="100%">
<feTurbulence type="fractalNoise" baseFrequency={0.72} numOctaves={4} seed={15} result="paperNoise" />
<feColorMatrix in="paperNoise" type="saturate" values="0" result="greyNoise" />
<feComponentTransfer in="greyNoise" result="subtleNoise">
<feFuncA type="linear" slope={0.045} />
</feComponentTransfer>
<feBlend in="SourceGraphic" in2="subtleNoise" mode="multiply" />
</filter>
</defs>
</svg>
<div className="relative w-full max-w-3xl px-10 z-[1]" style={{ filter: `url(#${grainId})` }}>
<div className="relative flex flex-col gap-6" style={{ filter: `url(#${inkId})` }}>
<div className="flex flex-col gap-1 leading-[1.1]">
<span className="block" style={{ fontSize, color: textColor, ...fontStyle }}>{heroLine1}</span>
<span className="block" style={{ fontSize, color: textColor, ...fontStyle, fontStyle: "normal" }}>{heroLine2}</span>
</div>
<span className="block max-w-xl text-[24px] leading-snug" style={{ color: textColor, ...fontStyle }}>{bodyText}</span>
<span className="block text-[14px] leading-relaxed" style={{ color: textColor, opacity: 0.6, ...fontStyle }}>
A tool that reads images and renders their soul as code.
</span>
</div>
</div>
<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">Print</p>
<p className="text-[12px] tracking-[0.08em] mt-1 text-foreground/30">displacement · grain</p>
</div>
<DraggablePanel
isLight={false}
controls={[
{ label: "Font size", value: fontSize, set: (v) => setFontSize(Math.round(v)), min: 28, max: 120, step: 1 },
{ label: "Displacement", value: dispScale, set: setDispScale, min: 0, max: 10, step: 0.1 },
{ label: "Grain freq", value: grainFreq, set: setGrainFreq, min: 0.01, max: 0.2, step: 0.005 },
{ label: "Grain opacity", value: grainOpacity, set: setGrainOpacity, min: 0, max: 0.4, step: 0.01 },
]}
>
<PanelText label="Line 1" value={heroLine1} set={setHeroLine1} isLight={false} />
<PanelText label="Line 2" value={heroLine2} set={setHeroLine2} isLight={false} />
<PanelTextArea label="Body" value={bodyText} set={setBodyText} rows={3} isLight={false} />
<PanelSelect label="Font" value={font} options={["display", "sans", "noto"] as const} set={setFont} isLight={false} />
<PanelColor label="Ink" value={textColor} set={setTextColor} isLight={false} />
<PanelColor label="Paper" 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>
);
}