HTML/CSSではいとも簡単に実現できてしまう、こんな表示。
もしもSVGしか使えない(style
属性も禁止!な)世界に転生したとしたら、この表示をどう実現しますか?
このチュートリアルで、そんな過酷な世界を一緒に生きてみましょう。
持ち物はなにも要りません!
SVG Filterを触ったことがなくても、きっと大丈夫です。
影のベースとなるぼかしを作る
影とは結局、要素の内側か外側に加えた、特定の色のぼかし領域。
そこで、まずはぼかしの作り方を考える。
<feGaussianBlur />
は、ガウスぼかしを実現するためのフィルタである。
<feGaussianBlur/>
では、stdDeviation
属性でぼかしの強さ(標準偏差)を指定できる。
冒頭のCSSのbox-shadow
では、ぼかし半径の値を8px
に設定していたが、ここでのstdDeviation
にはその半分の値4
を指定している。
ガウスぼかしでは、ぼかし効果のほとんど(95%)が、元のピクセルから半径stdDeviation * 2px
以内に広がる。
つまり、stdDeviation
属性には、ぼかしたい半径の半分の値を指定すればよい。
ちなみにこのフィルタだけなら、CSSのfilter: blur(4px);
で事足りる。
SourceGraphic
とSourceAlpha
<filter />
は、入力画像を変換した上で出力するものである。
<filter />
の最初の子要素が受け取る入力画像は、このフィルタを指定した要素の描画結果である。
つまり、以下のコードでいえば、<feGaussianBlur />
が受け取る入力画像は、filter="url(#blur)"
を指定した<rect />
要素の描画結果だ。
<feXXX />
では、入力画像をin
属性で指定することができる。
フィルタを指定した要素の描画結果は、SourceGraphic
という名前で参照できる。
つまり、先ほどのコードの<feGaussianBlur />
にin="SourceGraphic"
と指定しても、表示は変わらない。
一方で、in="SourceAlpha"
と指定すると、次のような表示になる。
SourceAlpha
は、filter
プロパティを指定した要素の描画部分の影と考えるとよい。
仮に、rect
要素にfill-opacity
属性を指定することで、描画部分を半透明にすると、SourceAlpha
の結果である影も薄くなる。
要素が半透明になると、要素が完全に光を遮ることはなく、わずかに光が透過する…とイメージできる。
まずは外側の影を作ってみる
SourceAlpha
は、要素自身が落とす影だと捉えられる。
つまり、inset
を指定しないbox-shadow
やdrop-shadow
の影は、SourceAlpha
をそのまま少しずらしたり、ぼかしたりすることで実現できる。
まずは、inset
もoffset
も指定しない真っ黒な影をbox-shadow
を再現してみよう。
以下はHTMLとCSSで実現したもの。
上の例は、周囲をぼかした黒い四角形の上に、元の四角形を乗せることで再現できる。
ぼかし半径の分だけ、黒い四角形がはみ出して見えるので、その部分がbox-shadow
を模倣するのだ。
さて、前の節で見たように、入力画像をSourceAlpha
として、<feGaussianBlur />
を使えば、影用の四角形を作ることができる。
result
属性によって、フィルタの出力に名前をつけることができる。
上の例では、影用の四角形にSHADOW
という名前をつけている。
この影用の四角形(SHADOW
)の上に、元の四角形(SourceGraphic
)を重ねたい。
このようなニーズを叶えるのが、<feMerge />
である。
<feMerge />
を使うことで、複数のフィルタの出力画像を順番に重ねて出力することができる。
重ねたい出力画像は、<feMerge />
の子として置いた<feMergeNode>
のin
属性で指定する。
そして、<feMergeNode>
を並べた順に上へと重なっていく。
ちなみに、上のコードは、次のように書いてもよい。
<filter />
内に<feXXX />
を並べていくことは、メソッドチェーンやパイプラインにたとえられる。
<filter />
の子として並べられた各<feXXX />
要素は、その直前の<feXXX />
の出力を入力として受け取る。
<feMergeNode />
にin
属性がない場合は、親である<feMerge />
への入力がそのまま渡される。
今回の場合、<feMerge />
の直前に<feGaussianBlur />
を置いているので、result="SHADOW"
とin="SHADOW"
で明示しなくても、<feGaussianBlur />
の出力がそのまま渡されるのである。
影をずらす
inset
キーワードを加えるのは最後の工程として、次はoffset
の指定を考えてみる。
この節で目指すのは、次のような表示だ。以下はHTML/CSSで描画したもの。
<feOffset />
を使うことで、入力画像をずらすことができる。
x方向の移動量はdx
属性で、y方向の移動量はdy
属性で指定する。
<feOffset />
を<feGaussianBlur />
の直後に置くことで、<feOffset />
への入力画像が<feGaussianBlur />
になるので、ぼかした影をずらすことになる。
そしてそのずらした結果は最初の<feMergeNode />
にそのまま渡され、元の<rect />
にずらした影を合成した結果が得られる。
影に色をつける
真っ黒な影を望む場合は、このままでも十分だろう。
しかし、そうでない場合が多いと思うので、影の色をカスタマイズする方法を探っていく。
以下はHTML/CSSで描画した、目標とする表示。
まず、フィルタ内で色を使うためには、<feFlood />
を使用する。
とはいえ、<feFlood />
は、フィルタがかかる領域を超えて、全面を指定した色で塗りつぶすものなので、これだけでは<svg>
要素全体に色が載ってしまう。
特定の領域にのみ色を載せたい場合は、うまいことマスクしてあげる必要がある。
<feComposite />
を使えば、in
属性とin2
属性で指定した2つの画像を、各部分でどちらの表示を採用するかを選択しながら合成することができる。
どちらの表示を採用するかの選択は、<feComposite />
のoperator
属性に指定した合成演算によって行われる。
中でも、operator="in"
と指定すると、
in2
画像の不透明な部分ではin
画像を採用
in2
画像の透明な部分ではin2
画像を採用
という合成が行われる。
今回の場合は、in2
に影用の四角形、in
に<feFlood />
によって塗られた無限平面を指定すればよい。
- 影用の四角形の内側は
<feFlood />
で指定した色で塗りつぶされる
- 影用の四角形の外側では、
<feFlood />
の色は破棄される
という挙動になり、次のような表示が得られる。
<feComposite />
のin
属性を省略した場合、その直前の<feXXX />
の出力がin
属性として渡される。
いよいよinset
の再現へ
これで、inset
を指定しなかった場合のbox-shadow
が完成した。
いよいよ、inset
と同じ効果を持つフィルタを加えていくフェーズである。
inset
を再現する方法はいくつかあるので、それぞれの考え方を鑑賞してみよう。
方法1: xor
とin
の合わせ技
<feComposite />
をうまく使えば、影用の四角形が<rect />
の内側でのみ見えるようにマスクすることができる。
<feComposite />
での演算を考える際には、in
属性の入力画像とin2
属性の入力画像が、どのように重なっているかを考えるとよい。
以下の図では、グレーに塗られている部分が影となる四角形、赤い枠線内が元の<rect />
部分(SourceAlpha
の領域)を表している。
<feComposite />
のin
属性を影の四角形、in2
属性をSourceAlpha
としよう。
このとき、operator="xor"
と指定すると、in
画像とin2
画像の重ならない部分のみが残る。
外側の影と内側の影が両方とも残る結果になっている。
そこで、さらに<feComposite />
を使って、外側の影を消してしまおう。
operator="in"
と指定すると、in
画像とin2
画像の重なる部分のみが残るので、元の<rect />
(SourceAlpha
)の範囲外の部分は消えることになる。
これで目的の位置の影が得られた🎉
完成形はこちら。
ちなみに、外側に影をつけたときとは<feMergeNode />
の順序が異なっている。
内側に影をつける場合は、<feMergeNode in="SourceGraphic" />
を先に置くことで、影の方が上に重なるようにしている。
そうしなければ、要素の内側の影は要素の塗りに隠れてしまうからだ。
方法2: arithmetic
で差集合をつくる
方法1では、<feComposite />
を2回繰り返して使うことになり、あまりスマートとはいえない。
<feComposite />
のoperator
には、arithmetic
という値を指定することもできる。
この値を指定すると、色の選び方をより細かいパラメータk1
、k2
、k3
、k4
属性で制御できるようになる。
具体的には、各ピクセルで次のような計算が行われる。
i1
はin
画像の計算対象ピクセルの色の値、i2
はin2
画像の計算対象ピクセルの色の値を表している。
k1
、k2
、k3
、k4
属性は、指定しなければデフォルト値として0
が使われる。
仮に、k2 = -1
、k3 = 1
とし、それ以外は0
のままにしておくと、おもしろい式が得られる。
つまり、in2
画像とin
画像の差集合をとる演算になる。
集合in2 - in
は、in2
の領域のうち、in
が重ならない部分を表す。
下の図でいえば、赤枠内のうち、影が乗っていない白い部分だけが残ることになる。
つまり、こうなる。(差集合はあくまでもイメージで、実際にはアルファ値の引き算を行っているため、ぼかしにより半透明になっている部分は、その透明度に応じて残ることになる。)
この演算を利用した完成形はこちら。
方法3: 内側に影を落とすための壁を作る(Safari・iOS非対応)
要素の外側の影は、要素自身が落とす影である。
しかし、inset
指定で実現される要素の内側の影は、その要素を取り囲む壁が落とす影だと考えられる。
そこで、まずは要素の周りに壁をつくりたい。
SourceAlpha
では、塗られている部分は真っ黒に設定されてしまうが、塗りの外側の部分は透明なままである。
この透明部分と黒い部分を反転させることができれば、塗りの外側の部分が黒く塗りつぶされるので、これが要素を取り囲む壁となる。
入力画像の色を加工する<feComponentTransfer />
をうまく使うことで、SourceAlpha
の反転を実現できる。
まずは<feComponentTransfer />
の使い方の一例を見てみよう。
<feComponentTransfer />
は、子要素として<feFuncR />
, <feFuncG />
, <feFuncB />
, <feFuncA />
を並べることで、入力画像の色のR, G, B, Aそれぞれを個別に操作して色を加工するフィルタ。
<feFuncX />
では、type
属性で操作の種類を指定する。
type="table"
と指定すると、tableValues
属性で指定した値に従って、色の範囲を変換することができる。
上の例では、フィルタを適用した右側の<rect />
だけ、半透明になっている。
半透明にする指示を表しているのが、<feFuncA />
の部分だ。
<feFuncA />
のtableValues
属性には、0 0.5
と指定している。
本来、RGBAのA値(アルファチャネル、不透明度)は、0
から1
の値を取りうるが、それを0
から0.5
の範囲に圧縮しているのだ。
結果、元の不透明度が1
の場合は0.5
に、0.5
の場合は0.25
に…と、不透明度が半分になるように変換されている。
さて、透明な部分と塗りの部分を反転させるには、tableValues
属性の値をどう設定すべきか、予想がついただろうか?
tableValues
属性の値は、数が小さい順に並べなければならないわけではない。
むしろ最大値と最小値を入れ替えることで、反転を実現することができる。
透明(不透明度0
)な部分と塗り(不透明度1
)の部分を反転させたい場合は、tableValues
属性の値を1 0
と指定すればよい。
すると、元の不透明度が0
の場合は1
に、1
の場合は0
に…と、不透明度が反転する。
これで、<rect />
を取り囲む壁ができた。
<rect />
の範囲外を黒く塗りつぶすことができているのは、SVGのフィルタ操作は、元画像より10%
大きいボックスを用意して行われるためだ。
(しかし、SafariやiOSでは挙動が異なるようで、<rect />
の範囲外の塗りの部分は見えなくなってしまっているはずだ。)
さて、これまで作ってきた影は、入力画像がSourceAlpha
であるため、<rect />
自身が落とす影になっていた。
たった今作った壁を入力画像に変えることで、この壁が落とす影を生成できる。
そのためには、<feGaussianBlur />
のin
属性の値が、<feComponentTransfer />
の結果(壁)になるようにすればよい。
つぎに、<rect />
の外側の壁を消して、影だけを残したい。
特定の領域にのみ色を載せたい場合のマスク処理は、<feComposite operator="in" />
を使うことで実現できたことを覚えているだろうか。
今回は、in="SHADOW"
、in2="SourceAlpha"
とすることで、
<rect />
の塗りの部分ではin
を採用し、影のみ残る
<rect />
の範囲外ではin2
を採用し、何も残らない(<rect />
範囲外のin2
は透明)
という挙動を実現する。
最後に、元の<rect />
と影を合成するための<feMerge />
を再度追加しよう。
完成!!🎉