Canvasでグリッチアニメーション


:::note info
viviONグループでは、DLsiteやcomipoなど、二次元コンテンツを世の中に届けるためのサービスを運営しています。
ともに働く仲間を募集していますので、興味のある方は[こちら](#一緒に二次元業界を盛り上げていきませんか)まで。
:::

# :loudspeaker: はじめに

Canvasでもグリッチアニメーションがしたい！ということで、この記事ではCanvas上でのグリッチアニメーションの実装を解説します。

グリッチアニメーションもといグリッチエフェクトとは、意図的にノイズや歪みを加え、エラーやバグのような視覚効果を再現したものです。元々はCG分野で使用されていましたが、現在ではWebデザインにも利用されています。

Web上でグリッチエフェクトを表示する方法にはCSSやSVGを使用した例がたくさんありますが、Canvasを使うことでさらに細かい制御や複雑なアニメーションを実装できます。Canvasはピクセル単位で操作が可能で、自由度が高く、シンプルなAPIにより実装に集中できます。これによりグラフィック操作や描画アルゴリズムについてよく学ぶことができます。

実際にCanvas上で実装すると、以下のようになります。

<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="QWRZJJL" data-pen-title="Untitled" data-user="tenorichan" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/tenorichan/pen/QWRZJJL">
  Untitled</a> by tenori (<a href="https://codepen.io/tenorichan">@tenorichan</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

# :deciduous_tree: 環境準備

今回はviteで作成した適当な環境にて実装します。

※`main.js`の内容をほぼそのままcodepenで実装してますので、環境構築は不要です。

```bash
npm create vite glitch-canvas -- --template vanilla
```

# :runner: チャンネルシフトの実装

任意のRGB値を横方向や縦方向にずらすことで、色収差やフリンジのような効果を与えます。

以下のコードは、指定された範囲のピクセルデータを取得する例です。

```jsx
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const imageData = ctx.getImageData(sx, sy, sw, sh);
```

`getImageData`メソッドは、`ImageData`オブジェクトを返します。`ImageData`の`data`プロパティは、RGBAの順で0から255の間の整数を含む1次元配列で、各ピクセルの色データを保持しています。
この配列を操作することで、チャンネルシフト効果を実現します。以下のコードはキャンバス上のR（赤）チャンネル要素を右方向にシフトさせる例です。

<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="NWVOvmj" data-pen-title="Untitled" data-user="tenorichan" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/tenorichan/pen/NWVOvmj">
  Untitled</a> by tenori (<a href="https://codepen.io/tenorichan">@tenorichan</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

## :underage: Rチャンネルのみシフトする

```jsx
for (let y = 0; y < height; y++) {
  for (let x = 0; x < width; x++) {
    const i = (y * width + x) * 4;
    const dstX = x + shiftX;

    if (dstX >= 0 && dstX < width) {
      const dstI = (y * width + dstX) * 4;
      imageData.data[dstI + channel] = tmp[i + channel];
    }
  }
}
```

この実装は、forの二重ループでキャンバスの各ピクセルを処理し、Rチャンネル要素のみを横方向にシフトします。`i`は元のピクセルのインデックス、`dstX`はシフト後のX座標、`dstI`は新しいインデックスを、`channel`は対象のRGBAチャンネルを示します。

:::note
0: Red, 1: Green, 2: Blue, 3: Alpha
:::

# :scissors: 行で分割してずらす

行で分割してずらす事で、ブロックノイズのような効果を与えることができます。

以下は適当な高さの適当な幅で要素をずらしたコードの実装例です。

<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="jOoeeZK" data-pen-title="Untitled" data-user="tenorichan" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/tenorichan/pen/jOoeeZK">
  Untitled</a> by tenori (<a href="https://codepen.io/tenorichan">@tenorichan</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

## :vhs: RGBチャンネル全ての要素をシフトする

```jsx
for (let y = 0; y < height; y++) {
  for (let x = 0; x < width; x++) {
    const i = (y * width + x) * 4;
    const dstX = x + shiftX;

    if (dstX >= 0 && dstX < width) {
      const dstI = (y * width + dstX) * 4;
      imageData.data[dstI + 0] = tmp[i + 0];
      imageData.data[dstI + 1] = tmp[i + 1];
      imageData.data[dstI + 2] = tmp[i + 2];
    }
  }
}
```

今回のコードは前項と似てますが、RGBチャンネル全ての要素をシフトさせてます。

## :pencil2: `measureText`メソッド

テキストの描画サイズに合わせて分割を行うことを考慮し、`measureText`メソッドを用いてベースとなる座標とテキストサイズの計算を行ってます。

`measureText` は測定したテキストの情報（幅など）を持つ`TextMetrics`オブジェクトを返します。`TextMetrics`から参照可能な諸々の数値は`textAlign`や`textBaseline`の設定に影響されることがあるので注意が必要です。

以下のコードではテキストの高さを得るために`actualBoundingBoxDescent`から値を得てますが、`textBaseline`から底までのサイズを返すので2倍にしています。

```jsx
const textHeight = ctx.measureText("Glitch").actualBoundingBoxDescent * 2;
const baseY = Math.floor(height / 2 - textHeight / 2);
```

# :watch: アニメーションの追加

グリッチエフェクトにアニメーションを追加することで、より動的で視覚的な効果を発揮します。これまでの実装を踏まえて、ランダムな動きをするグリッチアニメーションを実装してみましょう。

以下のコードでは前項までの実装を合わせて、毎ループランダムな計算結果を表示させています。

<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="QWRZJJL" data-pen-title="Untitled" data-user="tenorichan" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/tenorichan/pen/QWRZJJL">
  Untitled</a> by tenori (<a href="https://codepen.io/tenorichan">@tenorichan</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

かなりガチャガチャしていていい感じです :hugging:

分割数や幅にこだわるともっとガチャガチャして良いかと思います。

## :clapper: ImageDataについて

特段解説する内容でもないですが、各所の`putImageData`にてオリジナルの`ImageData`が失われてしまいます。上書きされる前にオリジナルを変数に保持しておきましょう

# :airplane: 応用

今回はテキストを用いて解説しましたが、画像データも利用することができます。

<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="ExzdGMe" data-pen-title="Untitled" data-user="tenorichan" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/tenorichan/pen/ExzdGMe">
  Untitled</a> by tenori (<a href="https://codepen.io/tenorichan">@tenorichan</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

# :speech_balloon: 余談

今どきのWebGLライブラリにはグリッチエフェクト以外にも、標準でもっとハイクオリティのエフェクトがたくさん付いてきます。お得だね :robot:

# :heart: 参考

- https://qiita.com/SnO2WMaN/items/e0582b64c41f31fc92cb
    - CSSを用いたグリッチエフェクト実装例
- https://qiita.com/righteous/items/ecc3b9b2725202e350fd
    - SVGを用いたグリッチエフェクト実装例
- https://note.kiriukun.com/entry/20171118-rgb-channel-shifting
    - Canvas上でRGB値をずらす実装例

# 一緒に二次元業界を盛り上げていきませんか？
株式会社viviONでは、フロントエンドエンジニアを募集しています。

https://vivion.jp/recruit/

また、フロントエンドエンジニアに限らず、バックエンド・SRE・スマホアプリなど様々なエンジニア職を募集していますので、ぜひ採用情報をご覧ください。