68
66

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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

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

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

が基準となります。

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

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

<div class="canvas-wrapper">
    <canvas id="canvas1"></canvas>
    <canvas id="canvas2"></canvas>
    <canvas id="canvas3"></canvas>
</div>
.canvas-wrapper {
    position: relative;
}
.canvas-wrapper canvas {
    position: absolute;
    top: 0;
    left: 0;
}

この注意点としては

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

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

canvas-layer1.png

後者については、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

アッコレジャナイ

canvas-mask.png

こういうことです。

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

ImageDataってなんぞや

ImageData - Web API インターフェイス | MDN

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に描画 引数についてはあとで図解

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

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

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

canvas-imagedata.png

Canvasで実装

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

<canvas id="canvas_main"></canvas>
<canvas id="canvas_main"></canvas>
<canvas id="canvas_main"></canvas>
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にあるものとします。コンテキストはそれぞれmainCtxmaskCtxとします。mainCanvasmaskCanvasの大きさは同じ、maskCanvasはグレースケール画像という前提で話を続けます。

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;

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

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

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

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

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

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

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に戻す

resultCtx.putImageData(resultImgData, 0, 0);

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

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

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

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

<body>
    <canvas id="canvas"></canvas>
    <script src="script.js"></script>
</body>
body {
    margin: 0;
}
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を変える

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

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

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

CSSを変える

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

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

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

68
66
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
68
66

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?