{
  "id": "motion:motion-ink-bleed",
  "slug": "motion-ink-bleed",
  "kind": "motion",
  "title": {
    "ja": "滲み",
    "en": "Ink bleed"
  },
  "aiName": "Ink bleed text reveal",
  "summary": {
    "ja": "にじんだ墨が輪郭を結んで文字になる",
    "en": "A blot of bled ink resolves into crisp letters"
  },
  "number": 58,
  "group": "text-motion",
  "techniques": [
    "css-filter",
    "svg",
    "scroll-progress"
  ],
  "rules": [
    {
      "ja": "ぼかしが 0 になったらフィルターごと外し、通常の描画に戻す（JS が動かなくても文字が読め、細い字の縁も痩せない）",
      "en": "Remove the filter altogether once the blur reaches 0 so the text renders normally, reads without JavaScript and keeps its thin edges"
    },
    {
      "ja": "色は変えず、透明度だけに段差をつける（色のリテラルを書かない）",
      "en": "Step only the alpha channel and never write colour literals"
    },
    {
      "ja": "フィルターは 1 本を見出しと副題で共有する",
      "en": "Share one filter between the heading and the subtitle"
    },
    {
      "ja": "動きを減らす設定ではフィルターを当てない",
      "en": "Never apply the filter under reduced motion"
    }
  ],
  "reproduction": {
    "prompt": {
      "ja": "滲み — Ink bleed text reveal\n\nにじんだ墨の塊が輪郭を結んで文字になる動きを作ってください。SVG の filter を 1 本定義し、feGaussianBlur（stdDeviation を動かす）の出力を feColorMatrix（values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 22 -9.5\"、透明度を 22 倍して 9.5 を引いて段差をつける）に通します。見出しと副題の CSS に filter: url(#…) を当て、340vh の pin 区画の進み 10% から 70% で、stdDeviation を 9 × (1 − e)^1.6（e は 0 から 1）で 9 から 0 へ戻します。ぼかしが大きいあいだは墨の塊が丸く滲み、小さくなるにつれて縁が文字の形に締まります。ぼかしが 0 になったらフィルターを外して通常の描画に戻すので、JavaScript が動かなくても読めます。動きを減らす設定ではフィルターを当てません。CSS と JavaScript だけで実装し、ライブラリは使わないでください。\n\n外すと崩れる点:\n- ぼかしが 0 になったらフィルターごと外し、通常の描画に戻す（JS が動かなくても文字が読め、細い字の縁も痩せない）\n- 色は変えず、透明度だけに段差をつける（色のリテラルを書かない）\n- フィルターは 1 本を見出しと副題で共有する\n- 動きを減らす設定ではフィルターを当てない\n\n動く実物（1 ファイルで完結する HTML）: https://design-vault.pages.dev/designs/motion-ink-bleed/demo.html",
      "en": "Ink bleed — Ink bleed text reveal\n\nMake a blot of bled ink resolve into crisp letters. Define one SVG filter that passes feGaussianBlur (with an animated stdDeviation) through an feColorMatrix with values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 22 -9.5\", which multiplies alpha by 22 and subtracts 9.5 to create a hard step. Apply filter: url(#…) to the heading and its subtitle and, from 10% to 70% of a 340vh pinned section, move stdDeviation from 9 down to 0 as 9 × (1 − e)^1.6, with e running 0 to 1. While the blur is large the ink swells into round blots, and as it shrinks the edges tighten into the letterforms. Once the blur reaches 0 remove the filter so the text renders normally and reads even without JavaScript. With reduced motion never apply it. Implement it with CSS and JavaScript only, without any library.\n\nDo not drop:\n- Remove the filter altogether once the blur reaches 0 so the text renders normally, reads without JavaScript and keeps its thin edges\n- Step only the alpha channel and never write colour literals\n- Share one filter between the heading and the subtitle\n- Never apply the filter under reduced motion\n\nA working example (one self-contained HTML file): https://design-vault.pages.dev/designs/motion-ink-bleed/demo.html"
    },
    "code": {
      "url": "https://design-vault.pages.dev/designs/motion-ink-bleed/demo.html",
      "html": "<!doctype html>\n<html lang=\"ja\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>58 滲み — Ink bleed text reveal</title>\n<style>\n/* dv-motion-base:start */\n:root {\n  --ground: #FFFFFF;\n  --surface: #F3F5F8;\n  --ink: #14181F;\n  --muted: #5A6472;\n  --accent: #1F5FA8;\n  --rule: #D8DDE4;\n  --accent-soft: color-mix(in srgb, var(--accent) 12%, var(--ground));\n  --accent-mid: color-mix(in srgb, var(--accent) 45%, var(--ground));\n  --shade: color-mix(in srgb, var(--ink) 14%, transparent);\n\n  --font: \"Zen Kaku Gothic New\", \"Hiragino Sans\", \"Noto Sans JP\", \"Yu Gothic\", system-ui, sans-serif;\n  --mono: ui-monospace, \"SF Mono\", Menlo, Consolas, monospace;\n\n  --t-label: 13px;\n  --t-body: 16px;\n  --t-lead: 20px;\n  --t-h2: 32px;\n  --t-h1: clamp(40px, 7vw, 72px);\n\n  --s1: 4px;\n  --s2: 8px;\n  --s3: 16px;\n  --s4: 24px;\n  --s5: 40px;\n  --s6: 64px;\n  --s7: 104px;\n\n  --radius: 12px;\n  --ease: cubic-bezier(.2, .7, .2, 1);\n  --dur: .7s;\n  --measure: 1080px;\n}\n*, *::before, *::after { box-sizing: border-box; }\nhtml { -webkit-text-size-adjust: 100%; }\nbody {\n  margin: 0;\n  background: var(--ground);\n  color: var(--ink);\n  font-family: var(--font);\n  font-size: var(--t-body);\n  line-height: 1.7;\n  font-feature-settings: \"palt\";\n  overflow-x: hidden;\n}\nh1, h2, h3, p, ul, ol, figure { margin: 0; padding: 0; }\nul, ol { list-style: none; }\n.dv-wrap { width: min(100% - 2 * var(--s4), var(--measure)); margin-inline: auto; }\n.dv-label {\n  min-height: 72vh;\n  display: grid;\n  align-content: center;\n  gap: var(--s3);\n  padding-block: var(--s6);\n  border-bottom: 1px solid var(--rule);\n}\n.dv-num { font-family: var(--mono); font-size: var(--t-lead); font-weight: 700; color: var(--accent); letter-spacing: .04em; }\n.dv-name { font-size: var(--t-h1); font-weight: 700; line-height: 1.15; letter-spacing: .01em; }\n.dv-ai { font-family: var(--mono); font-size: var(--t-lead); color: var(--muted); }\n.dv-desc { font-size: var(--t-lead); color: var(--ink); }\n.dv-hint { font-size: var(--t-label); color: var(--muted); letter-spacing: .12em; margin-top: var(--s4); }\n.dv-stage { padding-block: var(--s7); }\n.dv-h2 { font-size: var(--t-h2); font-weight: 700; line-height: 1.3; }\n.dv-lead { font-size: var(--t-lead); color: var(--muted); }\n.dv-eyebrow { font-family: var(--mono); font-size: var(--t-label); color: var(--accent); letter-spacing: .12em; text-transform: uppercase; }\n.dv-card { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius); padding: var(--s4); }\n.dv-ph {\n  border-radius: var(--radius);\n  background: linear-gradient(135deg, var(--accent-soft), var(--accent-mid) 60%, var(--accent));\n  aspect-ratio: 16 / 9;\n}\n.dv-ph--alt { background: linear-gradient(200deg, var(--surface), var(--accent-soft) 55%, var(--accent-mid)); }\n.dv-ph--ink { background: linear-gradient(160deg, var(--ink), color-mix(in srgb, var(--ink) 55%, var(--accent))); }\n.dv-end {\n  min-height: 60vh;\n  display: grid;\n  place-items: center;\n  border-top: 1px solid var(--rule);\n  color: var(--muted);\n  font-family: var(--mono);\n  font-size: var(--t-label);\n  letter-spacing: .12em;\n}\n/* dv-motion-base:end */\n\n/* ---- この演出だけの CSS。色は var(--…) か color-mix() のみ ---- */\n.stack { display: grid; gap: var(--s6); }\n.ib-zone { height: 340vh; }\n.ib-stick { position: sticky; top: 0; height: 100vh; display: grid; place-content: center; gap: var(--s4); overflow: clip; }\n@media (prefers-reduced-motion: reduce) {\n  .ib-zone { height: auto; }\n  .ib-stick { position: static; height: auto; padding-block: var(--s6); }\n}\n.ib-title { font-size: clamp(40px, 11vw, 140px); font-weight: 700; line-height: 1.2; text-align: center; }\n.ib-sub { font-size: clamp(16px, 3vw, 32px); color: var(--muted); text-align: center; letter-spacing: .3em; }\n/* 滲みは JS が動くあいだだけ。止まったあとは通常の描画に戻し、細い字の縁が痩せないようにする。 */\n.js .ib-title.is-bleeding, .js .ib-sub.is-bleeding { filter: url(#ib-bleed); }\n.ib-defs { position: absolute; width: 0; height: 0; }\n</style>\n</head>\n<body data-motion-number=\"58\">\n<header class=\"dv-label\">\n  <div class=\"dv-wrap\">\n    <p class=\"dv-num\">58</p>\n    <h1 class=\"dv-name\">滲み</h1>\n    <p class=\"dv-ai\">Ink bleed text reveal</p>\n    <p class=\"dv-desc\">にじんだ墨が輪郭を結んで文字になる</p>\n    <p class=\"dv-hint\">SCROLL ↓</p>\n  </div>\n</header>\n\n<main class=\"dv-stage\">\n<div class=\"dv-wrap stack\">\n    <section class=\"dv-card\">\n      <p class=\"dv-eyebrow\">Section 1</p>\n      <h2 class=\"dv-h2\">見出しが入ります</h2>\n      <p class=\"dv-lead\">本文のダミーです。スクロールすると、下の文字が、にじんだ墨の滲みから輪郭を結んでいきます。</p>\n    </section>\n  </div>\n\n  <section class=\"ib-zone\" data-progress=\"pin\">\n    <div class=\"ib-stick\">\n      <svg class=\"ib-defs\" aria-hidden=\"true\" xmlns=\"http://www.w3.org/2000/svg\">\n        <filter id=\"ib-bleed\" x=\"-10%\" y=\"-30%\" width=\"120%\" height=\"160%\" color-interpolation-filters=\"sRGB\">\n          <feGaussianBlur id=\"ib-blur\" in=\"SourceGraphic\" stdDeviation=\"0\"/>\n          <feColorMatrix type=\"matrix\" values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 22 -9.5\"/>\n        </filter>\n      </svg>\n      <h2 class=\"ib-title\">墨がにじむ</h2>\n      <p class=\"ib-sub\">INK BLEED</p>\n    </div>\n  </section>\n\n  <div class=\"dv-wrap stack\">\n    <section class=\"dv-card\">\n      <p class=\"dv-eyebrow\">Section 2</p>\n      <h2 class=\"dv-h2\">見出しが入ります</h2>\n      <p class=\"dv-lead\">本文のダミーです。戻すと、文字はまた滲みの中へ溶けます。</p>\n    </section>\n  </div>\n</main>\n\n<footer class=\"dv-end\">END — 58 滲み</footer>\n\n<script>\n/* dv-motion-helpers:start */\nconst dvReduce = matchMedia('(prefers-reduced-motion: reduce)').matches;\ndocument.documentElement.classList.add('js');\n\n/**\n * 画面に入った要素へ .is-in を付ける。\n * data-reveal=\"repeat\" の要素は画面を出たら外す（何度でも再生）。\n */\nfunction dvReveal(selector = '[data-reveal]', { threshold = 0.2, rootMargin = '0px 0px -8% 0px' } = {}) {\n  const els = [...document.querySelectorAll(selector)];\n  if (dvReduce || !('IntersectionObserver' in window)) {\n    els.forEach((el) => el.classList.add('is-in'));\n    return;\n  }\n  const io = new IntersectionObserver((entries) => {\n    for (const e of entries) {\n      if (e.isIntersecting) {\n        e.target.classList.add('is-in');\n        if (e.target.dataset.reveal !== 'repeat') io.unobserve(e.target);\n      } else if (e.target.dataset.reveal === 'repeat') {\n        e.target.classList.remove('is-in');\n      }\n    }\n  }, { threshold, rootMargin });\n  els.forEach((el) => io.observe(el));\n}\n\n/**\n * スクロール量を 0〜1 にして、各 [data-progress] 要素の CSS 変数 --p に書く。\n *   data-progress=\"through\" : 要素の上端が画面の下端に来たとき 0、下端が画面の上端を抜けたとき 1\n *   data-progress=\"pin\"     : 要素の上端が画面の上端に来たとき 0、下端が画面の下端に来たとき 1\n *                             （中に position: sticky の箱を持つ、背の高い区画用）\n *   data-progress=\"page\"    : ページ全体のスクロール量\n * onUpdate(el, p) を渡すと、値が変わるたびに呼ばれる。\n */\nfunction dvProgress(onUpdate) {\n  const els = [...document.querySelectorAll('[data-progress]')];\n  const clamp = (n) => Math.min(1, Math.max(0, n));\n  let queued = false;\n  const measure = () => {\n    queued = false;\n    const vh = window.innerHeight;\n    for (const el of els) {\n      const r = el.getBoundingClientRect();\n      let p;\n      if (el.dataset.progress === 'pin') p = r.height > vh ? -r.top / (r.height - vh) : 0;\n      else if (el.dataset.progress === 'page') {\n        const max = document.documentElement.scrollHeight - vh;\n        p = max > 0 ? window.scrollY / max : 0;\n      } else p = (vh - r.top) / (vh + r.height);\n      p = clamp(p);\n      el.style.setProperty('--p', p.toFixed(4));\n      if (onUpdate) onUpdate(el, p);\n    }\n  };\n  const request = () => {\n    if (!queued) { queued = true; requestAnimationFrame(measure); }\n  };\n  addEventListener('scroll', request, { passive: true });\n  addEventListener('resize', request);\n  measure();\n}\n/* dv-motion-helpers:end */\n\n/* ---- この演出だけの JS ---- */\n// ぼかしてから、透明度に段差をつけて切る（22 倍して 9.5 を引く）と、墨が紙に滲んだ縁になる。\n// ぼかしを大きい値から 0 へ戻すと、滲みから輪郭が結ばれる。\nconst blur = document.getElementById('ib-blur');\nconst targets = document.querySelectorAll('.ib-title, .ib-sub');\nfunction place(p) {\n  const e = Math.min(1, Math.max(0, (p - 0.1) / 0.6));\n  const sd = 9 * Math.pow(1 - e, 1.6);\n  blur.setAttribute('stdDeviation', sd.toFixed(2));\n  // ぼかしが 0 になったら、フィルターごと外す。\n  targets.forEach((t) => t.classList.toggle('is-bleeding', e < 1));\n}\nif (!dvReduce) dvProgress((el, p) => place(p));\n</script>\n</body>\n</html>\n"
    }
  },
  "license": "Original",
  "catalogVersion": 1,
  "designSchemaVersion": 2,
  "capabilities": [
    "live-preview",
    "copy-prompt",
    "get-code"
  ],
  "reusePolicy": "construction-reuse",
  "urls": {
    "ja": "https://design-vault.pages.dev/designs/motion-ink-bleed",
    "en": "https://design-vault.pages.dev/en/designs/motion-ink-bleed",
    "json": "https://design-vault.pages.dev/ai/assets/motion-ink-bleed.json",
    "designJson": null
  }
}