[図解] HTML5 Canvasあれこれ - レイヤー, マスク(ImageData), 全画面でのハマりポイント

HTML5のCanvasを使う上で知っておくと役に立つかもしれないことを今更書いておきます。思いついたら随時更新していきます。

#対象読者
Canvasに関しての基本的な解説は省きますので、この記事はある程度Canvasが使える方向けとなります。具体的には

- Canvasの生のAPIで四角形やパスを書くことができる

が基準となります。

#Canvasをレイヤーのように重ねる

これはもうCanvasではなくCSSだと思いますが、Canvasを複数重ねることができます。Canvasは背景が透明なので`position: relative;`を親要素に指定し、Canvasを`position: absolute;`にすることで、複数のCanvasを重ねる事ができます。

```html
<div class="canvas-wrapper">
    <canvas id="canvas1"></canvas>
    <canvas id="canvas2"></canvas>
    <canvas id="canvas3"></canvas>
</div>
```

```css
.canvas-wrapper {
    position: relative;
}
.canvas-wrapper canvas {
    position: absolute;
    top: 0;
    left: 0;
}
```

この注意点としては

- HTMLで先に記述したものが下になる
- `top`と`left`を指定しているので、すべてのCanvasの大きさが揃わないときは左上揃えになる。

の2点です。
前者については先程の例で図解すると以下のようになります。

![canvas-layer1.png](https://qiita-image-store.s3.amazonaws.com/0/166423/4ff50fdb-bff5-2fbe-d512-6759c379f940.png)

後者については、CSS次第で色々できます。例えば上下左右中央揃えにするときは

```css
.canvas-wrapper canvas {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    margin: auto;
}
```

を指定します。すべて0にして、`margin: auto`にするのがポイントです。こうしないとうまく中央に揃ってくれません。4つの角のいずれかに揃えるときは`margin: auto`は要りません。


#Canvasでマスクをかける

Canvasで、いわゆるマスクというのをやってみます。GIMPやPhotoshopなどの画像編集ソフトには「レイヤーマスク」という機能がありますが、これは通常のレイヤー画像と、グレースケールのマスク画像を用意し、マスク画像の色にあわせて通常のレイヤー画像の透明度を操作する、というものです。言葉で説明してもわかりにくいので図でどうぞ。

![yashiori.png](https://qiita-image-store.s3.amazonaws.com/0/166423/f9ad3aaf-cee4-82b2-aa28-75d10af85193.png)

ｱｯｺﾚｼﾞｬﾅｲ

![canvas-mask.png](https://qiita-image-store.s3.amazonaws.com/0/166423/a1f522ce-0581-7f8f-dde0-e8d9eb12feed.png)

こういうことです。

マスク画像が白に近いところは不透明に、黒に近いところは透明になります。これのいいところは色がそのまま透明度になるのでアンチエイリアスつきの画像をマスクにしたとき境界部分のグレーがきちんと半透明になるところです。これを実装するために「ImageData」を使います。

## ImageDataってなんぞや

[ImageData - Web API インターフェイス | MDN](https://developer.mozilla.org/ja/docs/Web/API/ImageData)

ImageDataでは、Canvasの各ピクセルのRGBA(R: 赤, G: 緑, B: 青, A: 透明度)の値を直接操作できます。このImageDataに関連した関数がCanvasのcontextに3つあります。

- `context.createImagedata(width, height)` - 幅と高さからImagedataを生成(すべてのピクセルは透明な黒)
- `context.createImagedata(imagedata)` - 引数と同じ幅と高さのImagedataを生成(すべてのピクセルは透明な黒)
- `context.getImagedata(x, y, width, height)` - 引数で与えられた矩形の形のImageDataをCanvasから取得
- `context.putImageData(imagedata, x, y [, dirtyX, dirtyY, dirtyWidth, dirtyHeight ])` - ImageDataをCanvasに描画 引数についてはあとで図解

このうち、`createImageData`と`getImageData`はImageDataが返ってきます。ではImageDataを見てみましょう。ImageDataには3つのプロパティがあります。

- data - ImageDataの本体(Uint8ClampedArray… ですがとりあえず配列と思っててください)
- width - ImageDataの幅
- height - ImageDataの高さ

dataは1次元配列で長さは`width * height * 4`になります。どうなっているかというと、一つのピクセルのRGBAの4つの情報を繰り返し、画像の左上から右へ、右端へついたらひとつ下の段の左端から右へ… といった構造になっています。

![canvas-imagedata.png](https://qiita-image-store.s3.amazonaws.com/0/166423/4c38940a-31d5-e128-8812-3795f23cf859.png)

## Canvasで実装

ではこれをcanvasで実装するにはどうしたらいいか、簡単です。ImageDataというものがCanvasには備わっています。これを利用して以下のように実装します。

```html
<canvas id="canvas_main"></canvas>
<canvas id="canvas_main"></canvas>
<canvas id="canvas_main"></canvas>
```

```js
window.addEvevntListener("DOMContentLoaded", () => { //jQueryでいうところのready
    //各キャンバスを持っておく
    const mainCanvas = document.getElementById("canvas_main");
    const maskCanvas = document.getElementById("canvas_mask");
    const resultCanvas = document.getElementById("canvas_result");

    //各キャンバスのコンテキストを取得
    const mainCtx = mainCanvas.getContext("2d");
    const maskCtx = maskCanvas.getContext("2d");
    const resultCtx = resultCanvas.getContext("2d");

    //マスクを更新する関数
    const updateMask = () => {
        //Canvasのサイズ
        const {width, height} = mainCanvas;

        //メインとマスクのイメージデータを取得
        const mainImgData = mainCtx.getImageData(0, 0, width, height);
        const maskImgData = maskCtx.getImageData(0, 0, width, height);
        const mainData = mainImgData.data;
        const maskData = maskImgData.data;

        //結果のイメージデータを作成
        const resultImgData = resultCtx.createImageData(width, height);
        const resultData = resultImgData.data;

        //各ピクセルに対して繰り返し
        for(let i = 0, len = width * height; i < len; i++){
            //ピクセル
            const p = i * 4;
            //RGB値はメインをそのまま
            resultData[p] = mainData[p];
            resultData[p + 1] = mainData[p + 1];
            resultData[p + 2] = mainData[p + 2];
            //alpha値はメインの透明度とマスクのR値(グレースケール前提のため)を掛け算 ImageDataの値は255~0のため、255の2乗で割る
            resultData[p + 3] = (mainData[p + 3] * maskData[p]) / Math.pow(255, 2);
        }

        //Canvasに戻す
        resultCtx.putImageData(resultImgData, 0, 0);
    };

    //初回呼びだし
    updateMask();
});
```

この`updateMask()`関数がマスクをかける関数です。

## コードの解説

では、マスクをかけていきます。まず、マスクをかける前の画像が`mainCanvas`に、マスクとなるグレースケール画像が`maskCanvas`にあるものとします。コンテキストはそれぞれ`mainCtx`、`maskCtx`とします。`mainCanvas`と`maskCanvas`の大きさは同じ、`maskCanvas`はグレースケール画像という前提で話を続けます。

### Canvasのサイズ

まずは画像の幅と高さを取得します。

```js
const {width, height} = mainCanvas;
```

分割代入、便利ですよね。

### メインとマスクのイメージデータを取得

```js
const mainImgData = mainCtx.getImageData(0, 0, width, height);
const maskImgData = maskCtx.getImageData(0, 0, width, height);
const mainData = mainImgData.data;
const maskData = maskImgData.data;
```

出ました、`getImageData()`。4つの引数、(x, y, width, height)を渡します。Canvas全体を取得します。

ImageDataの`data`プロパティから配列を取り出します。参照渡しになるので、これでこのあとforで何度も呼び出すときの処理を少し高速化できます。

### 結果のイメージデータを作成

```js
const resultImgData = resultCtx.createImageData(width, height);
const resultData = resultImgData.data;
```

resultに表示するためのImageDataを作成します。この段階では、透明な黒の矩形になっています。

### 各ピクセルに対して繰り返し


```js
for(let i = 0, len = width * height; i < len; i++){
    const p = i * 4;
    resultData[p] = mainData[p];
    resultData[p + 1] = mainData[p + 1];
    resultData[p + 2] = mainData[p + 2];
    resultData[p + 3] = (mainData[p + 3] * maskData[p]) / Math.pow(255, 2);
}
```

`width * height`がすべてのピクセルの数となりますので、この回数分繰り返します。

`p`という変数には現在のピクセルのRの配列上でのインデックスを入れています。そのあと3行は、RGBの値をそのまま移し替えています。

最後がポイントです。`resultData[p + 3]`はA(透明度)を指しています。メインの透明度と、マスクのR値を掛け算しています。マスクはグレースケール前提なので、RGBは同じなのでどれか一つをとればいいことになります。マスクがグレースケールでないならここでRGBの平均を取るなり最大値を取るなりしてください。RGBAの値はすべて0~255となっています。そのため、255の2乗で割ることで0~255の範囲におさめています。ここは常に一定の数なので`65025`に置き換えても構いません。可読性のためにこうしています。

### Canvasに戻す

```js
resultCtx.putImageData(resultImgData, 0, 0);
```

resultのCanvasに戻します。引数は配列ではなくImageDataを渡してください。

これでマスクは終わりです。

# Canvasを全画面表示にしたら隙間ができてハマった

Canvasを全画面表示にしたいことありますよね。普通はこんな感じで実装します。

```html
<body>
    <canvas id="canvas"></canvas>
    <script src="script.js"></script>
</body>
```

```css
body {
    margin: 0;
}
```

```js
window.addEventListener("DOMContentLoaded", () => {
    const canvas = document.getElementById("canvas");
    const resize = () => {
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
    };
    window.addEventListener("resize", resize);
    resize();
});
```

これ、やると隙間できちゃうんですよ。スクロールバーが出てきてしまうんですよ。
bodyに`overflow: hidden;`を指定すればいいんですがこれなんでだかわからなくて気持ち悪かったです。

## インデント・改行のせいだった…

意味わかりませんよね。HTMLではインデントは影響しないはずなのに。

実はHTMLでは連続する空白は1つの空白とみなすんです。そのせいで画面にはCanvasしかないはずなのに空白文字も入ってしまって隙間ができたのです。では解決しましょう。

## HTMLを変える

インデント・改行がなければいいのですから、こうしてしまいます。

```html
<body><!--
    --><canvas id="canvas"></canvas><!--
    --><script src="script.js"></script>
</body>
```

コメントアウトしてしまう方法です。これでも消せますがスマートじゃなくて好きではないので、もう一つの方法を提示します。

## CSSを変える

HTMLはそのまま、CSSで対応します。bodyのCSSをこう書き換えます。

```css
body {
    margin: 0;
    font-size: 0;
}
```

`font-size: 0;`にしてしまう方法です。これで文字がなくなるので隙間もなくなります。個人的にはこれが好きです。もしCanvasの上に普通の要素を重ねてそこに文字を出したいなら、その要素に`font-size: 1rem;`を指定します。remはルートの文字サイズなので、bodyが0でも大丈夫です。