NEMAWASHI LAB

Melt

SVG

Two modes of the same idea: making letterforms look liquid. Inflate mode chains an SVG feMorphology dilate, a Gaussian blur, and a high-pass colour matrix (alpha channel: 18x - 7) to round off sharp edges into blobby, inflated shapes, with an optional ring variant that composites dilate-out to produce a thick organic border. Goo mode takes the CSS shortcut -- blur the text, then crank contrast to 20 on a same-colour background -- collapsing the blur fringe into gooey metaball edges.

Kansei

Melt

inflate

Controls
Font size96
Dilate2.0
Blur4.0
Border width4
Text
Mode
Font
Text
Border
Background

Source

Melt.tsx118 lines
"use client";

import { useId, useState } from "react";
import ScrambleLink from "../ScrambleLink";
import DraggablePanel from "../DraggablePanel";
import { PanelColor, PanelSelect, PanelToggle, PanelText } from "../_kansei/controls";
import { FONT_STYLES, type FontChoice } from "../_kansei/fonts";

const ALPHA = "1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 18 -7";

export default function Melt() {
  const [mode, setMode] = useState<"inflate" | "goo">("inflate");
  const [dilateRadius, setDilateRadius] = useState(2);
  const [blurAmount, setBlurAmount] = useState(4);
  const [fontSize, setFontSize] = useState(96);
  const [font, setFont] = useState<FontChoice>("display");
  const [blobText, setBlobText] = useState("Kansei");
  const [showBorder, setShowBorder] = useState(false);
  const [textColor, setTextColor] = useState("#5b7fc4");
  const [borderColor, setBorderColor] = useState("#3a5fa0");
  const [borderWidth, setBorderWidth] = useState(4);
  const [bgColor, setBgColor] = useState("#f0e8d8");

  const uid = useId().replace(/:/g, "");
  const inflateId = `blob-inflate-${uid}`;
  const ringId = `blob-ring-${uid}`;
  const fontStyle = FONT_STYLES[font];

  return (
    <div className="fixed inset-0 flex items-center justify-center select-none" style={{ background: bgColor }}>
      {/* filter defs */}
      <svg width="0" height="0" style={{ position: "absolute" }} aria-hidden>
        <defs>
          <filter id={inflateId} x="-20%" y="-20%" width="140%" height="140%">
            <feMorphology operator="dilate" radius={dilateRadius} result="dilated" />
            <feGaussianBlur in="dilated" stdDeviation={blurAmount * 0.5} result="blurred" />
            <feComposite in="blurred" in2="SourceGraphic" operator="over" result="withOutline" />
            <feColorMatrix in="withOutline" type="matrix" values={ALPHA} />
          </filter>
          <filter id={ringId} x="-25%" y="-25%" width="150%" height="150%">
            <feMorphology operator="dilate" radius={dilateRadius} result="dilated" />
            <feGaussianBlur in="dilated" stdDeviation={blurAmount * 0.5} result="blurred" />
            <feComposite in="blurred" in2="SourceGraphic" operator="over" result="comp" />
            <feColorMatrix in="comp" type="matrix" values={ALPHA} result="blob" />
            <feFlood floodColor={borderColor} result="flood" />
            <feComposite in="flood" in2="blob" operator="in" result="ink" />
            <feMorphology operator="dilate" in="ink" radius={borderWidth} result="outer" />
            <feComposite in="outer" in2="blob" operator="out" />
          </filter>
        </defs>
      </svg>

      {mode === "inflate" ? (
        <div className="relative flex items-center justify-center z-[1]" style={{ height: fontSize * 2 }}>
          <span
            aria-hidden
            className="block text-center font-black leading-none"
            style={{ fontSize, color: textColor, filter: `url(#${showBorder ? ringId : inflateId})`, position: "absolute", ...fontStyle }}
          >
            {blobText}
          </span>
          <span
            className="block text-center font-black leading-none"
            style={{ fontSize, color: showBorder ? "transparent" : textColor, position: "relative", zIndex: 1, ...fontStyle }}
          >
            {blobText}
          </span>
        </div>
      ) : (
        <div
          className="z-[1] flex items-center justify-center"
          style={{ filter: `blur(${blurAmount}px) contrast(20)`, background: bgColor, height: fontSize * 2, padding: "0 40px" }}
        >
          <span className="block text-center font-black leading-none" style={{ fontSize, color: textColor, ...fontStyle }}>
            {blobText}
          </span>
        </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">Melt</p>
        <p className="text-[12px] tracking-[0.08em] mt-1 text-foreground/30">{mode}</p>
      </div>

      <DraggablePanel
        isLight={false}
        controls={[
          { label: "Font size", value: fontSize, set: (v) => setFontSize(Math.round(v)), min: 48, max: 200, step: 1 },
          { label: "Dilate", value: dilateRadius, set: setDilateRadius, min: 0, max: 10, step: 0.5 },
          { label: "Blur", value: blurAmount, set: setBlurAmount, min: 0, max: 16, step: 0.5 },
          { label: "Border width", value: borderWidth, set: (v) => setBorderWidth(Math.round(v)), min: 1, max: 16, step: 1 },
        ]}
      >
        <PanelText label="Text" value={blobText} set={setBlobText} placeholder="Kansei" isLight={false} />
        <PanelSelect label="Mode" value={mode} options={["inflate", "goo"] as const} set={setMode} isLight={false} />
        <PanelSelect label="Font" value={font} options={["display", "sans", "noto"] as const} set={setFont} isLight={false} />
        <PanelToggle label="Border (inflate)" value={showBorder} set={setShowBorder} isLight={false} />
        <PanelColor label="Text" value={textColor} set={setTextColor} isLight={false} />
        <PanelColor label="Border" value={borderColor} set={setBorderColor} 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