Help us understand the problem. What is going on with this article?

[Web] 要素を分割せずに済むグリッチエフェクト ~SVGフィルターのキホン~

Note: この記事に掲載しているCodePenはSVGフィルターを使用しており、ChromeとFirefox以外のブラウザーでは正常に表示されない可能性がある。

概要

この記事は👇の記事にインスパイアされたものである。

CSSでグリッチっぽい表現をやる

グリッチエフェクトを表現するには文字や画像を横にスライスして横方向に少し位置をずらす必要がある。上の記事はスライスごとにHTMLの要素を用意するという方法をとっており、同じテキスト(or 画像)を持った要素をスライスの数だけ用意する必要がある。ここではSVGフィルターを用いた、要素1個で済むグリッチエフェクトを紹介する(厳密には、<svg>とフィルター要素を除いて1個)。このエフェクトはテキストや画像、SVGで表現できるあらゆる図形に適用できる。

最終的には👇のようなものができる(ループ可)。横に分割してずらすだけでなく、色収差エフェクトを加えることでよりぽくなっている。

glitch

CodePenのデモ👇(普通にテキストをマウスで選択するができる)

CodePenで開く

この記事は、SVGフィルターの簡単な説明から始めて、少しずつコードを構築していく形を取る。

SVGフィルター

SVGはベクター形式の画像であり、HTMLの<img>タグで外部SVGファイルを表示させることができるだけでなく、HTML内に直接SVGを書くことができる。また、SVGの中にはテキストや四角形、円などいろいろな図形を含めることができるが、それらに対してフィルター効果を付与することができる。

フィルターは普通<svg>内の<defs>内で宣言する。フィルターは一度宣言すれば何度も参照でき、同じページ内の別の<svg>からでも参照することができる

svgfilter-def.png

<filter>の中では"ぼかしフィルター"や"色変換フィルター"などの基本的なフィルター処理(フィルタープリミティブ)を組み合わせることで、全体として一つのフィルターを定義する。今回用いるフィルタープリミティブは<feOffset><feColorMatrix><feBlend><feMerge>の4つである。これ以外にもいくつかあるが、ここではこの4つだけ説明する。

<feOffset>: オフセット

これは非常に単純で、入力画像をx軸、y軸方向に指定した長さだけ平行移動(オフセット)させるものである。

See the Pen SVG feOffset Filter by righteous (@righteous_github) on CodePen.

後で説明するが、ここでは<filter>primitiveUnits="objectBoundingBox"が指定されているため、dx dyに指定した値はフィルターが適用されている要素の幅、高さに対する割合となる。今の場合、dxが0.1なので<image>の幅200に0.1を掛けた20だけx軸方向に移動することになる。

<feColorMatrix>: 行列による色変換

これは入力画像の各ピクセルのRGBA値を行列によって変換するものである。数式で表すと👇のようになる。$R,G,B,A$が入力の各ピクセルのRGBA値であり、$R',G',B',A'$が出力の値である。単にRGBAの列ベクトルに指定した行列を掛けるだけなので、行列の乗算を知らない人はぜひ調べてみてほしい。実際指定できるのは$a_1$から$a_{20}$の部分だけであり、これらをvaluesに並べて指定する。

\begin{bmatrix}
R'\\
G'\\
B'\\
A'\\
1
\end{bmatrix} = 
\begin{bmatrix}
a_1 & a_2 & a_3 & a_4 & a_5\\
a_6 & a_7 & a_8 & a_9 & a_{10}\\
a_{11} & a_{12} & a_{13} & a_{14} & a_{15}\\
a_{16} & a_{17} & a_{18} & a_{19} & a_{20}\\
0 & 0 & 0 & 0 & 1\\
\end{bmatrix}
\begin{bmatrix}
R\\
G\\
B\\
A\\
1
\end{bmatrix}

例えば次のような行列を指定すれば、入力画像の緑成分だけ取り出すことができる。

\begin{bmatrix}
R'\\
G'\\
B'\\
A'\\
1
\end{bmatrix} = 
\begin{bmatrix}
0 & 0& 0& 0& 0\\
0 & 1& 0& 0 & 0\\
0 & 0& 0& 0 & 0\\
0 & 0& 0& 1 & 0\\
0 & 0 & 0 & 0 & 1\\
\end{bmatrix}
\begin{bmatrix}
R\\
G\\
B\\
A\\
1
\end{bmatrix} = 
\begin{bmatrix}
0\\
G\\
0\\
A\\
1
\end{bmatrix}

See the Pen SVG feColorMatrix Filter by righteous (@righteous_github) on CodePen.

<feBlend>: ブレンド

これは画像編集ソフトにおけるレイヤーのブレンドと同じようなもので、2つの入力画像を指定したブレンドモードで合成する。👇はdarkenモードでブレンドしたものである。

See the Pen SVG feBlend Filter by righteous (@righteous_github) on CodePen.

<feComposite>: 合成

これは<feBlend>と同じようなものであるが、ブレンドモードは固定でnormalで合成する(画像編集ソフトにおけるレイヤーのデフォルトのブレンドモードと同じである)。ただし<feBlend>と違い、3個以上の入力画像を指定できるため、コードがコンパクトになる。下は画像の上に緑色の四角を重ね、さらに透明度0.5の青色の四角を重ねている。

See the Pen SVG feMerge Filter by righteous (@righteous_github) on CodePen.

フィルタープリミティブの適用範囲の指定

<filter>とすべてのフィルタープリミティブは、フィルターを適用する範囲をx y width height で指定することができる。例えば<feOffset>を適用すると元の要素の範囲をはみ出すことになるため、それも表示されてほしいならば<filter>の範囲を要素の範囲より広めに取る必要がある。親切にも<filter>の範囲のデフォルト値はx="-10%" y="-10%" width="120%" height="120%"となっており、少しはみ出しても表示されるようになっている。<filter>の範囲をはみ出すような範囲をフィルタープリミティブに指定しても意味はない。

補足: <filter>primitiveUnits属性について

フィルタープリミティブで指定する色々な座標や長さは<filter>primitiveUnits属性の影響を受ける。

  • primitiveUnits="userSpaceOnUse": このフィルターが適用された要素の座標系における値(パーセント値を指定した場合、その要素を含むSVG要素のサイズに対する割合)
  • primitiveUnits="objectBoundingBox": このフィルターが適用された要素のサイズに対する割合(0.1なら0.1倍)

例えばフィルタープリミティブの1つ<feOffset>dx0.5のとき、userSpaceOnUseの場合は、フィルターが適用される側の要素上での0.5ピクセルとなり、objectBoundingBoxの場合はフィルターが適用される側の要素のサイズの0.5倍となる。

ここではChromeのバグ1を避けるためprimitiveUnits="objectBoundingBox"を使用している。

グリッチエフェクトの実装

グリッチエフェクトは適用される要素のサイズに対して、横に少しはみ出す形になるため、左右に少し余裕を持ってフィルターの領域を指定する。広げすぎるとパフォーマンスに影響が出るため、必要最小限にするのがよい。

<filter id="glitch" primitiveUnits="objectBoundingBox" x="-10%" y="0%" width="120%" height="100%">
</filter>

svg filter.png

色収差エフェクト

色収差とは光の波長によって屈折率が異なるために、レンズを通して撮影すると輪郭が滲んだような写真になる現象である。ここでは色収差を表現するために、簡易的に入力画像をRとGとBに分解し、等間隔にずらす、という方法をとる。イメージとしては👇のような感じである。

CodePenで開く: Chromatic Aberration Effect Visualization

(このCodePenはどのように色収差を表現するかを視覚化するためのものであって、これ自体はSVGフィルターを使っていない)

RGB各成分の抽出

まずは入力画像からR成分G成分B成分をそれぞれ抽出する必要がある。これは先述のとおり<feColorMatrix>を使って👇のように実現することができる。フィルタープリミティブは入力画像をin属性で指定することができ、result属性で出力に名前をつけることができる。いずれもin属性にSourceGraphicを指定しているが、これは文字通り元の画像(フィルターを適用しようとしている図形の、フィルターを適用する前のレンダリング結果)である。各成分は後で使用するため、resultでそれぞれの出力に名前(red green blue)をつけている。

<filter id="glitch" primitiveUnits="objectBoundingBox" x="-10%" y="0%" width="120%" height="100%">
  <feColorMatrix in="SourceGraphic" result="red" type="matrix" values="1 0 0 0 0
                                                                        0 0 0 0 0
                                                                        0 0 0 0 0
                                                                        0 0 0 1 0" />
  <!-- green: G成分 -->
  <feColorMatrix in="SourceGraphic" result="green" type="matrix" values="0 0 0 0 0
                                                                          0 1 0 0 0
                                                                          0 0 0 0 0
                                                                          0 0 0 1 0" />
  <!-- blue: B成分 -->
  <feColorMatrix in="SourceGraphic" result="blue" type="matrix" values="0 0 0 0 0
                                                                        0 0 0 0 0
                                                                        0 0 1 0 0
                                                                        0 0 0 1 0" />
</filter>

各成分を横にずらす

次に、R成分を左に、B成分を右にずらす。これは明らかに<feOffset>の出番である。次のように、上で抽出したR成分とB成分をinで参照し、±0.005(×幅)だけ左右にずらす。

<filter id="glitch" primitiveUnits="objectBoundingBox" x="-10%" y="0%" width="120%" height="100%">
  <!-- ...省略... -->

  <!-- red-shifted: R成分を左にずらしたもの -->
  <feOffset in="red" result="red-shifted" dx="-0.005" dy="0" />
  <!-- blue-shifted: B成分を右にずらしたもの -->
  <feOffset in="blue" result="blue-shifted" dx="0.005" dy="0" />
</filter>

ブレンド

最後に3つの成分を重ね合わせて一つの画像にする。これはお察しの通り<feBlend>を使う。色が明るくなる方向にブレンドしたいので、screenモードを使用する。<feBlend>は2つの画像しか入力できないため、3つの画像を合成するには2回<feBlend>を適用する必要がある。

<filter id="glitch" primitiveUnits="objectBoundingBox" x="-10%" y="0%" width="120%" height="100%">
  <!-- ...省略... -->

  <!-- blended: ブレンド結果 -->
  <feBlend mode="screen" in="red-shifted" in2="green" result="red-green" />
  <feBlend mode="screen" in="red-green" in2="blue-shifted" result="blended" />
</filter>

これで、色収差エフェクトの完成である。

CodePenで開く

横ずれエフェクト

スライスしてずらす

次に、画像を横向きにいくつかスライスして、さらに一部のスライスを横向きに色々な距離動かす必要がある。そのためには、<feOffset>各スライスの範囲にだけ適用する必要がある。

先述の通り、各フィルタープリミティブはx y width height でその適用範囲を指定することができる。このとき、その出力における指定した範囲外の領域はすべて透明となる。スライスごとに<feOffset>で該当範囲を横に動かす。

svg filter2.png

<filter id="glitch" primitiveUnits="objectBoundingBox" x="-10%" y="0%" width="120%" height="100%">
  <!-- ...省略... -->

  <!-- スライスごとに横に動かす -->
  <feOffset in="blended" result="slice1" dx="0" dy="0" y="0%" height="30%"></feOffset>
  <feOffset in="blended" result="slice2" dx="0.01" dy="0" y="30%" height="4%"></feOffset>
  <feOffset in="blended" result="slice3" dx="0" dy="0" y="34%" height="26%"></feOffset>
  <feOffset in="blended" result="slice4" dx="-0.007" dy="0" y="60%" height="2%"></feOffset>
  <feOffset in="blended" result="slice5" dx="0" dy="0" y="62%" height="38%"></feOffset>
</filter>

マージ

最後に、スライスを1つの画像にまとめる必要がある。スライス同士は範囲が重複しないので、<feMerge>を使って単に重ね合わせればよい(<feBlend>を使ってもできるが、<feMerge>の方がコンパクトに書ける)。

<filter id="glitch" primitiveUnits="objectBoundingBox" x="-10%" y="0%" width="120%" height="100%">
  <!-- ...省略... -->

  <!-- すべてのスライスをマージ -->
  <feMerge>
    <feMergeNode in="slice1" />
    <feMergeNode in="slice2" />
    <feMergeNode in="slice3" />
    <feMergeNode in="slice4" />
    <feMergeNode in="slice5" />
  </feMerge>
</filter>

結果は👇のようになる。

CodePenで開く

アニメーション

アニメーションといってもCSSアニメーションではなく、SVG独自のアニメーション機能を使用する。アニメーションは属性値に対して適用するものであり、適用したい要素の子要素として<animate>を追加する。

例えば2つ目のスライスの<feOffset>に対して👇のように<animate>を追加すると、そのスライスが動くようになる。attributeNameは変化させたい属性の名前、durがアニメーションの時間、keytimesはタイミング(0から1の値)、valuesは各タイミングにおけるその属性の値である。グリッチエフェクトでは滑らかに移動してほしくないため、calcModediscreteにすることで飛び飛びに移動するようにしている。また、repeatCountindefiniteにすることで永久に繰り返すようにしている。

<filter id="glitch" primitiveUnits="objectBoundingBox" x="-10%" y="0%" width="120%" height="100%">
  <!-- ...省略... -->

  <!-- 2つ目のスライス -->
  <feOffset in="blended" result="slice2" dy="0" y="30%" height="4%">
    <animate attributeName="dx"
            values="0; -0.03; -0.06"
            keytimes="0; 0.2; 0.8"
            begin="0s"
            dur="3s"
            calcMode="discrete"
            repeatCount="indefinite" />
  </feOffset>

  <!-- ...省略... -->
</filter>

CodePenで開く

最後に、スライスの数や高さを調節し、スライスだけでなく色収差の<feOffset>にもアニメーションを追加してvalueskeytimesを調節すると完成である👇。

CodePenで開く

注意点

今回作成したものはWindows 10のChrome 80.0.3987.87とFirefox 74.0b1とEdge 80.0.361.48、 AndroidのChrome 80.0.3987.87とFirefox 68.4.2で正常に表示されることを確認したが、IEや非ChromiumのEdgeでは正常に表示されなかった。また、ChromeやFirefoxでも(今回使用しなかった)一部のフィルターでバグが存在するため、十分に注意する必要がある。

最後に

SVGフィルターを用いることで、元の要素を複製するなどの変更をHTMLに加えなくても、フィルターを1つ適用するだけでグリッチエフェクトを実現することができる。また、フィルターの定義はページのどこからでも参照できるため、何度も定義する必要がない。

このようなエフェクトを作るとき、ほとんどの場合CSSで事足りるし、ブラウザーの対応状況が少し不安であるということもあり、SVGフィルターを使う機会はあまりないと思われるが、複雑なフィルターを作ることができるという点で利点があることを覚えておくとよいかもしれない。

参考


  1. Chrome 80で試した限りでは、あるSVGで定義されたフィルターがprimitiveUnits="userSpaceOnUse"であるとき、別のSVG内の要素にこのフィルター適用すると、フィルタープリミティブのx y width heightがパーセント値であるとき、その100%が後者のSVGのサイズではなく前者のSVGのサイズとなるというバグが発生する。Firefoxではこの現象は起きなかった。また、検索してもこのことに関する情報は見つからなかった。デモ: https://codepen.io/righteous_github/pen/ZEGzwJy 

righteous
主にJavaScriptとPythonを使う。 あまり日本語に翻訳されていない情報を中心に紹介する。 分かりやすい説明、正確な用語、曖昧さのない文章を心がける。 オブジェクト指向より関数型に興味がある。 ホビープログラマーなのでプロの方からすると突っ込みどころがあるかもしれないですがご指摘いただけるとありがたいです。 感想をコメントやツイッターなどで書いていただけると喜びます。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした