NEMAWASHI LAB

Grain

SVG

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

Grain

film noise overlay

Controls
Amount0.40
Font size200
Text
Font
Ink
Background

Source

Grain.tsx96 lines
"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>
  );
}

NEMAWASHI — Kotaro Abe