blobモーフィング
JSで制御点の半径を補間し、SVGパスを滑らかに変形させる有機的な背景。遊び心のあるLPの主役背景になります。
ライブデモ
使用例(お題: カフェ 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。