こんにちは、鈴木です。
最近 HTML5 関連の情報を調べているのですが、Canvas は見た目で分かりやすくて楽しいです。
今回は Canvas にフラクタルの定番、マンデルブロー集合とジュリア集合を描いてみたいと思います。
フラクタルを簡単に説明すると、部分を拡大すると全体に似ている(自己相似)などの特徴を持つものです。言葉で説明すると分かりづらいのですが、以下の画像を見て頂けば、フラクタルが持つ不思議な雰囲気を感じて頂けると思います。
左がマンデルブロー集合、右がジュリア集合をそれぞれ視覚化したものです。
Google の画像検索で「fractal」をキーワードに検索すると色々な種類のフラクタル画像を見つけることができます。見るだけでも楽しいので、興味のある方は検索してみてください。
Canvas を使う
Canvas は以下のように canvas 要素で定義します。
1 |
<canvas id="drawable" width="256" height="256"></canvas> |
描画領域の大きさは width と height で指定します。また、Javascript で操作しやすいように "drawable" という id も指定しています。
それではためし描きということで、単純な矩形を描画してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<script> // Canvas を取得する. var canvas = document.getElementById("drawable"); // 描画用のコンテキストを取得する. var context = canvas.getContext("2d"); // 塗りつぶしスタイルを指定する. context.fillStyle = "#000000"; // 指定した範囲を塗りつぶす. context.fillRect(0, 0, 100, 100); </script> |
Canvas に描画するには、document.getElementById("drawable") で作成した Canvas を取得し、canvas.getContext("2d") で描画用のコンテキストを取得します。コンテキストには描画用のメソッドがいくつか提供されていますので、それらを用いて好きに描画することができます。ここでは fillStyle に塗りつぶしスタイルを設定後、fillRect() で矩形を描画しています。
上記コードを実行すると、以下のような画像が得られるはずです。
context には他にも多くの機能があります。例えば、グラデーションで描画することや、ベジェ曲線を描くことができます。
ピクセル操作 API
コンテキストが持つ高レベル API は非常に便利ですが、時にはピクセルデータを直接扱いたいこともあります。そのような時のために、ピクセル操作 API というものが用意されています。
以下のように context.getImageData() で取得できる ImageData オブジェクトを通じてピクセルデータにアクセスすることができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<script> var canvas = document.getElementById("drawable"); var context = canvas.getContext("2d"); // imageData を通じてピクセルデータにアクセスできます. // imageData.width ... 横幅 // imageData.height ... 縦幅 // imageData.data がピクセルデータの配列 (R, G, B, A, R, G, B, A, ... の繰り返し) var imageData = context.getImageData(0, 0, canvas.width, canvas.height); // ... ピクセルデータを操作する. // ピクセルデータを描画する. context.putImageData(imageData, 0, 0); </script> |
コード中のコメントにも記述していますが、imageData.data がピクセルデータの配列です。
配列は 4 要素で 1 ピクセル分の情報を持ち、先頭から順に「赤(R)」、「緑(G)」、「青(B)」、「不透明度(A)」、「赤(R)」、「緑(G)」、「青(B)」、「不透明度(A)」、・・・という順番で情報が収められています。つまり、座標が (x, y) の色情報には以下のようにアクセスします。
1 2 3 4 |
imageData.data[(y * imageData.width + x) * 4 + 0] ... 赤(R) imageData.data[(y * imageData.width + x) * 4 + 1] ... 緑(G) imageData.data[(y * imageData.width + x) * 4 + 2] ... 青(B) imageData.data[(y * imageData.width + x) * 4 + 3] ... 不透明度(A) |
例として、全ピクセルを赤く塗りつぶすコードは以下のようになります。
1 2 3 4 5 6 7 8 |
for(var y = 0; y < imageData.height; y++) { for(var x = 0; x < imageData.width; x++) { imageData.data[(y * imageData.width + x) * 4 + 0] = 0xFF; // 赤 (R) imageData.data[(y * imageData.width + x) * 4 + 1] = 0x00; // 緑 (G) imageData.data[(y * imageData.width + x) * 4 + 2] = 0x00; // 青 (B) imageData.data[(y * imageData.width + x) * 4 + 3] = 0xFF; // 不透明度 (A) } } |
フラクタル画像を描く
さて、ピクセル操作に慣れてきたところで、フラクタル画像を描いてみたいと思います。冒頭でご紹介したマンデルブロー集合とジュリア集合を描きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
<script> function drawMandelbrotSet(imageData) { var minReal = -2.0; var maxReal = +1.0; var minImag = -1.5; var maxImag = +1.5; for(var y = 0; y < imageData.height; y++) { for(var x = 0; x < imageData.width; x++) { var zReal = 0.0; var zImag = 0.0; var cReal = (maxReal - minReal) / imageData.width * x + minReal; var cImag = (maxImag - minImag) / imageData.height * y + minImag; setPixelColor(imageData, x, y, zReal, zImag, cReal, cImag); } } } function drawJuliaSet(imageData) { var minReal = -1.5; var maxReal = +1.5; var minImag = -1.5; var maxImag = +1.5; for(var y = 0; y < imageData.height; y++) { for(var x = 0; x < imageData.width; x++) { var zReal = (maxReal - minReal) / imageData.width * x + minReal; var zImag = (maxImag - minImag) / imageData.height * y + minImag; var cReal = 0.3; var cImag = 0.5; setPixelColor(imageData, x, y, zReal, zImag, cReal, cImag); } } } function setPixelColor(imageData, x, y, zReal, zImag, cReal, cImag) { for(var i = 0; i < 100; i++) { if(zReal * zReal + zImag * zImag >= 4) { imageData.data[(y * imageData.width + x) * 4 + 0] = i * 10 % 256; imageData.data[(y * imageData.width + x) * 4 + 1] = i * 20 % 256; imageData.data[(y * imageData.width + x) * 4 + 2] = i * 30 % 256; imageData.data[(y * imageData.width + x) * 4 + 3] = 0xFF; return; } var real = zReal * zReal - zImag * zImag + cReal; var imag = 2 * zReal * zImag + cImag; zReal = real; zImag = imag; } imageData.data[(y * imageData.width + x) * 4 + 0] = 0x00; imageData.data[(y * imageData.width + x) * 4 + 1] = 0x00; imageData.data[(y * imageData.width + x) * 4 + 2] = 0x00; imageData.data[(y * imageData.width + x) * 4 + 3] = 0xFF; } var canvas = document.getElementById("drawable"); var context = canvas.getContext("2d"); var imageData = context.getImageData(0, 0, canvas.width, canvas.height) // マンデルブロー集合を描く. drawMandelbrotSet(imageData); // ジュリア集合を描く. // drawJuliaSet(imageData); context.putImageData(imageData, 0, 0); </script> |
コードが長くなるため、いくつかの関数に分割しました。
drawMandelbrotSet() がマンデルブロー集合を描画するメソッド、drawJuliaSet() がジュリア集合を描画するメソッドです。ジュリア集合を描きたい場合は「// ジュリア集合を描く」の部分のコメントアウトを外します。
実行すると以下のような画像が得られます。
ピクセル操作 API は、最初に紹介した fillRect() のような高水準 API と比較すると非常に高速です。今回のように 1 ピクセルごとに計算で色を求めるようなケースでは、ピクセル操作 API がおすすめです。