phina.jsのfilter機能を使ってみる

phina.jsには画像（テクスチャ）にフィルタをかける機能があります。
この機能でSTG用に一つの弾画像から色とりどりの弾画像を動的に生成したり、SLGなどでアクティブでないキャラの色を薄暗くする、みたいな処理などに使うことができます。

[実行例](https://goo.gl/NdHqfS)

以降で示すソースコードは

- phina.js（v0.2.0）が読み込み済み
- phina.globalizeが実行済み

であることが前提です。

## フィルタの定義
関数で表現します。

```darkenFilter.js
// テクスチャを薄暗くするフィルタ
const darkenFilter = function(pixel, i, x, y, imageData) {
  if (pixel[3] === 0) return; // pixel[3]はalpha値、0の場合は透明ピクセルなので無視
  imageData.data[i] *= 0.3; // r
  imageData.data[i+1] *= 0.3; // g
  imageData.data[i+2] *= 0.3; // b
};
```

この関数が画像の各ピクセル毎に実行されることでフィルタ効果を実現しています。

#### 引数について

- pixel：ピクセル情報が [r, g, b, a] という配列の形で渡される[^1]
- i：ピクセルの開始インデックス。4, 8, 12...など4の倍数が渡ってくるはず
- x：ピクセルのx軸位置
- y：ピクセルのy軸位置
- imageData：画像データそのもの（の参照）。

基本的には第四引数までは条件式などに利用し、実際にエフェクトを適用する際はimageDataのdataプロパティを書き換えることになる（はず）。

[^1]: ちなみにクローンされた配列のため、元のピクセルへの参照は持ってません。

## フィルタの適用
Texture（画像）クラスのfilterメソッドを使います。

```js
const darkenFilter = function(pixel, i, x, y, imageData) {
  if (pixel[3] === 0) return; // 透明ピクセルは無視      
  imageData.data[i] *= 0.3; // r
  imageData.data[i+1] *= 0.3; // g
  imageData.data[i+2] *= 0.3; // b
};

const logoTexture = AssetManager.get('image', 'logo'); // ロード済み画像を取得
logoTexture.filter(darkenFilter); // 適用
```

### 複数のフィルタを適用させる
配列でフィルタ関数を渡せば複数のフィルタを適用できます。フィルタは順番に適用されます。

```js
const filter_1 = function(pixel, i, x, y, imageData) {/* 省略 */}
const filter_2 = function(pixel, i, x, y, imageData) {/* 省略 */}

const texture = AssetManager.get('image', 'logo');
texture.filter([filter_1, filter_2]);
```

### おまけ：フィルタ定義自体に引数を渡したい
```js
// 明るさ調整フィルタ
const luminescenceFilter = function(ratio) {
  const f = function(pixel, i, x, y, imageData) {
    if (pixel[3] === 0) return; // 透明ピクセルは無視      
    imageData.data[i] *= ratio; // r
    imageData.data[i+1] *= ratio; // g
    imageData.data[i+2] *= ratio; // b
 };
 return f;
};

AssetManager.get('image', 'logo').filter(luminescenceFilter(0.3));
```

フィルタ関数を返す関数、という形にすればフィルタ自体にパラメータを設定できます。

## 注意点
- 内部でgetImageDataメソッドを使っているため、CORS制約に引っかかることがあります。filterを使う際はサーバ上で実行、そして外部オリジン画像の使用を避ける必要があります。ローカル環境で試す際は要注意。~~めんどくさい~~
  * 画像をBase64に変換するという手もあります。


- またピクセルを一つ一つイテレートする性質上、 かなり計算負荷が高いです。（例えば128x128の画像だったら16384回ループ処理される）
なので初期化の際だけ適用し、それをキャッシュして使いまわすことをおすすめします。
毎フレームごとにフィルタをかけつつパフォーマンスを出したい場合は[WebGLの力](http://github.dev7.jp/b/2016/12/22/phinajsadvcal20161218/)を頼ったほうがいいでしょう。

- フィルタで書き換えたテクスチャ画像は元に戻せません。
元画像もそのまま利用したい場合は、フィルタをかけるテクスチャは`clone()`で複製して別のテクスチャとして登録し直しておくと良いでしょう。

```js
const darkenFilter = function(pixel, i, x, y, imageData) { /* 省略 */ };

const darkenTexture = AssetManager.get('image', 'player').clone().filter(darkenFilter);
AssetManager.set('image', 'player_inactive', darkenTexture);

// 使うときなど
Sprite('player_inactive').addChildTo(this);
```

## 付録：フィルタ色々
最後に色々お試しで作ったフィルタを並べておきます。

#### グレースケール化
![grayscale.png](https://qiita-image-store.s3.amazonaws.com/0/81823/96a05ac1-9b4a-bb20-0fff-b017722d614b.png)

```js
const grayScaleFilter = function(pixel, i, x, y, imageData) {
  if (pixel[3] === 0) return; // 透明ピクセルは無視     
  
  // YUV変換: http://www.mis.med.akita-u.ac.jp/~kata/image/rgbtoyuv.html
  const Y = 0.299 * pixel[0] + 0.587 * pixel[1] + 0.114 * pixel[2];
  imageData.data[i] = Y; // r
  imageData.data[i+1] = Y; // g
  imageData.data[i+2] = Y; // b
};
```

#### 色反転
![negate.png](https://qiita-image-store.s3.amazonaws.com/0/81823/3891172d-3b47-07d3-f726-4cd8df569f9d.png)

```js
const negateFilter = function(pixel, i, x, y, imageData) {
  if (pixel[3] === 0) return; // 透明ピクセルは無視
  imageData.data[i] = 255 - pixel[0]; // r
  imageData.data[i+1] = 255 - pixel[1]; // g
  imageData.data[i+2] = 255 - pixel[2]; // b
};
```

#### ゲームボーイ風


#### なんかさわやかな感じ？のシルエット
![sawayaka.png](https://qiita-image-store.s3.amazonaws.com/0/81823/38167300-b2ee-8790-b0b2-845f58e4f6df.png)

```js
const skySilhouetteFilter = function(pixel, i, x, y, imageData) {
  if (pixel[3] === 0) return; // 透明ピクセルは無視
  imageData.data[i] = y; // r
  imageData.data[i+1] = y; // g
  imageData.data[i+2] *= y; // b
};
```

#### 輪郭抽出（もどき）
![outlinefilter.png](https://qiita-image-store.s3.amazonaws.com/0/81823/89935733-0bb3-6640-aa54-89b73000909d.png)

こちらを参考にさせていただきました：[http://github.dev7.jp/b/2015/10/10/outline/](http://github.dev7.jp/b/2015/10/10/outline/)

```js
const outlineFilter = function(r, g, b) {
  // 輪郭線の色
  r = (r != null) ? r : 255;
  g = (g != null) ? g : 255;
  b = (b != null) ? b : 255;

  const f = function(pixel, i, x, y, imageData) {
    if (pixel[3] === 0) return; // 透明ピクセルは無視

    const w = imageData.width;
    const data = imageData.data;

    // 上下左右ピクセルのアルファ値インデックス
    const tIndex = i - w*4 + 3; // 上
    const rIndex = i + 4 + 3; // 右
    const lIndex = i - 4 + 3; // 左
    const bIndex = i + w*4 + 3; // 下

    // それぞれのピクセルの存在確認 (1に特に意味はない)
    const ta = (data[tIndex] != null) ? data[tIndex] : 1;
    const ra = (data[rIndex] != null) ? data[rIndex] : 1;
    const la = (data[lIndex] != null) ? data[lIndex] : 1;
    const ba = (data[bIndex] != null) ? data[bIndex] : 1;

    // 上下左右どれかが透明だったら指定色で塗りつぶす
    if (ta === 0 || ra === 0 || la === 0 || ba === 0) {
      data[i] = r; // r
      data[i+1] = g; // g
      data[i+2] = b; // b
      data[i+3] = 255; // a
    } else {
      // それ以外は限りなく透明にする（完全に透明すると上の透明判定に引っかかるため）
      data[i+3] = 1; // a
    }
  };
  return f;
};

AssetManager.get('image', 'logo').filter(outlineFilter(255, 0, 0));
```