NEMAWASHI LAB

Print

SVG

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.

Distill the soul.Define the code.
Every design carries a hidden logic — the grain of a decision, the weight of restraint.A tool that reads images and renders their soul as code.

Print

displacement · grain

Controls
Font size64
Displacement2.0
Grain freq0.04
Grain opacity0.15
Line 1
Line 2
Body
Font
Ink
Paper

Source

Print.tsx107 lines
"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>
  );
}

NEMAWASHI — Kotaro Abe