とほほえんどの術

とほほな結末エンドでよく使われるエフェクト「アイリスアウト(アイリスワイプ)」。アイリスとはレンズの絞りのことで、カメラレンズの絞りを閉じていく様子を模した場面転換の手法です。そんなエフェクトをCSSで表現する手段を3つ記します:)

長押ししたらこりごりだよー

仕組み

ここでは、場面転換からは少しスケールを落として、下図の「とほほなパンちゃん」画像にカーソルを合わせる(かタップするかする)と、パンちゃんの顔の部分が丸くフォーカスされるっていうサンプルを通して、アイリスアウトを作る3つの手段の、仕組みとコツを記してゆきます。

ぽぺんよ落とされたパンちゃん
とほほなパンちゃん

HTMLは下ソースコードのように、figure要素の中に、img要素と、アイリス用のdiv要素(以下、アイリス要素と呼ぶ)を入れます。アイリス要素の中に「とほほー!」という台詞せりふを入れておいて、アイリスが閉じた時にアイリスの上に重なるっていう仕組みです:)

<figure class="irisout">
	<img src="tohoho_pan.svg" alt="とほほなパンちゃん" width="800" height="800" loading="lazy" class="pan">
	<div class="iris"><img src="tohoho.svg" alt="とほほー!" width="485" height="266" loading="lazy" class="serif"></div>
</figure>

透明な丸に落ちる影

要素の中央を丸くくり抜くって考えると難しそうだけれど、丸型の周りを塗りつぶすと考えると分かり良いかもしれません。
アイリスとなるbox-shadowプロパティを使って作ります。box-shadowプロパティは、透明な要素にも影を落とすことができるってトコがポイント;)

透明な丸に落ちる影という仕組み

アイリス要素自体はfigure要素を覆うように絶対配置しておいて、アイリス面は::after擬似要素で作ります。

.irisout {
	--size: min(400vw / 4.8, 400px);
	position: relative;
	overflow: hidden;
	width: var(--size);
	height: calc(var(--size) / 1.5);
}
.irisout .iris {
	position: absolute;
	inset: 0;
}

擬似要素で、画像の対角線と同じ直径の正円を作って、「のどかな空色の影」を落とします。影をボカさずめいっぱい広げれば、中央に丸い穴が空いたベタ塗りの出来上がり;D

ここでは、アイリス要素のサイズを--sizeというCSS変数で管理してます。親要素のサイズを決め打ちしておくことで、内部の要素サイズを調整しやすくなります。

要素の比率3:2になるよう決め打ちしてるので、横幅の1.2倍くらいが、ちょうど対角線の長さになるはずです。ここではsqrt()という、平方根を返す指数関数ってのを使って、より正確に対角線の長さを指定しています。

sqrt()について詳しくは下記ページを参照のこと。
sqrt() - CSS: Cascading Style Sheets | MDN

影の広さもCSS変数を利用すれば、画像を覆い隠せるサイズの影にできますね;)

.irisout .iris::after {
	content: "";
	display: block;
	aspect-ratio: 1 / 1;
	width: calc(var(--size) * sqrt(13 / 9));
	height: auto;
	border-radius: 50%;
	box-shadow: 0 0 0 var(--size) #86c0de;
	transition: width 1s cubic-bezier(.7,0,.3,1);
}

タップした時に、アイリス要素の横幅を小さくしてゆけば、アイリスが絞られていく様子を表現できます。ここにもCSS変数を利用して、画像の幅の36%の幅(パンちゃんの顔が入るくらいのサイズ感)にしています。

.irisout:hover .iris::after {
	width: calc(var(--size) * .36);
}

要素の中央に配置されるようにしたいので、アイリス要素をグリッドにして、place-contentプロパティの値をcenterとしておけば、擬似要素は、アイリス要素をハミ出しても常に中央に配置されるようになります;)

.irisout .iris {
	display: grid;
	place-content: center;
	⋮
}
.irisout .iris::after {
	content: "";
	aspect-ratio: 1 / 1;
	⋮
}

アイリスの位置は、top/leftプロパティで調整します。ここでは少し横着おうちゃくしてinsetプロパティで一括指定しています。

.irisout .iris::after {
	content: "";
	position: relative;
	inset: 0 auto auto 0;
	⋮
	transition: width 1s cubic-bezier(.7,0,.3,1), inset 1s cubic-bezier(.7,0,.3,1);
}
.irisout:hover .iris::after {
	width: calc(var(--size) * .36);
	inset: -16% auto auto -42%;
}

あとは台詞を、タップしてから表示されるよう指定したら、出来上がり:D

.irisout.shift .serif {
	opacity: 0;
	transition: opacity .3s;
}
.irisout.shift:hover .serif {
	opacity: 1;
	transition-delay: .7s;
}

画像をクリッピングする

要素の穴から画像をのぞかせるのではなくて、画像自体を丸くくり抜いちゃう方法です。台詞は画像の下に隠れるので、事前に非表示にしておかなくても良いのが良いところ。

画像をクリッピングする仕組み

clip-pathプロパティの値をcircle()関数を使って指定すれば、要素を丸くクリッピングすることができます。circle()の括弧の中に指定する値は円の半径なので、前章でアイリス要素に指定したサイズの半分対角線の長さの半分を指定すれば、アイリス要素の外接円でクリッピングすることができます。(下サンプルひとつめ)
タップした時のサイズも、前章に倣って、その半分のサイズを指定しますcalc(var(--size) * .36)だったのでcalc(var(--size) * .18)

.irisout .pan {
	clip-path: circle(calc(var(--size) * sqrt(13 / 9) / 2));
	transition: clip-path 1s cubic-bezier(.7,0,.3,1);
}
.irisout:hover .pan {
	clip-path: circle(calc(var(--size) * .18));
}

circle()の値にcalc(100% / sqrt(2))という値を指定しても、要素の外接円を適用することができます。こちらの方が汎用性があるかも。(下サンプルふたつめ)

.irisout .pan {
	clip-path: circle(calc(100% / sqrt(2)));
	transition: clip-path 1s cubic-bezier(.7,0,.3,1);
}

circle()は、円の中心点at X Yという風に座標でもって指定することができます。ここでは、タップする前は50% 50%、タップしたら34.7% 41.4%となるよう、半径の指定の後に追記します。

.irisout .pan {
	clip-path: circle(calc(100% / sqrt(2)) at 50% 50%);
	transition: clip-path 1s cubic-bezier(.7,0,.3,1);
}
.irisout:hover .pan {
	clip-path: circle(calc(var(--size) * .18) at 34.7% 41.4%)
}

できあがり:D

マスクで空ける穴

要素に丸く穴を空けて画像を覗かせる方法。ちゃんとアイリスで画像を隠す構造だし、アイリス要素内の台詞も一緒にマスクされるから、box-shadowとclip-pathの良いとこ取りみたいな仕組みです。

マスクで穴を空ける仕組み

maskプロパティのマスクの型となる画像を、radial-gradient()関数を使って指定します。透明なところが非表示になって、不透明なところが表示されるので、中央が透明で外側が不透明な円を描けば良いですね:D
#0000(透明な黒)から外へ向かって#000(黒)へグラデーションさせつつ、色経由点(始点と終点)を同じ値にすることでパキッと切り替わるようにします。
色経由点は、要素の中央から要素の四隅までの距離、つまり対角線の半分を指定すれば良いので、clip-pathプロパティの時と同じサイズにすれば外接円が描けるはずです。
画像の表示位置center画像サイズは縦も横もアイリス要素の横幅と同じ大きさvar(--size)にしておきます。

.irisout .iris {
	position: absolute;
	inset: 0;
	background: #86c0def4;
	mask: center / var(--size) var(--size) no-repeat;
	mask-image: radial-gradient(#0000 calc(var(--size) * sqrt(13 / 9) / 2), #000 calc(var(--size) * sqrt(13 / 9) / 2));
	transition: mask-image 1s cubic-bezier(.7,0,.3,1);
}

タップした時には、グラデーションの色経由点を要素の内側へしぼめることで、円形を小さくします。ここも、clip-pathプロパティの時と同じサイズを指定しています。

.irisout:hover .iris {
	mask-image: radial-gradient(#0000 calc(var(--size) * .18), #000 calc(var(--size) * .18));
}

けどアニメーションしません。radial-gradient()内の値は、アニメーションがサポートされていないんですね:((2024.11現在)

しかし、変化させたい値をカスタムプロパティとして定義する事で、アニメーションさせることができます:D@propertyルールで、プロパティを定義します。定義しなきゃならない決め事は以下の3つ。

syntax
どのような値を受け入れるか。
inherits
指定した値を子要素に継承するか否か。
initial-value
プロパティの初期値。

ここでは、以下のように指定しています。

@property --radius {
	syntax: "<length-percentage>";
	inherits: false;
	initial-value: 0%;
}

syntaxに、<length><number><color>といったデータ型を指定したプロパティは、アニメーション可能なプロパティとなります。<length-percentage>数値も割合も受け入れるというデータ型で、アニメーションも可能になります。

@propertyルールについて詳しくは下記ページを参照のこと。
@property - CSS: カスケーディングスタイルシート | MDN

定義した--radiusというカスタムプロパティを、radial-gradient()に指定する色経由点の値として使います。 それからtransition--radiusで指定し直して、タップした時は--radiusの値を更新します(下サンプルひとつめ)

.irisout .iris {
	--radius: calc(var(--size) * sqrt(13 / 9) / 2);
	⋮
	mask-image: radial-gradient(#0000 var(--radius), #000 var(--radius));
	transition: --radius 1s cubic-bezier(.7,0,.3,1);
}
.irisout:hover .iris {
	--radius: calc(var(--size) * .18);
}

radial-gradient()は、色経由点の前に、circle at X Yというふうに指定すれば、その要素内の座標でもって円の中心点を指定することができます。
ここでは、タップした後の中心点を34.7% 44.3%と指定しています(下サンプルふたつめ)

.irisout .iris {
	mask-image: radial-gradient(circle at 34.7% 44.3%, #0000 var(--radius), #000 var(--radius));
}

そうです、radial-gradient()がアニメーションをサポートしていないので、中心点ももちろんアニメーションしません。そんなわけで、新たに--x--yというカスタムプロパティも定義します…!

.irisout .iris {
	⋮
	mask-image: radial-gradient(circle at var(--x) var(--y), #0000 var(--radius), #000 var(--radius));
	transition: --radius 1s cubic-bezier(.7,0,.3,1), --x 1s cubic-bezier(.7,0,.3,1), --y 1s cubic-bezier(.7,0,.3,1);
}
@property --x {
	syntax: "<length-percentage>";
	inherits: false;
	initial-value: 50%;
}
@property --y {
	syntax: "<length-percentage>";
	inherits: false;
	initial-value: 50%;
}
.irisout:hover .iris {
	--x: 34.7%;
	--y: 44.3%;
	--radius: calc(var(--size) * .18);
}

出来ました!

あとがき

maskプロパティを使った方法は使い勝手が良いけれど、古めのiPhoneやSafariでは@propertyをサポートしていないことがあるので、box-shadowプロパティを使った方法かclip-pathプロパティを使った方法でのフォローが必要かもしれません。心配な時はJavaScriptを使って、@propertyをサポートしているかどうかを判別した上で実装すると良いです:)

if ('registerProperty' in CSS) {
	document.body.classList.add('is-supported');
} else {
	document.body.classList.add('not-supported');
}

最後まで読んでいただきありがとうございました!
以上、「とほほえんどの術」でした。