JavaScript/Canvas
Canvas API[1] を使ってウェブブラウザにグラフィックスを描いてみましょう。
長方形の描画 編集
- 矩形を描画するプログラムの例
<!DOCTYPE html> <html lang='ja'> <meta charset="utf-8"> <title>Canvas 要素に JavaScript で四角形を描く</title> <canvas id="myCanvas" width="300" height="300">HTML5のCANVAS要素に対応したウェブブラウザで御覧ください。</canvas> <script> const myCanvas.getContext("2d"); context.strokeRect(20, 30, 150, 200); </script> </html>
- 文書型宣言はHTML5を指定します。
- 言語は日本語
- エンコーディングは UTF-8
- タイトルは必須要素です
- 今回の主役のCANVAS要素;サポートしていないウェブブラウザのケアも忘れずに[2]
- JavaScriptのプログラムはSCRIPT要素の中に書きます
- myCanvas は、CANVAS要素のElementオブジェクト[3]、id属性を持つ要素はid属性の値を変数名としてJavaScriptからアクセスできます。継承関係 HTMLCanvasElement < HTMLElement < Element
- 描画コンテキストを得ます[4]。今回は "2d" コンテキストタイプを使いますが他に "wgl" や "wgl2" があります
- strokeRectメソッド[5]は対角の座標を与えて矩形(Rectangle)を描画します
文字列の描画 編集
Canvasには、文字列を画像として描画することも出来ます。
文字列を描画するプログラムの例
<!DOCTYPE html>
<html lang='ja'>
<meta charset="utf-8">
<title>Canvas 要素に JavaScript で文字列を描く</title>
<canvas id="canvasTest" width="300" height="300">HTML5のCANVAS要素に対応したウェブブラウザで御覧ください。</canvas>
<script>
const context = canvasTest.getContext("2d"),
text = "こんにちは Text";
context.font = "32px serif"
context.fillText(text, 30, 50);
</script>
</html>
imageData へのアクセスを使った高速化 編集
マンデルブロ集合の計算(と描画)は、 演算性能を評価するベンチマークテストにも使われるほど膨大な計算時間を必要とするとともに、 可視化するとフラクタルの持つ特有の美しさを持ち頻繁にプログラミングの題材とされます。
JavaScriptで描いてみます。
マンデルブロ集合を描画するプログラム
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='utf-8'>
<title>JavaScriptでマンデルブロー集合を描画してみた</title>
<meta content='CANVAS要素のimageDataに直描するというやや野蛮な方法でマンデルブロ集合を描画してみました。' name='description'>
</head>
<body onload='init()'>
<h1>JavaScriptでマンデルブロー集合を描画してみた</h1>
<canvas height='800' id='canvas' width='800'>Canvas対応ブラウザを使用してください。</canvas>
<div style='text-align: right; width: 800px'>
<span id='laptime'></span>
<a href='#' id='link'>画像を保存</a>
</div>
<script>
function init() {
let t = Date.now();
draw();
document.querySelector("#laptime").innerHTML = `描画処理時間: ${(Date.now() - t) / 1000}秒`;
function draw() {
const canvas = document.querySelector("#canvas"),
context = canvas.getContext("2d"),
{clientWidth, clientHeight} = canvas,
rMin = -2, rMax = 2, rSpan = rMax - rMin,
iMin = -2, iMax = 2, iSpan = iMax - iMin,
nmax = 300, /* 収束判定の上限回数 */
zLimit = 4, /* 収束判定条件 */
n = 5;
let imageData = // Canvas要素から描画用ImageDataを取得
context.createImageData(clientWidth, clientHeight),
index = 0; // buffer のインデックス左上が0
for (let j = 0; j < clientHeight; j++) {
let ci = iMin + iSpan * j / (clientHeight - 1);
for (let i = 0; i < clientWidth; i++) {
const cr = rMin + rSpan * i / (clientWidth - 1);
let r = 0, im = 0;
let k = 0, z2 = 0;
for (k = 0; k < nmax; k++) {
const zr = r * r - im * im + cr,
zi = 2 * r * im + ci;
r = zr;
im = zi;
z2 = r * r + im * im;
if (z2 >= zLimit) {
break;
}
}
if (k == nmax) { /* 収束しなかった */
imageData.data[index++] = z2 * n * 4 // Red
imageData.data[index++] = z2 * n * 16 // Green
imageData.data[index++] = z2 * n * 64 // Bule
}
else { /* 収束した */
imageData.data[index++] = z2 * n * 2 // Red
imageData.data[index++] = z2 * n * 4 // Green
imageData.data[index++] = z2 * n * 8 // Bule
}
imageData.data[index++] = 255; // Alpha
}
}
context.putImageData(imageData, 0, 0);
document.querySelector("#link").setAttribute("href", canvas.toDataURL());
}
}
</script>
</body>
</html>
まとまった長さのプログラムなのでステップ・バイ・ステップの解説は行いませんが、要点だけ。
<body onload='init()'>
Loadが終わるまで(DOMが準備完了に成るまで)Canvasは使えないのでBODY要素のonload属性に初期化メソッドinitを設定します。
{clientWidth, clientHeight} = canvas,
Canvasとは直接関係ありませんが、オブジェクトのプロパティの値を同名の変数に代入するイディオムです。
clientWidth = canvas.clientWidth, clientHeight = canvas.clientHeight,
と同じです。
短くかけるとともに、ミスタイプの入り込む余地がなく元のオブジェクトのプロパティ名の変数の名付けを行う動機づけになります(別の名前の変数にプロパティを代入する構文もあります)。
let imageData = // Canvas要素から描画用ImageDataを取得
context.createImageData(clientWidth, clientHeight),
index = 0; // buffer のインデックス左上が0
Canvas オブジェクトの領域にあるピクセルデータ(ImageData オブジェクト)を得ています[8]。 imageData.dataプロパティはUint8ClampedArrayオブジェクトです。 このUint8ClampedArraオブジェクトは型付き配列の一種で、 U:符号なし int8:8ビット整数う Clamped:? Array:配列、というネーミングで符号なし整数の配列であろうということはわかりますがClamped が問題です。 Clamped は「強制された」と訳されますが、この場合の強制は
255以上の値が要素に代入されたら255に丸める。0未満の値が要素に代入されたら0に丸める。
という挙動を示します。のみならず、
浮動小数点数が代入されたら、端数が0.5より小さいなら切り捨て/端数が0.5より大きいならは切り上げ/端数がちょうど0.5なら切り捨て、と切り上げのうち結果が偶数となる方へ丸める
という動作をします(最近接偶数丸め)。
このUint8ClampedArraオブジェクトのimageData.dataは
[
R(0,0), G(0,0), B(0,0), A(0,0), R(1,0), G(1,0), B(1,0), A(1,0), ... R(width-1,0), G(width-1,0), B(width-1,0), A(width-1,0),
R(0,1), G(0,1), B(0,1), A(0,1), R(1,1), G(1,1), B(1,1), A(1,1), ... R(width-1,1), G(width-1,1), B(width-1,1), A(width-1,1),
:
:
R(0,height-1), G(0,height-1), B(0,height-1), A(0,height-1), ... R(width-1,height-1), G(width-1,height-1), B(width-1,height-1), A(width-1,height-1),
]
という(二次元ではなく)一次元配列で、赤 緑 青 透過率 の4バイトの対が左から右・上から下の順で並んでいます。
context.putImageData(imageData, 0, 0);
document.querySelector("#link").setAttribute("href", canvas.toDataURL());
imageDataに操作を行ったあと、putImageDataメソッドでCanvasで反映します。
次の行のtoDataURLメソッドはCanvasの中のイメージを data: スキームで(ディフォルトでは img/png で)返します。
この事で「名前をつけて画像を保存」を実現しています。
HAMLソース 編集
このプログラムは、HAMLでコーディングしHTMLに変換しました。
!!!
:ruby
CANVAS_DIM, RANGE = 800, 2
%html{:lang => "ja"}
%head
%meta{charset: "utf-8"}/
%title JavaScriptでマンデルブロー集合を描画してみた
%meta{content: 'CANVAS要素のimageDataに直描するというやや野蛮な方法でマンデルブロ集合を描画してみました。', name: 'description'}
%body{onload: "init()"}
%h1 JavaScriptでマンデルブロー集合を描画してみた
%canvas#canvas{height: CANVAS_DIM, width: CANVAS_DIM} Canvas対応ブラウザを使用してください。
%div{:style => "text-align: right; width: #{CANVAS_DIM}px"}
%span#laptime
%a#link{:href => "#"} 画像を保存
:javascript
function init() {
let t = Date.now();
draw();
document.querySelector("#laptime").innerHTML = `描画処理時間: ${(Date.now() - t) / 1000}秒`;
function draw() {
const canvas = document.querySelector("#canvas"),
context = canvas.getContext("2d"),
{clientWidth, clientHeight} = canvas,
rMin = -#{RANGE}, rMax = #{RANGE}, rSpan = rMax - rMin,
iMin = -#{RANGE}, iMax = #{RANGE}, iSpan = iMax - iMin,
nmax = 300, /* 収束判定の上限回数 */
zLimit = 4, /* 収束判定条件 */
n = 5;
let imageData = // Canvas要素から描画用ImageDataを取得
context.createImageData(clientWidth, clientHeight),
index = 0; // buffer のインデックス左上が0
for (let j = 0; j < clientHeight; j++) {
let ci = iMin + iSpan * j / (clientHeight - 1);
for (let i = 0; i < clientWidth; i++) {
const cr = rMin + rSpan * i / (clientWidth - 1);
let r = 0, im = 0;
let k = 0, z2 = 0;
for (k = 0; k < nmax; k++) {
const zr = r * r - im * im + cr,
zi = 2 * r * im + ci;
r = zr;
im = zi;
z2 = r * r + im * im;
if (z2 >= zLimit) {
break;
}
}
if (k == nmax) { /* 収束しなかった */
imageData.data[index++] = z2 * n * 4 // Red
imageData.data[index++] = z2 * n * 16 // Green
imageData.data[index++] = z2 * n * 64 // Bule
}
else { /* 収束した */
imageData.data[index++] = z2 * n * 2 // Red
imageData.data[index++] = z2 * n * 4 // Green
imageData.data[index++] = z2 * n * 8 // Bule
}
imageData.data[index++] = 255; // Alpha
}
}
context.putImageData(imageData, 0, 0);
document.querySelector("#link").setAttribute("href", canvas.toDataURL());
}
}
マンデルブロ集合を描画するプログラム:実行例 編集
まとめ 編集
Canvas は様々なグラフィックインスタンスや効果をJavaScriptから操作する機能があります。 これを使いこなすには DOMの知識が必須です。 また、Canvasのイメージバッファを直接操作する事ができます。 この場合は、型付き配列の特性(特にUint8ClampedArrayの特性)を理解することが、処理速度の向上やコードの見通しの良さに繋がります。
脚注 編集
- ^ https://momdo.github.io/html/canvas.html#the-canvas-element
- ^ https://momdo.github.io/html/dom.html#fallback-content
- ^ https://momdo.github.io/html/canvas.html#htmlcanvaselement
- ^ https://momdo.github.io/html/canvas.html#dom-canvas-getcontext
- ^ https://momdo.github.io/html/canvas.html#dom-context-2d-strokerect
- ^ https://momdo.github.io/html/canvas.html#dom-context-2d-font
- ^ https://momdo.github.io/html/canvas.html#dom-context-2d-filltext
- ^ https://momdo.github.io/html/canvas.html#imagedata