「JavaScript/Canvas」の版間の差分

削除された内容 追加された内容
Ef3 (トーク | 投稿記録)
M Ef3 がページ「JavaScript/画像表示」を「JavaScript/Canvas」に移動しました: JavaScript/画像表示 だとIMG要素を想起してしまう
Ef3 (トーク | 投稿記録)
→‎imageData へのアクセスを使った高速化: JavaScriptでマンデルブロ集合を描いてみた
52 行
</ol>
 
== imageData へのアクセスを使った高速化 ==
[[w:マンデルブロ集合|マンデルブロ集合]]の計算(と描画)は、
演算性能を評価する[[w:ベンチマーク|ベンチマークテスト]]にも使われるほど膨大な計算時間を必要とするとともに、
可視化すると[[w:フラクタル|フラクタル]]の持つ特有の美しさを持ち頻繁にプログラミングの題材とされます。
 
JavaScriptで描いてみます。
 
'''マンデルブロ集合を描画するプログラム'''
<source lang="html5" highlight="8,23,29-31" line>
<!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.getElementById("laptime").innerHTML = `描画処理時間: ${(Date.now() - t) / 1000}秒`;
function draw() {
const canvas = document.getElementById("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.getElementById("link").setAttribute("href", canvas.toDataURL());
}
}
</script>
</body>
</html>
</source>
まとまった長さのプログラムなのでステップ・バイ・ステップの解説は行いませんが、要点だけ。
 
<source lang="javascript" start=8 line>
<body onload='init()'>
</source>
Loadが終わるまで(DOMが準備完了に成るまで)Canvasは使えないのでBODY要素のonload属性に初期化メソッドinitを設定します。
 
<source lang="javascript" start=23 line>
{clientWidth, clientHeight} = canvas,
</source>
Canvasとは直接関係ありませんが、オブジェクトのプロパティの値を同名の変数に代入するイデオムです。
<source lang="javascript" start=23 line>
clientWidth = canvas.clientWidth, clientHeight = canvas.clientHeight,
</source>
と同じです。<br>
短くかけるとともに、ミスタイプの入り込む余地がなく元のオブジェクトのプロパティ名の変数の名付けを行う動機づけになります(別の名前の変数にプロパティを代入する構文もあります)。
 
<source lang="javascript" start=29 line>
let imageData = // Canvas要素から描画用ImageDataを取得
context.createImageData(clientWidth, clientHeight),
index = 0; // buffer のインデックス左上が0
</source>
Canvas オブジェクトの領域にあるピクセルデータ(ImageData オブジェクト)を得ています<ref>https://momdo.github.io/html/canvas.html#imagedata</ref>。
imageData.dataプロパティはUint8ClampedArrayオブジェクトです。
このUint8ClampedArraオブジェクトは[[JavaScript/型付き配列|型付き配列]]の一種で、
U:符号なし int8:8ビット整数う Clamped:? Array:配列、というネーミングで符号なし整数の配列であろうということはわかりますがClamped が問題です。
Clamped は「強制された」と訳されますが、この場合の'''強制'''は
255以上の値が要素に代入されたら255に丸める。0未満の値が要素に代入されたら0に丸める。
という挙動を示します。のみならず、
浮動小数点数が代入されたら、端数が0.5より小さいなら切り捨て/端数が0.5より大きいならは切り上げ/端数がちょうど0.5なら切り捨て、と切り上げのうち結果が偶数となる方へ丸める
という動作をします([[w:端数処理#偶数への丸め(round_to_even)|最近接偶数丸め]])。
 
このUint8ClampedArraオブジェクトのimageData.dataは
<source lang="javascript">
[
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),
]
</source>
という(二次元ではなく)一次元配列で、赤 緑 青 透過率 の4バイトの対が左から右・上から下の順で並んでいます。
<source lang="javascript" start=61 line>
context.putImageData(imageData, 0, 0);
document.getElementById("link").setAttribute("href", canvas.toDataURL());
</source>
imageDataに操作を行ったあと、putImageDataメソッドでCanvasで反映します。<br>
次の行のtoDataURLメソッドはCanvasの中のイメージを data: スキームで(ディフォルトでは img/png で)返します。
この事で「名前をつけて画像を保存」を実現しています。
 
<!-- HAMLで書いたソース
!!!
:ruby
CANVAS_DIM, RANGE = 512、2
%html{lang: "ja"}
%head
%meta{charset: "utf-8"}
%title JavaScriptでマンデルブロ集合を描いてみた
%meta{name: "description", content: "CANVAS要素のimageDataに直描するというやや野蛮な方法でマンデルブロ集合を描画してみました。"}
%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.getElementById("laptime").innerHTML = `描画処理時間: ${(Date.now() - t) / 1000}秒`;
function draw() {
const canvas = document.getElementById("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.getElementById("link").setAttribute("href", canvas.toDataURL());
}
}
-->
== まとめ ==
Canvas は様々なグラフィックインスタンスや効果をJavaScriptから操作する機能があります。