実装の仕組みが分かれば簡単!画像の一部を切り取るカットアウトを実装するCSSとSVGのテクニック
Post on:2021年8月17日
Webページやアプリで見かける、通知や注目の役割を担うカットアウト(画像の一部を切り取る)を実装するCSSとSVGのテクニックを紹介します。
画像に小さなバッジをつけたり、画像をグループ化する際に重ねたり、ヘッダやヒーローで画像を重ねたり、さまざまなUIで見かけます。
Thinking About The Cut-Out Effect: CSS or SVG?
by Ahmad Shadeed
下記は各ポイントを意訳したものです。
※当ブログでの翻訳記事は、元サイト様にライセンスを得て翻訳しています。
- カットアウトとは
- アバター画像で見かけるカットアウト
- カットアウトの実装 1: clip-pathを使用する
- カットアウトの実装 2: CSSのマスク
- カットアウトの実装 3: SVGのマスク
- アバター画像を重ねるカットアウト
- カットアウトの実装 1: clip-pathでマスク
- カットアウトの実装 2: CSSグラデーションでマスク
- カットアウトの実装 3: circleでマスク
- Webサイトのヘッダで見かけるカットアウト
- カットアウトの実装 1: CSSラジアルグラデーション
- カットアウトの実装 2: SVGのマスク
- 終わりに
カットアウトとは
私が最近取り組んだフロントエンドのプロジェクトで、コンポーネントの一つに興味深いカットアウトのエフェクトが含まれていました。このカットアウトの実装にはCSSやSVGなど複数の方法がありますが、それぞれに長所と短所があります。私はこの課題に対する解決策を探り、みなさんと共有しようと考えました。
この解説記事を読むにあたって、CSSとSVGの基本的な知識が必要ですが、なるべく詳細に解説しようと思いますので、おそらく大丈夫でしょう。
まずは、カットアウトのエフェクトがどういうものかを紹介します。形から一部分を切り取ったものです。
カットアウトのエフェクトとは、形の一部を切り取ったもの
矩形から円形を減算して、穴を開けていることに注目してください。デザインアプリ(Photoshopなど)では、これは簡単にできます。しかし、Web上で同様のエフェクトを実装するとなると、次の理由で少し難しい場合があります。
- カットアウトするためにJavaScriptが必要になるかもしれません。
- 画像やテキストが含まれている可能性があります。
- ボーダーやシャドウを追加するのは難しいかもしれません。
この記事ではさまざまな実装方法を取りあげ、その中でCSSまたはSVGを使用してどのようにカットアウトのエフェクトを実現するかを考えてみたいと思います。
アバター画像で見かけるカットアウト
これは、Facebookのメッセンジャーでの実例です。ユーザーのアバター画像には、ユーザーが現在オンラインであることを示す緑のバッジが付けられています。
アバター画像で見かけるカットアウト
あなたはこう考えるかもしれません。グリーンのバッジにホワイトのボーダーを追加すれば簡単に実装できる、と。しかし、ここではそうではありません。ダークモードでは下記のように表示されます。
ホワイトのボーダーを追加するだけでは、ダークモードに対応できていない
また、背景色が変更される場合(例えば、ホバーエフェクト)も失敗します。
ホワイトのボーダーを追加するだけでは、背景色の変更に対応できていない
繰り返しになりますが、バッジのボーダーを背景に合わせて変更することは可能ですが、それは最善の解決策ではありません。
それでは、どのように実装すればよいのか解説します。
カットアウトの実装 1: clip-pathを使用する
このテクニックは、SVGとCSSを組み合わせて使用します。まず、パスを作成し、SVGとしてエクスポートする必要があります。これはデザインアプリで作業し、SVGとしてエクスポートします。私の場合はFigmaを使用しました。
デザインアプリで形のパスを作成する
完了したら、path値をコピーし、相対単位に変換します。デフォルトでは、SVGのパスポイントは絶対値です。つまり、幅や高さが変わると伸びてしまいます。この問題を解決するためには、Clip-path converterを使用すると便利です。
path値を相対単位に変換する
次に、そのパスを<clipPath>としてページのインラインSVGに記述します。
1 2 3 |
<svg class="svg"> <clipPath id="circle" clipPathUnits="objectBoundingBox"><path d="M0.5,0 C0.776,0,1,0.224,1,0.5 C1,0.603,0.969,0.7,0.915,0.779 C0.897,0.767,0.876,0.76,0.853,0.76 C0.794,0.76,0.747,0.808,0.747,0.867 C0.747,0.888,0.753,0.908,0.764,0.925 C0.687,0.972,0.597,1,0.5,1 C0.224,1,0,0.776,0,0.5 C0,0.224,0.224,0,0.5,0"></path></clipPath> </svg> |
clipPathUnits属性の値objectBoundingBoxは、パス内の値がクリップパスが適用されている要素のバウンディングボックスに対して相対的であることを意味します。
1 2 3 |
.item { clip-path:url("#circle"); } |
カットエフェクトの完成
うまく実装できました。
では、画像の内側にボーダーを加えたい場合はどうすればよいでしょうか? これはユーザーが明るい画像をアップロードした場合のフォールバックとして機能します。
画像の内側にボーダーを加える
残念ながら、<img>にインナーシャドウを追加することはできません。これを実装するには、HTML要素(spanなど)を追加するか、疑似要素を使用するかです。
ここでは疑似要素を使用してみます。
1 2 3 4 5 6 7 8 9 10 11 |
.item:after { content: ""; position: absolute; left: 0; top: 0; width: 100%; height: 100%; border-radius: 50%; border: 1px solid; opacity: 0.2; } |
疑似要素でボーダーを実装
おっと! これは何が起きているのでしょうか。ボーダーが本来あるべきではない場所にも表示されています。疑似要素にもclip-pathを適用すれば、期待通りの効果が得られるはずです。
1 2 3 4 |
.item:after { /* other styles */ clip-path:url("#my-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を使用することで、円形を描き、残りのスペースを別の色で塗りつぶすことができます。
グラデーションで円形を描き、残りを別の色に塗りつぶす
1 2 3 |
.item { background-image: radial-gradient(circle 20px at calc(100% - 30px) calc(100% - 30px), yellow 30px, purple 0); } |
次に、円の色を透明に変更し、要素にborder-radiusを追加します。
1 2 3 4 |
.item { background-image: radial-gradient(circle 20px at calc(100% - 30px) calc(100% - 30px), transparent 30px, purple 0); border-radius: 50%; } |
これで下記のようになります。
border-radiusを追加
これをもとに、下記のようにCSSのマスクとして使用できます。
1 2 3 4 |
.item { -webkit-mask-image: radial-gradient(circle 20px at calc(100% - 30px) calc(100% - 30px), transparent 30px, purple 0); border-radius: 50%; } |
このテクニックでは、シェイプ内でマスクされるため、外側のborderを追加できます。ただし、インナーボーダー(別名: インセットシャドウ)については先ほどと同様に、別の要素(spanや疑似要素)を使用しない限り不可能です。
この実装方法の長所と短所です。
- 長所
-
- クロスブラウザ対応ですが、Firefoxを除くすべてのブラウザにベンダープレフィックスが必要です。
- 短所
-
- 他の例では制限があったり、複雑になることがあります。
実際の動作は、下記でご確認ください。
See the Pen
Avatar - CSS Mask by Ahmad Shadeed (@shadeed)
on CodePen.
カットアウトの実装 3: SVGのマスク
まずは、SVGによるマスクの仕組みを解説します。
SVGでマスクを作成し、それをSVG自体のどこかに適用する必要があります。下記の例で考えてみましょう。
SVGでマスクを作成
上記は単に画像を円形にマスクしているだけです。SVGでは、CSSのマスクとは(構文的に)異なります。上記のコードを分析してみましょう。
- まず、円を含む<mask>要素があります。
- マスクは<image>要素に適用されています。SVGでは、たとえば、グループの<g>のようなものにすることができます。
もう一つの小さな円をマスクに加えてみましょう。
小さな円をマスクに加える
ここまでは簡単です。問題は、どうやってカットアウトのエフェクトにするかです。これを調べているときに、とても面白いことを知りましたので、紹介します。
マスクでは、白で塗りつぶされたオブジェクトは見せたい部分を表し、黒で塗りつぶされたオブジェクトは隠したい部分を表します。一方、黒く塗りつぶされたオブジェクトは、隠したい部分を表しています。面白いですよね。
それでは、小さな円の塗りつぶしを黒に変えてみましょう。
小さな円の塗りつぶしを黒に変更
これがこのトリックのポイントです。非常に便利で、デベロッパーに多くのチャンスをもたらしてくれます。もしあなたがデザイナーなら、視覚的な説明も用意しました。
SVGのマスクの仕組み
マスクアイテムが両方とも白の場合は、2つの形状をマージするのと同じ結果(別名Union)になります。一方が白で他方が黒の場合は、一方の形状が他方の形状から差し引かれることになります。
次のステップでは、アバターにインナーのボーダーを追加します。SVGでは、これはとても簡単です。空の塗りつぶしを持つ<circle>と、rgba()で半透明のボーダーを使用します。
1 2 3 4 5 6 7 8 9 10 11 |
<svg role="none"> <mask id="circle"> <circle fill="white" cx="100" cy="100" r="100"></circle> <circle fill="black" cx="86%" cy="86%" r="18"></circle> </mask> <g mask="url(#circle)"> <image x="0" y="0" height="100%" width="100%" xlink:href="shadeed.jpg" ></image> <circle fill="none" cx="100" cy="100" r="100" stroke="rgba(0,0,0,0.1)" stroke-width="2"></circle> </g> </svg> |
画像とボーダーはグループ内にあり、そのグループにはmask属性があることに注目してください。
この実装方法の長所と短所です。
- 長所
-
- シンプルな実装。
- 優れたクロスブラウザのサポート。
- メンテナンスも優れています。
- 短所
-
- 短所は、SVGを知らない人には少し難しいかもしれないということ以外、思いつきません。
私としては、このテクニックをお勧めします。
実は、この実装はFacebookにも採用されており、何を物語っているかというと、この実装はすべてのブラウザで問題なく動作し、必要のないときはマスクを無効にする方法もあるということです。
実際の動作は、下記でご確認ください。
See the Pen
Avatar - SVG Mask by Ahmad Shadeed (@shadeed)
on CodePen.
アバター画像を重ねるカットアウト
ここまで解説してきた例とは異なるカットアウトのエフェクトがあります。
「Seen Avatars」と呼ばれるもので、Facebookのメッセンジャーのグループチャットでメッセージが見られたことを示すUIです。
アバター画像を重ねるカットアウト
このUIを実装するには、2つの重なり合う円形を作成してから、一方を他方から引く必要があります。
円形を2つ重ねて、重なった部分を引く
それでは、どのように実装すればよいのか解説します。
カットアウトの実装 1: clip-pathでマスク
これをclip-pathで実装してみると、楽しい経験になりました。パスをSVGとしてエクスポートし、その値を相対値に変換(前述のやり方と同じ)したところ、下記のようになりました。
clip-pathでマスク
画像をborder-radius: 50%;にした場合、エクスポートされたパスは少し奇妙に見えます。残念ながら、これを実装するにはclip-pathは機能しません。
カットアウトの実装 2: CSSグラデーションでマスク
続いて、CSSグラデーションとマスクを組み合わせてみましょう。
前述と同様に、カットアウトを実装するために楕円を描く必要があります。
1 2 3 |
.item { -webkit-mask-image: radial-gradient(ellipse 54px 135px at 11px center, #0000 30px, #000 0); } |
CSSグラデーションで楕円を作成
これで完成です。しかし、小さな問題が1つあります。よく注意してみると、楕円の端がギザギザになっていることに気づくと思います。
楕円の端がギザギザになっている
このギザギザは、最初の色の停止値が次の色の開始値になっているために発生しています。つまり、最初の色は30pxで終わり、次の色は30pxから100%で始まります。これを回避するためには、次の色の値を30.5pxに変更します。
1 2 3 |
.item { -webkit-mask-image: radial-gradient(ellipse 54px 135px at 11px center, #0000 30px, #000 30.5px); } |
2つ目の色の開始値を30.5pxに変更
また、CSSマスクで解決する方法として、楕円形の画像を使用する方法もあります。
1 2 3 4 5 6 |
.item { -webkit-mask-image: url(oval.svg); -webkit-mask-repeat: no-repeat; -webkit-mask-position: -26px 54%; -webkit-mask-size: 80px 140px; } |
楕円形の画像を使用
しかし、これはご覧のとおり期待している結果ではありません。期待しているのはその逆で、楕円を除外して残りの部分を表示することです。どうすればよいと思いますか?
調べてみると、複数のマスクを追加して好きなように合成できるmask-compositeプロパティの存在を知りました。
linear-gradientに同じカラーストップを使用した塗りつぶしである別のマスクを追加しました。次にmask-compositeプロパティを追加し、excludeにするだけです。
1 2 3 4 5 6 7 8 |
.item { -webkit-mask-image: url(oval.svg), linear-gradient(#000, #000); -webkit-mask-repeat: no-repeat; -webkit-mask-position: -26px 54%, 100% 100%;; -webkit-mask-size: 80px 140px, 100% 100%; mask-composite: exclude; -webkit-mask-composite: destination-out; } |
注: mask-compositeはFirefoxで機能し、-webkit-mask-compositeはChromeとSafariで機能します。exclude値は、destination-out;と同等です。
mask-compositeプロパティでマスクを実装
この実装方法の長所と短所です。
- 長所
-
- クロスブラウザに対応していますが、Firefox以外のブラウザにはベンダープレフィックスが必要です。
- 短所
-
- 他の例では制限または複雑になる可能性があります。
実際の動作は、下記でご確認ください。
See the Pen
Seen Avatars - SVG Mask by Ahmad Shadeed (@shadeed)
on CodePen.
カットアウトの実装 3: circleでマスク
マスクに2つの<circle>要素を使用した実装を覚えていますか? 1つは白で、もう1つは黒です。今回のテクニックでも同じように使用します。
1 2 3 4 5 6 7 8 9 10 |
<svg role="none" class="avatar-wrapper"> <mask id="cut"> <circle cx="50" cy="50" r="50" fill="white"></circle> <circle fill="black" cx="-30" cy="50" r="50"></circle> </mask> <g mask="url(#cut)"> <image x="0" y="0" height="100%" width="100%" xlink:href="shadeed.jpg"></image> <circle fill="none" stroke="rgba(0,0,0,0.1)" stroke-width="2"></circle> </g> </svg> |
実装のポイントは、黒の<circle>のcx属性に負の値を定義したことです。
実装の仕組み
実際のプロジェクトでは、このコンポーネントに複数のバリエーションが必要になることがあります。ほとんどの場合、サイズの観点からです。
このコンポーネントの複数のバリエーション
サイズのバリエーションを考慮すると、<circle>のcx, cy, rの各値はCSS変数で管理することをお勧めします。下記は、アバターとマスクのサイズを管理するためのCSSです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
.avatar { --size: 100px; /* [1] */ width: var(--size); height: var(--size); } /* [2] */ .avatar-circle { cx: calc(var(--size) / 4 * -1); cy: calc(var(--size) / 2); r: calc(var(--size) / 2); } /* [3] */ .avatar-item { margin-left: calc(var(--size) / 5.5 * -1); } |
参考: CSS変数の優れた使い方、コンポーネントのバリエーションを実装するのに役立つ
参考: CSSの変数(カスタムプロパティ)便利な使い方を詳しく解説
上記のCSSを解説します。
- アバターのサイズを定義します。これは、幅と高さのプロパティに使用されます。
- サイズを使用して、cxとcyの位置を定義します。
- 2つのアバター間の負のマージン値を定義するには、サイズを5.5で割って-1を掛ける必要があります。
cx値がどのように計算されるかを見てみましょう。
cx: calc(var(--size) / 4 * -1);
まだ分からない人もいるかもしれませんが(私もそうです)、cxとcyの値は、円の中心から始まります。つまり、値の半分を使用すると、画像は完全に非表示になります。下記をご覧ください。
視覚化のために、紫は白い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サイトのヘッダで見かけるカットアウト
中央にロゴを配置したヘッダがあります。ここで実現したいのは、円形のロゴの後ろのエリアをカットアウトすることです。
ロゴの後ろのエリアをカットアウト
まず最初に思いつくのは、白いボーダーを追加することではないでしょうか? それだけで実装できるように思うかもしれませんが、スクロールすると、ロゴの白枠が少し奇妙に見えてしまいます。
白いボーダーを追加するだけでは、期待通りに機能しない
では、このカットアウトを実装する方法を解説します。
カットアウトの実装 1: CSSラジアルグラデーション
前述の例と同様に、放射状のグラデーション(radial-gradient)を使用して、ヘッダの中央にカットアウトのエリアを作成できます。
1 2 3 |
.site-header { background: radial-gradient(circle at 50% 70%, rgba(0, 0, 0, 0) 58px, #95a57f 58px, #95a57f 100%); } |
また、ロゴの位置はカットアウトのエリアと同じ位置に配置する必要があります。それを実現するために、position: relative;とtopを使用しました。
1 2 3 4 |
.logo { position: relative; top: 10px; } |
これで一応は機能しますが、完璧ではありません。
ロゴとカットアウトのエリアのサイズを動的にする必要があります。つまり、ビューポートのサイズに基づいてサイズを縮小したり拡大したりする必要があります。私が最初に考えたのは、CSSのclamp()関数です。
参考: CSSの比較関数が便利すぎる!min(), max(), clamp()の使い方を詳しく解説
1 2 3 4 |
:root { --radius: clamp(48px, 4vw, 60px); --logo-size: calc(calc(var(--radius) * 2) - 8px); } |
--radiusはご想像のとおり、円の半径です。そしてロゴのサイズを半径の2倍にし、透明なエリアを少しオフセットしています。
順調に進んでいると思っていましたが、top: 10px;がうまく機能していないことに気がつきました。10pxでは、マスクとロゴの大きさに比例しないため、うまくいきません。
top: 10px;だとずれてしまう
ロゴのtopプロパティに動的な値を使用するにはどうしたらよいかを考え始めました。まず、分かっていることをリストアップしてみました。
- ヘッダーの高さは100pxです。
- カットアウトのエリアの中心は、y軸の70%に配置されます
- --radius変数から取得できる円の半径。
下記をご覧ください。
分かっていることをリストアップ
動的なスペースを計算するために、次の式を考え出しました。
Distance = (Header Height * 70%) - Radius
数式をCSSに変換する方法は次のとおりです。calc()関数に感謝します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
:root { --header-height: 100px; --radius: clamp(48px, 4vw, 60px); --logo-size: calc(calc(var(--radius) * 2) - 8px); } .logo { display: block; position: relative; top: calc(var(--header-height) * 0.7 - var(--radius) + 2px); width: var(--logo-size); margin-left: auto; margin-right: auto; } .site-header { background: radial-gradient( circle at 50% 70%, rgba(0, 0, 0, 0) var(--radius), #95a57f var(--radius), #95a57f 100% ); } |
この実装方法の長所と短所です。
- 長所
-
- 優れたブラウザサポート。
- 短所
-
- 特にありません。
実際の動作は、下記でご確認ください。
See the Pen
Website Header - CSS Mask by Ahmad Shadeed (@shadeed)
on CodePen.
カットアウトの実装 2: SVGのマスク
このテクニックでは、前述したのと同じテクニックを使用しています。白く塗りつぶした矩形と、黒く塗りつぶした円形があります。これにより、カットアウトが作成されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<header class="site-header"> <img src="assets/logo.svg" alt="" /> <svg role="none" height="80"> <defs> <mask id="hole"> <rect width="100%" height="100%" fill="white" /> <circle cx="50%" cy="80%" r="58" fill="black"></circle> </mask> </defs> <rect width="100%" height="100%" mask="url(#hole)" /> </svg> </header> |
実装の仕組み
SVGはヘッダのエリア全体を覆うように配置する必要があることに注意してください。
1 2 3 4 5 6 7 8 9 10 11 |
.site-header { position: relative; } .site-header svg { position: absolute; left: 0; top: 0; width: 100%; height: 100%; } |
実際の動作は、下記でご確認ください。
See the Pen
Website Header - SVG Mask by Ahmad Shadeed (@shadeed)
on CodePen.
終わりに
この記録を記事にするのはとても楽しい経験でした。私は、Webデベロッパーが特定の結果を達成するために多くの方法を持っていることが好きです。時々注意が必要ですが、問題ありません。
リソース
この記事があなたのお役に立てれば幸いです。
コメントや提案があれば、@shadeed9までお願いします。
sponsors