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

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

# 概要

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

[CSSでグリッチっぽい表現をやる](https://qiita.com/SnO2WMaN/items/e0582b64c41f31fc92cb)

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

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

![glitch](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/654ee2bf-905c-0d8c-f962-fcd29b0e8cf3.gif)

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

<p class="codepen" data-height="400" data-theme-id="default" data-default-tab="result" data-user="righteous_github" data-slug-hash="NWqPyZG" data-pen-title="Glitch effect using SVG filters">
  <span><a href="https://codepen.io/righteous_github/pen/NWqPyZG">
    CodePenで開く
  </a></span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

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

# SVGフィルター

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

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

![svgfilter-def.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/beb277ef-2f1f-0491-caee-362bd269676b.png)

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

## `<feOffset>`: オフセット

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

<p class="codepen" data-height="400" data-theme-id="default" data-default-tab="result" data-user="righteous_github" data-slug-hash="bGdNQxq" data-pen-title="SVG feOffset Filter">
  <span>See the Pen <a href="https://codepen.io/righteous_github/pen/bGdNQxq">
  SVG feOffset Filter</a> by righteous (<a href="https://codepen.io/righteous_github">@righteous_github</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

後で説明するが、ここでは`<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`に並べて指定する。

```math
\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}
```
例えば次のような行列を指定すれば、入力画像の緑成分だけ取り出すことができる。

```math
\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}
```
<p class="codepen" data-height="400" data-theme-id="default" data-default-tab="result" data-user="righteous_github" data-slug-hash="OJVPBae" data-pen-title="SVG feColorMatrix Filter">
  <span>See the Pen <a href="https://codepen.io/righteous_github/pen/OJVPBae">
  SVG feColorMatrix Filter</a> by righteous (<a href="https://codepen.io/righteous_github">@righteous_github</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

## `<feBlend>`: ブレンド

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

<p class="codepen" data-height="400" data-theme-id="default" data-default-tab="result" data-user="righteous_github" data-slug-hash="eYNNQeG" data-pen-title="SVG feBlend Filter">
  <span>See the Pen <a href="https://codepen.io/righteous_github/pen/eYNNQeG">
  SVG feBlend Filter</a> by righteous (<a href="https://codepen.io/righteous_github">@righteous_github</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

## `<feComposite>`: 合成

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

<p class="codepen" data-height="400" data-theme-id="default" data-default-tab="result" data-user="righteous_github" data-slug-hash="PoqqxgZ" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="SVG feMerge Filter">
  <span>See the Pen <a href="https://codepen.io/righteous_github/pen/PoqqxgZ">
  SVG feMerge Filter</a> by righteous (<a href="https://codepen.io/righteous_github">@righteous_github</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

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

`<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>`の`dx`が`0.5`のとき、`userSpaceOnUse`の場合は、フィルターが適用される側の要素上での0.5ピクセルとなり、`objectBoundingBox`の場合はフィルターが適用される側の要素のサイズの0.5倍となる。

ここではChromeのバグ[^bug]を避けるため`primitiveUnits="objectBoundingBox"`を使用している。

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

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

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

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

![svg filter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/2ad39bad-bb2b-a356-cdbc-7a61d3a8b135.png)

## 色収差エフェクト

[色収差](https://ja.wikipedia.org/wiki/%E8%89%B2%E5%8F%8E%E5%B7%AE)とは光の波長によって屈折率が異なるために、レンズを通して撮影すると輪郭が滲んだような写真になる現象である。ここでは色収差を表現するために、簡易的に入力画像をRとGとBに分解し、等間隔にずらす、という方法をとる。イメージとしては👇のような感じである。

<p class="codepen" data-height="500" data-theme-id="default" data-default-tab="result" data-user="righteous_github" data-slug-hash="vYOEbEp" data-pen-title="Chromatic Aberration Effect Visualization">
  <span>CodePenで開く: <a href="https://codepen.io/righteous_github/pen/vYOEbEp">
  Chromatic Aberration Effect Visualization</a></span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>
(このCodePenはどのように色収差を表現するかを視覚化するためのものであって、これ自体はSVGフィルターを使っていない)

### RGB各成分の抽出

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

```html
<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(×幅)だけ左右にずらす。

```html
<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>`を適用する必要がある。

```html
<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>
```

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

<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="result" data-user="righteous_github" data-slug-hash="JjddKxz" data-pen-title="CA">
  <span><a href="https://codepen.io/righteous_github/pen/JjddKxz">CodePenで開く</a></span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

## 横ずれエフェクト

### スライスしてずらす

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

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

![svg filter2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/82132b04-7cd7-0ec3-0d84-d65768a93647.png)

```html
<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>`の方がコンパクトに書ける)。

```html
<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>
```

結果は👇のようになる。

<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="result" data-user="righteous_github" data-slug-hash="PoqqBQw" data-pen-title="glitch without animation">
  <span><a href="https://codepen.io/righteous_github/pen/PoqqBQw">
  CodePenで開く
  </a></span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

## アニメーション

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

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

```html
<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>
```

<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="result" data-user="righteous_github" data-slug-hash="MWwwqWV" data-pen-title="glitch with one animation">
  <span><a href="https://codepen.io/righteous_github/pen/MWwwqWV">
    CodePenで開く
  </a></span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

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

<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="result" data-user="righteous_github" data-slug-hash="NWqPyZG" data-pen-title="Glitch effect using SVG filters">
  <span><a href="https://codepen.io/righteous_github/pen/NWqPyZG">
    CodePenで開く
  </a></span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

# 注意点

今回作成したものは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フィルターを使う機会はあまりないと思われるが、複雑なフィルターを作ることができるという点で利点があることを覚えておくとよいかもしれない。

# 参考
- [Chromatic Aberration Filters by Fabien Fellay](https://inkscape.org/~fabien.fellay/%E2%98%85chromatic-aberration-filters)
- [The Art Of SVG Filters And Why It Is Awesome — Smashing Magazine](https://www.smashingmagazine.com/2015/05/why-the-svg-filter-is-awesome/)
- [Filter Effects – SVG 1.1 (Second Edition)](https://www.w3.org/TR/SVG11/filters.html) (仕様)