地図ピンのような不規則な形のSVGに、背景ぼかしをかけるCSS実装

下記画像のピンのように不規則な形のSVGの背景にCSSでぼかしをかけようとしたところ、思いの外詰まったので忘れないようにメモを残しておきます。

地図ピンのSVG画像の背景をぼかした例

FigmaのBackground Blur効果をSVGに適用した状態で書き出した場合は、インラインSVGとして描写することで比較的簡単に実現できます。今回はSVG側にぼかしが設定されていない場合の対処法となります。

使用したコード

今回使用するHTMLとCSSは以下の通りです。

index.html
<button class="pin"></button>
style.scss
.pin {
width: 38px;
aspect-ratio: 46 / 59;
background-image: url(../img/common/icon_pin.svg);
background-repeat: no-repeat;
background-size: contain;
}

HTMLにはSVGを配置せず、widthaspect-ratioでサイズを指定しつつbackground-imageプロパティでSVG画像を描写しています。この時の見た目は次のようになります。

背景のストライプがくっきり見え、この時点でぼかし効果はありません。

まず試したこと

ぼかしを表現するために、CSSにbackdrop-filter: blur(4px);を追加しました。

style.scss
.pin {
width: 38px;
aspect-ratio: 46 / 59;
background-image: url(../img/common/icon_pin.svg);
background-repeat: no-repeat;
background-size: contain;
backdrop-filter: blur(4px); // ぼかしを表現するために追加
}

すると次のような見た目になりました。

少しわかりにくいかもしれませんが、画像の四隅に沿ってぼかしがかかっています。

backdrop-filterはSVGのpathの形ではなく、要素の矩形領域に対してかかるため、形に沿ったぼかしは実現できません。MDNのbackdrop-filterでも適用対象に以下のように記載されていました。

適用対象:すべての要素。 SVG の場合は <defs> 要素やすべてのグラフィック要素を除いたコンテナー要素に適用される

以上のことから別のアプローチが必要となります。

解決の方針

pathの形に沿ってクリッピングすることで解決できるのではと考え、次のようにCSSを追加しました。

style.scss
.pin {
width: 38px;
aspect-ratio: 46 / 59;
background-image: url(../img/common/icon_pin.svg);
background-repeat: no-repeat;
background-size: contain;
backdrop-filter: blur(4px);
// clip-pathでSVGのパス形状をそのまま参照
clip-path: path(
"M45 22.9455C45 35.0656 23 57.8 23 57.8C23 57.8 1 35.0656 1 22.9455C1 10.8253 10.8497 1 23 1C35.1503 1 45 10.8253 45 22.9455Z"
);
}

pathの形状はSVGのd属性をコードから直接取得してきています。

<path d="M45 22.9455C45 35.0656 23 57.8 23 57.8C23 57.8 1 35.0656 1 22.9455C1 10.8253 10.8497 1 23 1C35.1503 1 45 10.8253 45 22.9455Z"/>

すると次の画像のようになりました(検証用にサイズを調整して拡大表示しています)。

ぼかし効果を確認できますが、左上が少し欠けていることがわかります。またぼかしが画像から少しはみ出していることも確認できます。pathの指定がうまくいっていないようです。

調べたところ、SVGのpath() の座標はピクセル固定のため、要素がリサイズされても座標は追従しないということがわかりました。確かにSVGの幅46pxに対して、CSSで幅38pxを指定していました。これがぼかしがズレる原因のようです。

最終的な解決策

path()で直接座標を指定するのではなく、インラインSVGのclipPathをCSSから参照する方法に方針転換しました。次のようにCSSで参照するインラインSVGをHTMLに追加しました。

index.html
<button class="pin"></button>
<svg width="0" height="0">
<defs>
<clipPath id="pin-clip" clipPathUnits="objectBoundingBox">
<path
d="M0.978 0.388C0.978 0.594 0.5 0.98 0.5 0.98C0.5 0.98 0.022 0.594 0.022 0.388C0.022 0.183 0.236 0.017 0.5 0.017C0.764 0.017 0.978 0.183 0.978 0.388Z" />
</clipPath>
</defs>
</svg>

ポイントとしてはclipPathclipPathUnits属性にobjectBoundingBoxを指定することです。これを指定することで座標が0〜1の相対値として扱われ、要素のサイズが変わっても形状が追従します。

またpathのd属性には、元のSVGのpath座標を相対値(0〜1)に変換した値を入れています。変換方法はシンプルで、x座標をviewBoxの幅(46)で、y座標をviewBoxの高さ(59)でそれぞれ割っています。

まとめると次のようになります。

clipPathUnits="objectBoundingBox"d属性の座標を0〜1に変換
役割座標系の宣言座標値の変換
何をするか「このclipPathの座標は0〜1の相対値として解釈する」とブラウザに伝える実際のpath座標をその座標系に合わせた値に書き換える
片方だけだとどうなるか座標系だけ変えても、座標値が46や59のままなので形が崩れる座標値を変換しても、座標系がデフォルト(ピクセル)のままなので追従しない

そしてCSSではHTMLに配置したpathを参照しています。

style.scss
.pin {
width: 38px;
aspect-ratio: 46 / 59;
background-image: url(../img/common/icon_pin.svg);
background-repeat: no-repeat;
background-size: contain;
backdrop-filter: blur(4px);
clip-path: path(
"M45 22.9455C45 35.0656 23 57.8 23 57.8C23 57.8 1 35.0656 1 22.9455C1 10.8253 10.8497 1 23 1C35.1503 1 45 10.8253 45 22.9455Z"
);
clip-path: url(#pin-clip);
}

最終的に以下のようになりました。

地図ピンのSVG画像の背景をぼかした例

綺麗にパスに沿ってクリッピングすることができました。CSSの幅を46pxに合わせなくても崩れることはありません。

clip-pathbackdrop-filterを組み合わせることで、CSSだけでSVGの形に沿ったぼかし表現が実現できました。Figmaで表現したデザインをそのまま再現したい場合に応用できそうです。

参考サイト

この記事を書いた人

写真:ブール 代表 西川雅人

Masato Nishikawa

ウェブ制作の現場でコーディングパートナーとして活動しているコーダーです。実務で得た知見やノウハウを、このブログで発信しています。

コーディングや
WordPress実装の
ご相談を承っています。

新規制作から運用フェーズの修正まで柔軟に対応します。
お見積りや実装可否の確認など、お気軽にご連絡ください。