blobモーフィング

JSで制御点の半径を補間し、SVGパスを滑らかに変形させる有機的な背景。遊び心のあるLPの主役背景になります。

#svg#javascript#animation

ライブデモ

使用例(お題: カフェ MOON BREW)

この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- MOON BREW:モーフィングblobを主役にした季節限定ドリンク告知 -->
<section class="mb-stage">
  <div class="mb-grid">
    <!-- ★主役:滑らかに変形する有機blob(ドリンク写真を内側に) -->
    <div class="mb-visual">
      <svg class="mb-blob" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
        <defs>
          <clipPath id="mbClip"><path id="mbPath" d=""></path></clipPath>
          <linearGradient id="mbGrad" x1="0" y1="0" x2="1" y2="1">
            <stop offset="0" stop-color="#e6b877"/>
            <stop offset="1" stop-color="#c98a3b"/>
          </linearGradient>
        </defs>
        <path id="mbPathBg" d="" fill="url(#mbGrad)"></path>
        <image href="https://picsum.photos/200/200?random=81"
               x="10" y="10" width="180" height="180"
               preserveAspectRatio="xMidYMid slice" clip-path="url(#mbClip)"/>
      </svg>
      <span class="mb-tag">期間限定</span>
    </div>

    <div class="mb-copy">
      <span class="mb-eyebrow">SEASONAL</span>
      <h1 class="mb-title">琥珀ハニー<br>カフェラテ</h1>
      <p class="mb-sub">深煎りエスプレッソに国産はちみつを溶かした、まろやかな一杯。秋限定でお届けします。</p>
      <div class="mb-foot">
        <span class="mb-price">¥640<small>(税込)</small></span>
        <button class="mb-btn" type="button">注文する</button>
      </div>
    </div>
  </div>
</section>
CSS
/* MOON BREW:クリーム地に、モーフィングblobのドリンクビジュアル */
* { box-sizing: border-box; margin: 0; padding: 0; }

.mb-stage {
  min-height: 400px;
  height: 400px;
  overflow: hidden;
  background: radial-gradient(120% 120% at 0% 0%, #f7efe1 0%, #efe2cf 100%);
  font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", system-ui, sans-serif;
  color: #2b1d12;
  display: flex;
  align-items: center;
}

.mb-grid {
  display: grid;
  grid-template-columns: 0.9fr 1.1fr;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 0 30px;
}

/* ★主役:blobビジュアル */
.mb-visual {
  position: relative;
  display: grid;
  place-items: center;
}
.mb-blob {
  width: 230px;
  height: 230px;
  filter: drop-shadow(0 16px 30px rgba(160,110,40,0.35));
}
.mb-tag {
  position: absolute;
  top: 8px;
  right: 6px;
  font-size: 11px;
  font-weight: 800;
  color: #fff;
  background: #2b1d12;
  padding: 5px 12px;
  border-radius: 999px;
  transform: rotate(8deg);
  box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}

.mb-eyebrow {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.24em;
  color: #c98a3b;
}
.mb-title {
  margin-top: 8px;
  font-size: 32px;
  font-weight: 800;
  line-height: 1.2;
  letter-spacing: 0.02em;
  color: #2b1d12;
}
.mb-sub {
  margin-top: 12px;
  font-size: 13px;
  line-height: 1.85;
  color: #6b5340;
  max-width: 320px;
}

.mb-foot {
  margin-top: 18px;
  display: flex;
  align-items: center;
  gap: 16px;
}
.mb-price { font-size: 24px; font-weight: 800; color: #2b1d12; }
.mb-price small { font-size: 11px; font-weight: 500; color: #8a715a; margin-left: 4px; }
.mb-btn {
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  color: #fff;
  background: linear-gradient(135deg, #c98a3b, #a86c24);
  border: none;
  padding: 11px 24px;
  border-radius: 999px;
  cursor: pointer;
  box-shadow: 0 8px 18px rgba(201,138,59,0.45);
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.mb-btn:hover { transform: translateY(-2px); box-shadow: 0 12px 24px rgba(201,138,59,0.55); }
.mb-btn:active { transform: scale(0.97); }
JavaScript
// 制御点の半径を補間し、SVGのblobパスを滑らかに変形させる
(() => {
  const path = document.getElementById("mbPath");     // clip用
  const pathBg = document.getElementById("mbPathBg"); // 背面の色blob
  if (!path || !pathBg) return; // null安全

  const cx = 100, cy = 100;        // 中心
  const baseR = 78;                // 基準半径
  const n = 8;                     // 制御点の数
  const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  // 各制御点に揺れの位相と振幅を持たせる
  const seeds = Array.from({ length: n }, (_, i) => ({
    ang: (i / n) * Math.PI * 2,
    ph: Math.random() * Math.PI * 2,
    amp: 8 + Math.random() * 8,
    sp: 0.5 + Math.random() * 0.4,
  }));

  // catmull-rom 風に滑らかな閉パスを生成
  const buildPath = (t) => {
    const pts = seeds.map((s) => {
      const r = baseR + Math.sin(t * s.sp + s.ph) * s.amp;
      return [cx + Math.cos(s.ang) * r, cy + Math.sin(s.ang) * r];
    });
    let d = `M ${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)} `;
    for (let i = 0; i < n; i++) {
      const p0 = pts[(i - 1 + n) % n];
      const p1 = pts[i];
      const p2 = pts[(i + 1) % n];
      const p3 = pts[(i + 2) % n];
      // 制御点を計算して三次ベジェで接続
      const c1x = p1[0] + (p2[0] - p0[0]) / 6;
      const c1y = p1[1] + (p2[1] - p0[1]) / 6;
      const c2x = p2[0] - (p3[0] - p1[0]) / 6;
      const c2y = p2[1] - (p3[1] - p1[1]) / 6;
      d += `C ${c1x.toFixed(1)} ${c1y.toFixed(1)}, ${c2x.toFixed(1)} ${c2y.toFixed(1)}, ${p2[0].toFixed(1)} ${p2[1].toFixed(1)} `;
    }
    return d + "Z";
  };

  const apply = (d) => { path.setAttribute("d", d); pathBg.setAttribute("d", d); };

  if (reduce) {
    apply(buildPath(0)); // 静止
    return;
  }

  let raf = 0;
  const loop = (ts) => {
    apply(buildPath(ts / 1000));
    raf = requestAnimationFrame(loop);
  };
  raf = requestAnimationFrame(loop);

  // タブ非表示で停止
  document.addEventListener("visibilitychange", () => {
    cancelAnimationFrame(raf);
    if (!document.hidden) raf = requestAnimationFrame(loop);
  });
})();

コード

HTML
<!-- blobモーフィング: SVGパスを滑らかに変形させる有機的な背景 -->
<div class="blob-stage">
  <svg class="blob-svg" viewBox="0 0 400 400" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
    <defs>
      <linearGradient id="blobGrad" x1="0" y1="0" x2="1" y2="1">
        <stop offset="0%" stop-color="#ff6ec4"/>
        <stop offset="50%" stop-color="#7873f5"/>
        <stop offset="100%" stop-color="#21d4fd"/>
      </linearGradient>
    </defs>
    <path id="blobPath" fill="url(#blobGrad)" d=""/>
  </svg>
  <div class="blob-content">
    <h1 class="blob-title">Blob Morph</h1>
    <p class="blob-sub">JSで制御点を補間し、SVGの形がぬるぬると変形。遊び心あるLPの主役背景に。</p>
  </div>
</div>
CSS
/* ぼかしたblobを背景に置き、その上にテキスト */
* { box-sizing: border-box; margin: 0; padding: 0; }

.blob-stage {
  position: relative;
  min-height: 360px;
  overflow: hidden;
  display: grid;
  place-items: center;
  background: radial-gradient(120% 120% at 50% 100%, #1b1430 0%, #0c0a1a 70%);
  font-family: "Segoe UI", "Hiragino Sans", system-ui, sans-serif;
}

.blob-svg {
  position: absolute;
  width: 130%;
  height: 130%;
  left: -15%;
  top: -15%;
  filter: blur(8px) drop-shadow(0 20px 60px rgba(120, 115, 245, 0.4));
  opacity: 0.92;
}

.blob-content {
  position: relative;
  z-index: 2;
  text-align: center;
  color: #fff;
  padding: 0 24px;
  max-width: 480px;
}

.blob-title {
  font-size: 44px;
  font-weight: 800;
  letter-spacing: 0.02em;
  text-shadow: 0 6px 28px rgba(0, 0, 0, 0.4);
}

.blob-sub {
  margin-top: 14px;
  font-size: 14px;
  line-height: 1.85;
  color: rgba(255, 255, 255, 0.9);
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.5);
}
JavaScript
// 円周上の制御点の半径を揺らし、滑らかなパスとしてSVGに描画してモーフィングさせる
(() => {
  const path = document.getElementById("blobPath");
  if (!path) return; // null安全

  const CX = 200, CY = 200;     // 中心
  const BASE = 150;             // 基準半径
  const N = 8;                  // 制御点の数
  const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  // 各点の揺れ位相と速度をランダムに用意
  const seeds = Array.from({ length: N }, (_, i) => ({
    ang: (Math.PI * 2 * i) / N,
    phase: Math.random() * Math.PI * 2,
    speed: 0.4 + Math.random() * 0.5,
    amp: 18 + Math.random() * 22,
  }));

  // Catmull-Rom 風にスムーズな閉曲線パスを生成
  const buildPath = (pts) => {
    const n = pts.length;
    let d = `M ${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)} `;
    for (let i = 0; i < n; i++) {
      const p0 = pts[(i - 1 + n) % n];
      const p1 = pts[i];
      const p2 = pts[(i + 1) % n];
      const p3 = pts[(i + 2) % n];
      // 制御点を Catmull-Rom からベジェへ変換
      const c1x = p1[0] + (p2[0] - p0[0]) / 6;
      const c1y = p1[1] + (p2[1] - p0[1]) / 6;
      const c2x = p2[0] - (p3[0] - p1[0]) / 6;
      const c2y = p2[1] - (p3[1] - p1[1]) / 6;
      d += `C ${c1x.toFixed(1)} ${c1y.toFixed(1)}, ${c2x.toFixed(1)} ${c2y.toFixed(1)}, ${p2[0].toFixed(1)} ${p2[1].toFixed(1)} `;
    }
    return d + "Z";
  };

  // 時刻 t における各点座標を算出
  const pointsAt = (s) => seeds.map((sd) => {
    const r = BASE + Math.sin(s * sd.speed + sd.phase) * sd.amp;
    return [CX + Math.cos(sd.ang) * r, CY + Math.sin(sd.ang) * r];
  });

  let raf = 0;
  const loop = (t) => {
    path.setAttribute("d", buildPath(pointsAt(t / 1000)));
    raf = requestAnimationFrame(loop);
  };

  if (reduce) {
    path.setAttribute("d", buildPath(pointsAt(0))); // 静止
  } else {
    raf = requestAnimationFrame(loop);
    document.addEventListener("visibilitychange", () => {
      cancelAnimationFrame(raf); // 二重ループ防止
      if (!document.hidden) raf = requestAnimationFrame(loop);
    });
  }
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「blobモーフィング」の効果を追加してください。

# 追加してほしい効果
blobモーフィング(背景 & グラデーション)
JSで制御点の半径を補間し、SVGパスを滑らかに変形させる有機的な背景。遊び心のあるLPの主役背景になります。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- blobモーフィング: SVGパスを滑らかに変形させる有機的な背景 -->
<div class="blob-stage">
  <svg class="blob-svg" viewBox="0 0 400 400" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
    <defs>
      <linearGradient id="blobGrad" x1="0" y1="0" x2="1" y2="1">
        <stop offset="0%" stop-color="#ff6ec4"/>
        <stop offset="50%" stop-color="#7873f5"/>
        <stop offset="100%" stop-color="#21d4fd"/>
      </linearGradient>
    </defs>
    <path id="blobPath" fill="url(#blobGrad)" d=""/>
  </svg>
  <div class="blob-content">
    <h1 class="blob-title">Blob Morph</h1>
    <p class="blob-sub">JSで制御点を補間し、SVGの形がぬるぬると変形。遊び心あるLPの主役背景に。</p>
  </div>
</div>

【CSS】
/* ぼかしたblobを背景に置き、その上にテキスト */
* { box-sizing: border-box; margin: 0; padding: 0; }

.blob-stage {
  position: relative;
  min-height: 360px;
  overflow: hidden;
  display: grid;
  place-items: center;
  background: radial-gradient(120% 120% at 50% 100%, #1b1430 0%, #0c0a1a 70%);
  font-family: "Segoe UI", "Hiragino Sans", system-ui, sans-serif;
}

.blob-svg {
  position: absolute;
  width: 130%;
  height: 130%;
  left: -15%;
  top: -15%;
  filter: blur(8px) drop-shadow(0 20px 60px rgba(120, 115, 245, 0.4));
  opacity: 0.92;
}

.blob-content {
  position: relative;
  z-index: 2;
  text-align: center;
  color: #fff;
  padding: 0 24px;
  max-width: 480px;
}

.blob-title {
  font-size: 44px;
  font-weight: 800;
  letter-spacing: 0.02em;
  text-shadow: 0 6px 28px rgba(0, 0, 0, 0.4);
}

.blob-sub {
  margin-top: 14px;
  font-size: 14px;
  line-height: 1.85;
  color: rgba(255, 255, 255, 0.9);
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.5);
}

【JavaScript】
// 円周上の制御点の半径を揺らし、滑らかなパスとしてSVGに描画してモーフィングさせる
(() => {
  const path = document.getElementById("blobPath");
  if (!path) return; // null安全

  const CX = 200, CY = 200;     // 中心
  const BASE = 150;             // 基準半径
  const N = 8;                  // 制御点の数
  const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  // 各点の揺れ位相と速度をランダムに用意
  const seeds = Array.from({ length: N }, (_, i) => ({
    ang: (Math.PI * 2 * i) / N,
    phase: Math.random() * Math.PI * 2,
    speed: 0.4 + Math.random() * 0.5,
    amp: 18 + Math.random() * 22,
  }));

  // Catmull-Rom 風にスムーズな閉曲線パスを生成
  const buildPath = (pts) => {
    const n = pts.length;
    let d = `M ${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)} `;
    for (let i = 0; i < n; i++) {
      const p0 = pts[(i - 1 + n) % n];
      const p1 = pts[i];
      const p2 = pts[(i + 1) % n];
      const p3 = pts[(i + 2) % n];
      // 制御点を Catmull-Rom からベジェへ変換
      const c1x = p1[0] + (p2[0] - p0[0]) / 6;
      const c1y = p1[1] + (p2[1] - p0[1]) / 6;
      const c2x = p2[0] - (p3[0] - p1[0]) / 6;
      const c2y = p2[1] - (p3[1] - p1[1]) / 6;
      d += `C ${c1x.toFixed(1)} ${c1y.toFixed(1)}, ${c2x.toFixed(1)} ${c2y.toFixed(1)}, ${p2[0].toFixed(1)} ${p2[1].toFixed(1)} `;
    }
    return d + "Z";
  };

  // 時刻 t における各点座標を算出
  const pointsAt = (s) => seeds.map((sd) => {
    const r = BASE + Math.sin(s * sd.speed + sd.phase) * sd.amp;
    return [CX + Math.cos(sd.ang) * r, CY + Math.sin(sd.ang) * r];
  });

  let raf = 0;
  const loop = (t) => {
    path.setAttribute("d", buildPath(pointsAt(t / 1000)));
    raf = requestAnimationFrame(loop);
  };

  if (reduce) {
    path.setAttribute("d", buildPath(pointsAt(0))); // 静止
  } else {
    raf = requestAnimationFrame(loop);
    document.addEventListener("visibilitychange", () => {
      cancelAnimationFrame(raf); // 二重ループ防止
      if (!document.hidden) raf = requestAnimationFrame(loop);
    });
  }
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。