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で先に記述したものが下になる
-
top
とleft
を指定しているので、すべてのCanvasの大きさが揃わないときは左上揃えになる。
の2点です。
前者については先程の例で図解すると以下のようになります。
後者については、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などの画像編集ソフトには「レイヤーマスク」という機能がありますが、これは通常のレイヤー画像と、グレースケールのマスク画像を用意し、マスク画像の色にあわせて通常のレイヤー画像の透明度を操作する、というものです。言葉で説明してもわかりにくいので図でどうぞ。
アッコレジャナイ
こういうことです。
マスク画像が白に近いところは不透明に、黒に近いところは透明になります。これのいいところは色がそのまま透明度になるのでアンチエイリアスつきの画像をマスクにしたとき境界部分のグレーがきちんと半透明になるところです。これを実装するために「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に描画 引数についてはあとで図解
このうち、createImageData
とgetImageData
はImageDataが返ってきます。ではImageDataを見てみましょう。ImageDataには3つのプロパティがあります。
- data - ImageDataの本体(Uint8ClampedArray… ですがとりあえず配列と思っててください)
- width - ImageDataの幅
- height - ImageDataの高さ
dataは1次元配列で長さはwidth * height * 4
になります。どうなっているかというと、一つのピクセルのRGBAの4つの情報を繰り返し、画像の左上から右へ、右端へついたらひとつ下の段の左端から右へ… といった構造になっています。
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
にあるものとします。コンテキストはそれぞれmainCtx
、maskCtx
とします。mainCanvas
とmaskCanvas
の大きさは同じ、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でも大丈夫です。