実装の仕組みが分かれば簡単!画像の一部を切り取るカットアウトを実装するCSSとSVGのテクニック

Webページやアプリで見かける、通知や注目の役割を担うカットアウト(画像の一部を切り取る)を実装するCSSとSVGのテクニックを紹介します。

画像に小さなバッジをつけたり、画像をグループ化する際に重ねたり、ヘッダやヒーローで画像を重ねたり、さまざまなUIで見かけます。

画像の一部を切り取るカットアウトを実装するCSSとSVGのテクニック

Thinking About The Cut-Out Effect: CSS or SVG?
by Ahmad Shadeed

下記は各ポイントを意訳したものです。
※当ブログでの翻訳記事は、元サイト様にライセンスを得て翻訳しています。

カットアウトとは

私が最近取り組んだフロントエンドのプロジェクトで、コンポーネントの一つに興味深いカットアウトのエフェクトが含まれていました。このカットアウトの実装にはCSSやSVGなど複数の方法がありますが、それぞれに長所と短所があります。私はこの課題に対する解決策を探り、みなさんと共有しようと考えました。

この解説記事を読むにあたって、CSSとSVGの基本的な知識が必要ですが、なるべく詳細に解説しようと思いますので、おそらく大丈夫でしょう。

まずは、カットアウトのエフェクトがどういうものかを紹介します。形から一部分を切り取ったものです。

カットアウトのエフェクト

カットアウトのエフェクトとは、形の一部を切り取ったもの

矩形から円形を減算して、穴を開けていることに注目してください。デザインアプリ(Photoshopなど)では、これは簡単にできます。しかし、Web上で同様のエフェクトを実装するとなると、次の理由で少し難しい場合があります。

  • カットアウトするためにJavaScriptが必要になるかもしれません。
  • 画像やテキストが含まれている可能性があります。
  • ボーダーやシャドウを追加するのは難しいかもしれません。

この記事ではさまざまな実装方法を取りあげ、その中でCSSまたはSVGを使用してどのようにカットアウトのエフェクトを実現するかを考えてみたいと思います。

アバター画像で見かけるカットアウト

これは、Facebookのメッセンジャーでの実例です。ユーザーのアバター画像には、ユーザーが現在オンラインであることを示す緑のバッジが付けられています。

Facebookのメッセンジャーでの実例

アバター画像で見かけるカットアウト

あなたはこう考えるかもしれません。グリーンのバッジにホワイトのボーダーを追加すれば簡単に実装できる、と。しかし、ここではそうではありません。ダークモードでは下記のように表示されます。

Facebookのメッセンジャーでの実例

ホワイトのボーダーを追加するだけでは、ダークモードに対応できていない

また、背景色が変更される場合(例えば、ホバーエフェクト)も失敗します。

Facebookのメッセンジャーでの実例

ホワイトのボーダーを追加するだけでは、背景色の変更に対応できていない

繰り返しになりますが、バッジのボーダーを背景に合わせて変更することは可能ですが、それは最善の解決策ではありません。

それでは、どのように実装すればよいのか解説します。

カットアウトの実装 1: clip-pathを使用する

このテクニックは、SVGとCSSを組み合わせて使用します。まず、パスを作成し、SVGとしてエクスポートする必要があります。これはデザインアプリで作業し、SVGとしてエクスポートします。私の場合はFigmaを使用しました。

デザインアプリで形のパスを作成

デザインアプリで形のパスを作成する

完了したら、path値をコピーし、相対単位に変換します。デフォルトでは、SVGのパスポイントは絶対値です。つまり、幅や高さが変わると伸びてしまいます。この問題を解決するためには、Clip-path converterを使用すると便利です。

SVGのパスを相対単位に変換

path値を相対単位に変換する

次に、そのパスを<clipPath>としてページのインラインSVGに記述します。

clipPathUnits属性の値objectBoundingBoxは、パス内の値がクリップパスが適用されている要素のバウンディングボックスに対して相対的であることを意味します。

カットエフェクトの完成

カットエフェクトの完成

うまく実装できました。
では、画像の内側にボーダーを加えたい場合はどうすればよいでしょうか? これはユーザーが明るい画像をアップロードした場合のフォールバックとして機能します。

画像の内側にボーダーを加える

画像の内側にボーダーを加える

残念ながら、<img>にインナーシャドウを追加することはできません。これを実装するには、HTML要素(spanなど)を追加するか、疑似要素を使用するかです。

ここでは疑似要素を使用してみます。

疑似要素でボーダーを実装

疑似要素でボーダーを実装

おっと! これは何が起きているのでしょうか。ボーダーが本来あるべきではない場所にも表示されています。疑似要素にもclip-pathを適用すれば、期待通りの効果が得られるはずです。

疑似要素でボーダーを実装

左: 修正前、右: 修正後

最後に、シャドウを与えてみましょう。CSSのdrop-shadowフィルターを使えば簡単に実装できます。素晴らしいのは、アバターの切り抜き形状に沿ってシャドウがつくことです。

シャドウを実装

左: box-shadowで実装したシャドウ
右: drop-shadowフィルターで実装したシャドウ

この実装方法の長所と短所です。

長所
  • クロスブラウザ対応、Chrome、Edge、Firefox、Safariのすべての主要バージョンで動作します。
  • 非常に基本的な例に適しています。ボーダーやシャドウを使って複雑にすることもできます。
短所
  • カットアウトを取り除くには、パスを変更する必要があります。これは異なるステータスを持つコンポーネントでは難しい場合があります。
  • デザインアプリで図形をマージする際の経験が必要です。

実際の動作は、下記でご確認ください。

See the Pen
Avatar - SVG clipPath
by Ahmad Shadeed (@shadeed)
on CodePen.

カットアウトの実装 2: CSSのマスク

CSSのマスクとグラデーションを組み合わせることで、カットアウトのエフェクトを実装できます。その方法を解説しましょう。

radial-gradientを使用することで、円形を描き、残りのスペースを別の色で塗りつぶすことができます。

カットアウトのエフェクトの仕組み

グラデーションで円形を描き、残りを別の色に塗りつぶす

次に、円の色を透明に変更し、要素にborder-radiusを追加します。

これで下記のようになります。

カットアウトのエフェクトの仕組み

border-radiusを追加

これをもとに、下記のようにCSSのマスクとして使用できます。

このテクニックでは、シェイプ内でマスクされるため、外側のborderを追加できます。ただし、インナーボーダー(別名: インセットシャドウ)については先ほどと同様に、別の要素(spanや疑似要素)を使用しない限り不可能です。

この実装方法の長所と短所です。

長所
  • クロスブラウザ対応ですが、Firefoxを除くすべてのブラウザにベンダープレフィックスが必要です。
短所
  • 他の例では制限があったり、複雑になることがあります。

実際の動作は、下記でご確認ください。

See the Pen
Avatar - CSS Mask
by Ahmad Shadeed (@shadeed)
on CodePen.

カットアウトの実装 3: SVGのマスク

まずは、SVGによるマスクの仕組みを解説します。
SVGでマスクを作成し、それをSVG自体のどこかに適用する必要があります。下記の例で考えてみましょう。

SVGのマスクの仕組み

SVGでマスクを作成

上記は単に画像を円形にマスクしているだけです。SVGでは、CSSのマスクとは(構文的に)異なります。上記のコードを分析してみましょう。

  1. まず、円を含む<mask>要素があります。
  2. マスクは<image>要素に適用されています。SVGでは、たとえば、グループの<g>のようなものにすることができます。

もう一つの小さな円をマスクに加えてみましょう。

SVGのマスクの仕組み

小さな円をマスクに加える

ここまでは簡単です。問題は、どうやってカットアウトのエフェクトにするかです。これを調べているときに、とても面白いことを知りましたので、紹介します。

マスクでは、白で塗りつぶされたオブジェクトは見せたい部分を表し、黒で塗りつぶされたオブジェクトは隠したい部分を表します。一方、黒く塗りつぶされたオブジェクトは、隠したい部分を表しています。面白いですよね。

それでは、小さな円の塗りつぶしを黒に変えてみましょう。

SVGのマスクの仕組み

小さな円の塗りつぶしを黒に変更

これがこのトリックのポイントです。非常に便利で、デベロッパーに多くのチャンスをもたらしてくれます。もしあなたがデザイナーなら、視覚的な説明も用意しました。

SVGのマスクの仕組み

SVGのマスクの仕組み

マスクアイテムが両方ともの場合は、2つの形状をマージするのと同じ結果(別名Union)になります。一方が白で他方が黒の場合は、一方の形状が他方の形状から差し引かれることになります。

次のステップでは、アバターにインナーのボーダーを追加します。SVGでは、これはとても簡単です。空の塗りつぶしを持つ<circle>と、rgba()で半透明のボーダーを使用します。

画像とボーダーはグループ内にあり、そのグループにはmask属性があることに注目してください。

この実装方法の長所と短所です。

長所
  • シンプルな実装。
  • 優れたクロスブラウザのサポート。
  • メンテナンスも優れています。
短所
  • 短所は、SVGを知らない人には少し難しいかもしれないということ以外、思いつきません。

私としては、このテクニックをお勧めします。
実は、この実装はFacebookにも採用されており、何を物語っているかというと、この実装はすべてのブラウザで問題なく動作し、必要のないときはマスクを無効にする方法もあるということです。

実際の動作は、下記でご確認ください。

See the Pen
Avatar - SVG Mask
by Ahmad Shadeed (@shadeed)
on CodePen.

アバター画像を重ねるカットアウト

ここまで解説してきた例とは異なるカットアウトのエフェクトがあります。
「Seen Avatars」と呼ばれるもので、Facebookのメッセンジャーのグループチャットでメッセージが見られたことを示すUIです。

Facebookのメッセンジャーでの実例

アバター画像を重ねるカットアウト

このUIを実装するには、2つの重なり合う円形を作成してから、一方を他方から引く必要があります。

アバター画像を重ねるカットアウトの仕組み

円形を2つ重ねて、重なった部分を引く

それでは、どのように実装すればよいのか解説します。

カットアウトの実装 1: clip-pathでマスク

これをclip-pathで実装してみると、楽しい経験になりました。パスをSVGとしてエクスポートし、その値を相対値に変換(前述のやり方と同じ)したところ、下記のようになりました。

clip-pathでマスクの仕組み

clip-pathでマスク

画像をborder-radius: 50%;にした場合、エクスポートされたパスは少し奇妙に見えます。残念ながら、これを実装するにはclip-pathは機能しません。

カットアウトの実装 2: CSSグラデーションでマスク

続いて、CSSグラデーションとマスクを組み合わせてみましょう。
前述と同様に、カットアウトを実装するために楕円を描く必要があります。

CSSグラデーションでマスクの仕組み

CSSグラデーションで楕円を作成

これで完成です。しかし、小さな問題が1つあります。よく注意してみると、楕円の端がギザギザになっていることに気づくと思います。

楕円の拡大図

楕円の端がギザギザになっている

このギザギザは、最初の色の停止値が次の色の開始値になっているために発生しています。つまり、最初の色は30pxで終わり、次の色は30pxから100%で始まります。これを回避するためには、次の色の値を30.5pxに変更します。

楕円の拡大図

2つ目の色の開始値を30.5pxに変更

また、CSSマスクで解決する方法として、楕円形の画像を使用する方法もあります。

CSSマスクで解決する方法

楕円形の画像を使用

しかし、これはご覧のとおり期待している結果ではありません。期待しているのはその逆で、楕円を除外して残りの部分を表示することです。どうすればよいと思いますか?
調べてみると、複数のマスクを追加して好きなように合成できるmask-compositeプロパティの存在を知りました。

linear-gradientに同じカラーストップを使用した塗りつぶしである別のマスクを追加しました。次にmask-compositeプロパティを追加し、excludeにするだけです。

注: mask-compositeはFirefoxで機能し、-webkit-mask-compositeはChromeとSafariで機能します。exclude値は、destination-out;と同等です。

CSSマスクで解決する方法

mask-compositeプロパティでマスクを実装

この実装方法の長所と短所です。

長所
  • クロスブラウザに対応していますが、Firefox以外のブラウザにはベンダープレフィックスが必要です。
短所
  • 他の例では制限または複雑になる可能性があります。

実際の動作は、下記でご確認ください。

See the Pen
Seen Avatars - SVG Mask
by Ahmad Shadeed (@shadeed)
on CodePen.

カットアウトの実装 3: circleでマスク

マスクに2つの<circle>要素を使用した実装を覚えていますか? 1つは白で、もう1つは黒です。今回のテクニックでも同じように使用します。

実装のポイントは、黒の<circle>cx属性に負の値を定義したことです。

マスクの仕組み

実装の仕組み

実際のプロジェクトでは、このコンポーネントに複数のバリエーションが必要になることがあります。ほとんどの場合、サイズの観点からです。

コンポーネントのバリエーション

このコンポーネントの複数のバリエーション

サイズのバリエーションを考慮すると、<circle>cx, cy, rの各値はCSS変数で管理することをお勧めします。下記は、アバターとマスクのサイズを管理するためのCSSです。

参考: CSS変数の優れた使い方、コンポーネントのバリエーションを実装するのに役立つ
参考: CSSの変数(カスタムプロパティ)便利な使い方を詳しく解説

上記のCSSを解説します。

  1. アバターのサイズを定義します。これは、幅と高さのプロパティに使用されます。
  2. サイズを使用して、cxcyの位置を定義します。
  3. 2つのアバター間の負のマージン値を定義するには、サイズを5.5で割って-1を掛ける必要があります。

cx値がどのように計算されるかを見てみましょう。

cx: calc(var(--size) / 4 * -1);

まだ分からない人もいるかもしれませんが(私もそうです)、cxcyの値は、円の中心から始まります。つまり、値の半分を使用すると、画像は完全に非表示になります。下記をご覧ください。

視覚化のために、紫は白いcircle(表示したい部分)を、輪郭があるものは黒いcircle(非表示にしたい部分)を表しています。

マスクの仕組み

実装の仕組み

実際の動作は、下記でご確認ください。

See the Pen
Mask Visualization
by Ahmad Shadeed (@shadeed)
on CodePen.

黒のcircleのcx値が0の場合は、すでに画像の半分が非表示になっています。これを微調整して、代わりにマイナスの値を使用できます。この値は、カットアウトする領域のサイズに応じて決めます。

アバター間の負のマージンについては、cx値を計算する方法と同じですが、少し大きくなります。これを正しく理解するには、検証が必要です。

この実装方法の長所と短所です。

長所
  • 優れたブラウザサポート。すべての主要なブラウザで安定して機能します。
  • CSS変数を使用することにより、全体を1つの変数だけで制御できます。
短所
  • SVGの経験が必要です。

実際の動作は、下記でご確認ください。

See the Pen
Seen Avatars - SVG Mask
by Ahmad Shadeed (@shadeed)
on CodePen.

Webサイトのヘッダで見かけるカットアウト

中央にロゴを配置したヘッダがあります。ここで実現したいのは、円形のロゴの後ろのエリアをカットアウトすることです。

Webサイトのヘッダで見かけるカットアウト

ロゴの後ろのエリアをカットアウト

まず最初に思いつくのは、白いボーダーを追加することではないでしょうか? それだけで実装できるように思うかもしれませんが、スクロールすると、ロゴの白枠が少し奇妙に見えてしまいます。

スクロールしたところ

白いボーダーを追加するだけでは、期待通りに機能しない

では、このカットアウトを実装する方法を解説します。

カットアウトの実装 1: CSSラジアルグラデーション

前述の例と同様に、放射状のグラデーション(radial-gradient)を使用して、ヘッダの中央にカットアウトのエリアを作成できます。

また、ロゴの位置はカットアウトのエリアと同じ位置に配置する必要があります。それを実現するために、position: relative;topを使用しました。

これで一応は機能しますが、完璧ではありません。
ロゴとカットアウトのエリアのサイズを動的にする必要があります。つまり、ビューポートのサイズに基づいてサイズを縮小したり拡大したりする必要があります。私が最初に考えたのは、CSSのclamp()関数です。
参考: CSSの比較関数が便利すぎる!min(), max(), clamp()の使い方を詳しく解説

--radiusはご想像のとおり、円の半径です。そしてロゴのサイズを半径の2倍にし、透明なエリアを少しオフセットしています。

順調に進んでいると思っていましたが、top: 10px;がうまく機能していないことに気がつきました。10pxでは、マスクとロゴの大きさに比例しないため、うまくいきません。

デスクトップとスマホでの表示

top: 10px;だとずれてしまう

ロゴのtopプロパティに動的な値を使用するにはどうしたらよいかを考え始めました。まず、分かっていることをリストアップしてみました。

  • ヘッダーの高さは100pxです。
  • カットアウトのエリアの中心は、y軸の70%に配置されます
  • --radius変数から取得できる円の半径。

下記をご覧ください。

実装の仕組み

分かっていることをリストアップ

動的なスペースを計算するために、次の式を考え出しました。

Distance = (Header Height * 70%) - Radius

数式をCSSに変換する方法は次のとおりです。calc()関数に感謝します。

この実装方法の長所と短所です。

長所
  • 優れたブラウザサポート。
短所
  • 特にありません。

実際の動作は、下記でご確認ください。

See the Pen
Website Header - CSS Mask
by Ahmad Shadeed (@shadeed)
on CodePen.

カットアウトの実装 2: SVGのマスク

このテクニックでは、前述したのと同じテクニックを使用しています。白く塗りつぶした矩形と、黒く塗りつぶした円形があります。これにより、カットアウトが作成されます。

実装の仕組み

実装の仕組み

SVGはヘッダのエリア全体を覆うように配置する必要があることに注意してください。

実際の動作は、下記でご確認ください。

See the Pen
Website Header - SVG Mask
by Ahmad Shadeed (@shadeed)
on CodePen.

終わりに

この記録を記事にするのはとても楽しい経験でした。私は、Webデベロッパーが特定の結果を達成するために多くの方法を持っていることが好きです。時々注意が必要ですが、問題ありません。

リソース

この記事があなたのお役に立てれば幸いです。
コメントや提案があれば、@shadeed9までお願いします。

sponsors

top of page

©2021 coliss