HTMLのCanvasを触れ始めたので備忘録として記事に残しておきます。
本記事で3記事目となります。前記事で触れた内容は割愛するので必要に応じて前記事もご確認ください。
1記事目:
2記事目:
テキスト
以降の各節ではCanvasのテキスト関係について色々触れていきます。
fillTextメソッド
fillTextメソッドでは塗りを設定したテキストを描画します。第一引数には設定するテキストの文字列、第二引数にX座標、第三引数にはY座標を指定します。
<html>
<body>
<canvas id="canvas" width="500" height="350">
</canvas>
</body>
<script type="text/javascript">
let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
context.fillStyle = "#0af";
context.font = "20px Arial";
context.fillText("Hello World!", 50, 50);
</script>
</html>
strokeTextメソッド
strokeTextメソッドはテキストの境界線部分のみ描画を行います。引数はfillTextメソッドと同じです。
<html>
<body>
<canvas id="canvas" width="500" height="350">
</canvas>
</body>
<script type="text/javascript">
let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
context.strokeStyle = "#0af";
context.lineWidth = 1;
context.font = "30px Arial";
context.strokeText("Hello World!", 50, 50);
</script>
</html>
font設定
font属性の設定ではフォントサイズ、フォントファミリー、太さ、斜体設定などが行えます。それぞれが別の属性というわけではなくまとめてfont属性で文字列で指定します。
指定の順番的には斜体などの設定(italicなど) → 太さ設定(boldなど) → フォントサイズ(14pxなど) → フォントファミリー(Arialなど)といった形のようです。斜体と太さ設定などの順番は入れ替えが効くようです。ただし最後のフォントサイズとフォントファミリーの順番などは入れ替えるとうまく反映されなかったりします。また、斜体や太さ設定などは省略が効きますがフォントファミリーの指定を省略するとフォントサイズなども反映されなくなる?ようなので基本的に最後のフォントサイズとフォントファミリーの指定は必要になりそうです。
<html>
<body>
<canvas id="canvas" width="500" height="350">
</canvas>
</body>
<script type="text/javascript">
let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
context.fillStyle = "#0af";
context.font = "20px Arial";
context.fillText("Hello World!", 50, 50);
</script>
</html>
<html>
<body>
<canvas id="canvas" width="500" height="350">
</canvas>
</body>
<script type="text/javascript">
let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
context.fillStyle = "#0af";
context.font = "italic bold 20px Arial";
context.fillText("Hello World!", 50, 50);
</script>
</html>
それぞれ以下のような値の指定ができます。フォント次第で設定しても反映されない設定値も色々あります。
- 斜体などの設定: normal, italic, oblique
- 太さ設定: normal, bold, bolder, lighter, 100, 400, 900などの3桁の数値など
- フォントサイズ: 20pxなどの数値指定, medium、largerなどの指定
- フォントファミリー: Arialなど。複数指定する場合にはコンマ区切りで指定できます。また、スペースなどが含まれるフォント名であればダブルクオーテーションなどで囲みます。例 :
Arial, "Hiragino Kaku Gothic ProN", Meiryo
font属性を設定しなかった場合のデフォルト値は10px, sans-serifとなり他の設定(太字など)はnormalとなります。
textAlign設定
textAlign属性では水平方向のテキストの文字揃えを設定することができます(左揃えや中央揃えなど)。
以下の値を設定することができます。startとendは後述するdirection属性の値を変更すると挙動が変わってきますがdirection属性が本記事執筆時点ではブラウザのサポートがまちまちなので基本的にはleft, center, rightのどれかを使う形になるとは思います(direction属性のブラウザのサポート具合を加味して本記事でもdirection属性などはスキップします)。デフォルトではleftとなります。
- left: 左揃え(デフォルト)になります。
- center: 中央揃えになります。
- right: 右揃えになります。
以下の例では上から左揃え(デフォルト)、中央揃え、右揃えを設定しています。同じX座標を設定していますが描画位置が結構変わります。
<html>
<body>
<canvas id="canvas" width="600" height="350">
</canvas>
</body>
<script type="text/javascript">
let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
context.fillStyle = "#0af";
context.font = "20px Arial";
context.fillText("Lorem ipsum dolor sit amet", 300, 50);
context.fillText("consectetur adipiscing", 300, 80);
context.textAlign = "center";
context.fillText("Lorem ipsum dolor sit amet", 300, 150);
context.fillText("consectetur adipiscing", 300, 180);
context.textAlign = "right";
context.fillText("Lorem ipsum dolor sit amet", 300, 250);
context.fillText("consectetur adipiscing", 300, 280);
</script>
</html>
textBaseline設定
textBaselineは縦軸方向の文字ぞろえを設定します。基準位置に対して上部を合わせるのか(top)、中央を合わせるのか(middle)、下部を揃えるのか(bottom)、アルファベットのベースラインを基準とするのか(alphabetic・デフォルト値)などの設定があります。そのほかにもアルファベットベースではなく漢字などを基準としたideographicやインド系の文字で使われるhangingなどの特殊な設定もあります。
以下の例では左から順番にalphabetic(デフォルト)、top、middle、bottomを設定しています。縦軸の座標指定は同じ値を設定しています。
<html>
<body>
<canvas id="canvas" width="650" height="350">
</canvas>
</body>
<script type="text/javascript">
let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
context.fillStyle = "#0af";
context.font = "20px Arial";
context.fillText("Lorem ipsum", 50, 50);
context.textBaseline = "top";
context.fillText("Lorem ipsum", 200, 50);
context.textBaseline = "middle";
context.fillText("Lorem ipsum", 350, 50);
context.textBaseline = "bottom";
context.fillText("Lorem ipsum", 500, 50);
</script>
</html>
measureTextメソッド
measureTextメソッドでは引数に指定した文字列におけるテキストの長さなどを格納した TextMetrics クラスのインスタンスを返却します。テキスト描画前にテキストの幅などを取得することができます。
TextMetrics クラスは様々な属性を持っていますが、基本的にはwidthの幅の値を参照することが多いかなという印象です。
以下の例では1つ目と2つ目のテキストでは文字列を変更し、2つ目と3つ目では文字サイズを変更してからmeasureTextで文字の幅を取得しコンソールに出力しています。
<html>
<body>
<canvas id="canvas" width="650" height="350">
</canvas>
</body>
<script type="text/javascript">
let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
context.font = "20px Arial";
console.log("Text width 1:", context.measureText("Hello!").width);
console.log("Text width 2:", context.measureText("Lorem ipsum").width);
context.font = "30px Arial";
console.log("Text width 3:", context.measureText("Lorem ipsum").width);
</script>
</html>
テキストの改行について
基本的にCanvasのテキストでは\n
などによる改行や指定幅によるwordwrap的な機能は無いようです。そのため自前で\n
などの部分でテキストを分割してそれぞれでfillTextを呼び出したり、もしくはmeasureTextを使って一定の幅を超えたらfillTextを分けてwordwrapのようにする・・・みたいな自前の対応が必要になります。
変換制御のインターフェイス
以降の節では回転や拡縮・移動などの制御について触れていきます。
rotateメソッド
rotateメソッドは後に続く描画処理に対する回転量を設定できます。引数にはラジアンの値が必要になるため、度で指定したい場合には度の値 * Math.PI / 180
といった計算が必要になります(例 : 45度であれば45 * Math.PI / 180
)。
回転量は呼び出す度に加算されていきます。例えば1回45度設定でrotateメソッドを呼び出してから再度45度設定でrotateメソッドを呼び出すと90度相当になります。
以下の例では3つの四角を描画しています。最初のシアンの四角は回転量0度(デフォルト)、2つ目のマゼンタの四角は回転量45度、最後の黄緑の四角は回転量90度としています。また、座標に関しては回転を使う場合fillRectで指定するよりもtranslateで指定した方が直観的・・・に感じたためそちらを使っています。translateメソッドについては後の節で触れます。
<html>
<body>
<canvas id="canvas" width="650" height="500">
</canvas>
</body>
<script type="text/javascript">
let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
context.fillStyle = "#0af";
context.translate(50, 50);
context.fillRect(0, 0, 100, 30);
context.rotate(45 * Math.PI / 180);
context.fillStyle = "#f0a";
context.fillRect(0, 0, 100, 30);
context.rotate(45 * Math.PI / 180);
context.fillStyle = "#af0";
context.fillRect(0, 0, 100, 30);
</script>
</html>
scaleメソッド
scaleメソッドでは後に続く描画の拡縮設定を行うことができます。第一引数には水平方向の拡縮値、第二引数には垂直方向の拡縮値の指定が必要になります。0.5を指定すれば半分、1を指定すれば変化無し・・・となります。rotateメソッドなどと同様に複数回メソッドを呼んだ場合には拡縮の値は累積されます(例 : 0.5の値で2回呼んだら0.25相当になります)。
以下の3つの四角に対して水平方向にそれぞれ1(デフォルト)、0.5、0.25の拡縮値を設定しています。
<html>
<body>
<canvas id="canvas" width="650" height="500">
</canvas>
</body>
<script type="text/javascript">
let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
context.fillStyle = "#0af";
context.fillRect(0, 0, 100, 30);
context.scale(0.5, 1);
context.fillStyle = "#f0a";
context.fillRect(0, 30, 100, 30);
context.scale(0.5, 1);
context.fillStyle = "#af0";
context.fillRect(0, 60, 100, 30);
</script>
</html>
translateメソッド
translateメソッドは後に続く描画の座標値を変更します。第一引数には加えるX座標、第二引数には加えるY座標を設定します。基本的に各描画のインターフェイスではこのメソッドは回転などの基準点を考慮しないといけない制御を行う場合に組み合わせて使うと便利なことがあります。
translateメソッドも他のrotateなどのメソッドと同様に複数回呼び出した場合には座標が加算されていきます。
以下の例では3つの四角を描画しており、それぞれの四角の描画時の座標は同じですがtranslateメソッドで座標をずらしています。
<html>
<body>
<canvas id="canvas" width="650" height="500">
</canvas>
</body>
<script type="text/javascript">
let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
context.fillStyle = "#0af";
context.fillRect(0, 0, 100, 30);
context.fillStyle = "#f0a";
context.translate(25, 30);
context.fillRect(0, 0, 100, 30);
context.fillStyle = "#af0";
context.translate(25, 30);
context.fillRect(0, 0, 100, 30);
</script>
</html>
画像のデータを直接操作する
以降の節では直接の画像データの操作について触れていきます。
CORS関係のエラーについて
画像データの読み込みと操作などを行うとCORS関係のエラーが発生するインターフェイスが存在します。
Cross origin requests are only supported for protocol schemes
やThe canvas has been tainted by cross-origin data.
といったようなエラーが発生しうる形になります。
ローカルでHTMLを開く場合やサーバーを起動している場合でも別のサーバーの画像を利用しようとすると発生します。単純な画像読み込みや表示などであればエラーは発生しませんが画像操作で画像データの詳細にアクセスしたり加工したりしようとするとそのままだとエラーとなります。
今回はPythonでローカルにサーバーを立てて対応しようと思います。HTMLや画像などが配置されているフォルダに移動して以下のようなコマンドを実行します。諸事情により8000ポートが使用済みだったため8001で立てています(ポート部分は8000もしくはエラーになるようでしたらご調整ください)。
$ python -m http.server 8001
これでブラウザで http://localhost:8001/HTMLファイル名.html といった感じでアクセスできるようになります。
使う画像
以下のフリー素材を利用させていただきます。ローカルに指定の名前で配置されている前提で進みます。
cat.jpg
:
画像データのCanvasへの反映
画像データを操作する前に以下のような事前準備が必要になります。
- Imageクラスのインスタンス化。
- 用意した画像のsrcへの画像の指定と読み込み完了時のイベント設定。
- イベント内でのコンテキストのdrawImageを使った画像のCanvasへの描画
コードにすると以下のような感じになります。
<html>
<body>
<canvas id="canvas" width="650" height="500">
</canvas>
</body>
<script type="text/javascript">
let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
let image = new Image();
image.src = "cat.jpg";
image.onload = function() {
context.drawImage(image, 0, 0, image.width, image.height);
};
</script>
</html>
これでブラウザでlocalhostの立てたサーバーの対象のHTMLにアクセスするとCanvas上に画像が反映されていて画像が表示されていることが確認できます。
画像データの取得
Canvasから特定領域の画像データを取得するにはgetImageData
メソッドを使います(この辺は前回までの記事で触れているため詳細は割愛します)。また、getImageData
メソッドで取得した画像データのインスタンスはdata属性を持っており、そのdata属性が画像データの配列になっています。
試しにdata属性の値をコンソールに出力してみます。
<html>
<body>
<canvas id="canvas" width="650" height="500">
</canvas>
</body>
<script type="text/javascript">
let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
let image = new Image();
image.src = "cat.jpg";
image.onload = function() {
context.drawImage(image, 0, 0, image.width, image.height);
let imageData = context.getImageData(
0, 0, image.width, image.height);
console.log(imageData.data);
};
</script>
</html>
135, 109, 92, 255, 142, 116, 99, 255, ...といった具合の配列になっています。この画像データの配列は以下のような形になっています。
- Uint8とコンソールに出力されている通り、0~255の範囲の整数の色としての値を取ります。
- 多次元の配列などではなく1次元の配列に各色の値が設定されます。
- 配列の値はRGBA(Red, Green, Blue, Alpha)の順番で4値ずつ繰り返し設定されます。つまり先頭のピクセルのR, 先頭のピクセルのG、先頭のピクセルのB、先頭のピクセルのA、2番目のピクセルのR、2番目のピクセルのG、...といったように繰り返す形で画像の値が設定されます。
今回はjpg画像なので透明度(Alpha)の値が最大値(255)になっていることなども配列から読み取れます。
画像変換などを行いたい場合にはこの配列を変更してからコンテキストのputImageData
メソッドで更新後の画像データを設定すれば変換をCanvasに反映することができます。
1行ずつの間隔で画像を暗くしてみる
折角なので画像変換のサンプルとして少し書いていってみます。
やることとしては1行間隔でピクセルを暗くした色を設定していきます。つまり1行ずつ黒い線が画像に入っている・・・ような効果を考えます。
コードにすると以下のような感じです(説明は後で入れます)。
<html>
<body>
<canvas id="canvas" width="650" height="500">
</canvas>
</body>
<script type="text/javascript">
let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
let image = new Image();
image.src = "cat.jpg";
image.onload = function() {
context.drawImage(image, 0, 0, image.width, image.height);
let imageData = context.getImageData(
0, 0, image.width, image.height);
let imageDataLength = imageData.data.length;
for (let i = 0; i < imageDataLength; i += 4) {
if (Math.trunc(i / 4 / image.width) % 2 === 1) {
let red = imageData.data[i];
let green = imageData.data[i + 1];
let blue = imageData.data[i + 2];
red = Math.trunc(red / 3);
green = Math.trunc(green / 3);
blue = Math.trunc(blue / 3);
imageData.data[i] = red;
imageData.data[i + 1] = green;
imageData.data[i + 2] = blue;
}
}
context.putImageData(imageData, 0, 0);
};
</script>
</html>
立てたサーバーでブラウザ上で確認してみると想定した感じに黒い線が1行ずつ入っていることが確認できます。
以下コードの説明となります。
まずは以下の部分でCanvasに設定した画像データを取得しています。画像全体を取得したいのでXやY座標には0を指定し、幅と高さはそのまま画像の幅と高さを指定しています。
let imageData = context.getImageData(
0, 0, image.width, image.height);
1ピクセルごとに処理を行うため以下のようにループを回しています。
画像データの長さ全体は1次元の配列となっているのでimageData.data.length
という記述で取れます。
ループではi += 4
と4ステップずつで設定していますが、これはRGBAで4つ分の値が配列に格納されているため、1ピクセルが4インデックス分使用するためです。
let imageDataLength = imageData.data.length;
for (let i = 0; i < imageDataLength; i += 4) {
1行ずつの間隔で画像データを操作しないので、以下のような条件式を挟んでいます。
画像のピクセル位置は画像の配列がRGBAで4倍となるため、i / 4
という記述で取ることができます。
そのピクセル位置をさらに画像の幅で割っています(/ image.width
)。これによって対象のピクセルが何行目なのかといった値を取ることができます。
さらにその値を % 2 === 1
で剰余を求めています。剰余の値は0か1になるので、値が1かどうかといった指定を入れれば1行ずつ処理を行う(それ以外は処理を行わない)といった制御ができます。
if (Math.trunc(i / 4 / image.width) % 2 === 1) {
参照するのはRGBの3つの値となります。4ずつループのインデックスをずらしていっているので、R(red)はループのインデックス、G(green)はループのインデックス + 1、B(blue)はループのインデックス + 2という指定で値が取れます。A(alpha)は今回使わないのでスキップしています。
let red = imageData.data[i];
let green = imageData.data[i + 1];
let blue = imageData.data[i + 2];
対象の行は暗くしたいので/ 3
で割って小さな値(0に近づけることで黒くする)にしています。
red = Math.trunc(red / 3);
green = Math.trunc(green / 3);
blue = Math.trunc(blue / 3);
そして更新した値を元の画像データの配列に戻しています。
imageData.data[i] = red;
imageData.data[i + 1] = green;
imageData.data[i + 2] = blue;
最後に更新後の画像データの値をputImageData
メソッドでCanvasに設定して完成です。
context.putImageData(imageData, 0, 0);
これ以外にもピクセル操作で様々な処理を行うことができます。CSSでもできたりはしますが例えばモノクロ画像にしたりも行うことができます。色々遊べそうですね。
参考文献・参考サイト