JavaScriptでジュリア集合を描画

2007/11/15

JavaScriptでドット(ピクセル)を使って直接絵を描くのは実は難しいですね。。。 非常に重くて汚い方法になってしまいました。。。

以前「プレコの模様を自動生成するプログラムを書いてみた」のエントリを書いた時に、久々にフラクタルで絵を書いて遊びたくなったので、やってみました。 いつもはPPMで出力しているのですが、それでは楽しくないので、JavaScriptで色々パラメータを入れて遊べるものを作ってみました。 ただ、実用に耐えないぐらい重くなってしまいました。。。 Firefox,Opera用にCanvasと、IE用にVMLで書くべきなのかも知れないと思いましたが、まだそこまでは出来ていません。

今回のJavaScriptは、サイズが1x1のDIVをピクセルに見たてて、その背景を塗りつぶしました。 最初はIEで高さ1pxのDIVが作れなかったのですが「font-size:1px」と同時に「height:1px」にするとIEでも1x1のピクセルが生成できました。

この手法の問題は、メモリと処理時間が莫大になることです。 IEとFirefoxでは「重過ぎるけどまだ続ける?」といった内容のダイアログが出てしまいます。 Operaでは結構順調に動いてくれます。 Operaは200x200ぐらいのサイズにしても、動いてくれるようです。 私の手元の環境では、FirefoxとIEはキャンバスサイズを拡大すると絶望的に遅くなってしまうようです。

はからずしもブラウザの負荷テストのようになってしまいました。

スクリプト

実数開始位置 :
実数終了位置 :
虚数開始位置 :
虚数終了位置 :
Aの実数部分 :
Aの虚数部分 :

幅 : , 高さ :

ジュリア集合解説

このスクリプトは、以下の数式を実行して色をつけていったものです。 この数式のzとAは実数部と虚数部を持つ複素数です。 Aは定数です。

上記数式を複素数に対して行います。 作図する平面は以下の図のように、0番目のzが取る複素数の値のうち、横軸を実数部とし、縦軸を虚数部とします。

平面上の各z_0に対して、この計算を繰り返し行っていきます。 この計算を行った結果が特定の値を超えた場合、その計算が発散したとします。 そして、発散するまでにかかった回数で平面上に色をつけていきます。 規定の回数まで行っても発散しない場合は、発散しなかったとして色を塗りません。 (ただし、今回のプログラムでは黒を塗っています。)

実数部の開始位置と終了位置を調整すると、画像全体のズーム率を調整できます。 定数Aの値を変更していくと、模様が変わります。 色々試して見てください。

何を入れていいのか解らない場合などには、以下の値などがお勧めです。

  • 実数 -0.5 〜 0.5、虚数 -0.5 〜 0.5、A実数 -0.2、A虚数 -0.68
  • 実数 -0.2 〜 0.2、虚数 -0.2 〜 0.2、A実数 -1.4、A虚数 0.001
  • 実数 -0.5 〜 0.5、虚数 -0.5 〜 0.5、A実数 -0.4、A虚数 -0.61

ソースコード

以下が今回のスクリプトのソースコードです。 試しやすいようにBODYなどのHTMLタグつきにしてあります。



<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
  </head>
  <body>

    <script type="text/javascript">
    //<![CDATA[

    // キャンバスの初期化が終わってるかどうか
    var g_bIsInit = false;

    var g_nWidth = 100;
    var g_nHeight = 100;
    var g_canvas;

    // pixelに対応するDIVへのキャッシュ
    var pixels;

    // サイズ変更最大値
    var MAX_WIDTH = 500;
    var MAX_HEIGHT = 500;

    // 発散まで繰り返す回数
    var TIMES   = 256;

    // 色に関する数値を初期化
    var color = new Array(TIMES);
    for (var i=0; i<256; i+=4) {
      var tmp = i.toString(16);
      if (i < 16) {
        tmp = '0' + tmp;
      }
      color[i/4] = '#' + '00' + '00' + tmp;
    }
    for (var i=0; i<256; i+=4) {
      var tmp = i.toString(16);
      if (i < 16) {
        tmp = '0' + tmp;
      }
      var i2 = 255 - i;
      var tmp2 = i2.toString(16);
      if (i2 < 16) {
        tmp2 = '0' + tmp2;
      }
      color[64 + (i/4)] = '#' + '00' + tmp + tmp2;
    }
    for (var i=0; i<256; i+=4) {
      var tmp = i.toString(16);
      if (i < 16) {
        tmp = '0' + tmp;
      }
      color[128 + (i/4)] = '#' + tmp + 'ff' + '00';
    }
    for (var i=0; i<256; i+=4) {
      var tmp = i.toString(16);
      if (i < 16) {
        tmp = '0' + tmp;
      }
      color[192 + (i/4)] = '#' + tmp + 'ffff';
    }

    function initCanvas() {
      g_canvas = document.getElementById('canvas');

      pixels = new Array(g_nWidth);
      for (var x=0; x<g_nWidth; x++) {
        pixels[x] = new Array(g_nHeight);
      }

      for (var y=0; y<g_nHeight; y++) {
        for (var x=0; x<g_nWidth; x++) {
          var element = document.createElement('div');

          element.setAttribute('id', x + 'x' + y);
          element.style.position = 'absolute';
          element.style.top = y + 'px';
          element.style.left = x + 'px';
          element.style.fontSize = '1px';
          element.style.border = 'none';
          element.style.padding = '0px';
          element.style.margin = '0px';
          element.style.width = '1px';
          element.style.height = '1px';
          element.style.backgroundColor = '#000000';

          g_canvas.appendChild(element);
          pixels[x][y] = element;
        }
      }

      g_bIsInit = true;
    }

    // pixelを塗りつぶす
    function drawPixel(x, y, c) {
      // 1x1サイズのDIVの背景をstyleで変更
      // colorに入っている各要素は例えば #0000ff のような値
      pixels[x][y].style.backgroundColor = color[c];
    }

    //
    // フラクタルを描画する
    //
    function drawCanvas() {

        // 入力からパラメータを受け取る

        var R_START = parseFloat(document.fractal.elements[0].value, 10);
        if (R_START == NaN) {
          alert('r start is NaN');
          return;
        }
        var R_END   = parseFloat(document.fractal.elements[1].value, 10);
        if (R_END == NaN) {
          alert('r end is NaN');
          return;
        }
        var I_START = parseFloat(document.fractal.elements[2].value, 10);
        if (I_START == NaN) {
          alert('i start is NaN');
          return;
        }
        var I_END   = parseFloat(document.fractal.elements[3].value, 10);
        if (R_END == NaN) {
          alert('i end is NaN');
          return;
        }
        var A_R     = parseFloat(document.fractal.elements[4].value, 10);
        if (A_R == NaN) {
          alert('A.r is NaN');
          return;
        }
        var A_I     = parseFloat(document.fractal.elements[5].value, 10);
        if (A_I == NaN) {
          alert('A.i is NaN');
          return;
        }

        // キャンバスの初期化が終わっていなければ初期化
        if (g_bIsInit == false) {
          initCanvas();
        }

        // ジュリア集合を計算
        var d_r = (R_END - R_START) / g_nWidth;
        var d_i = (I_END - I_START) / g_nHeight;

        var r0, r1, i0, i1;

        for (var x=0; x<g_nWidth; x++) {
          for (var y=0; y<g_nHeight; y++) {
            r0 = R_START + d_r*x;
            i0 = I_START + d_i*y;

            var bDraw = false;

            for (var k=0; k<TIMES; k++) {
              r1 = r0*r0 - i0*i0 + A_R;
              i1 = 2.0*r0*i0 + A_I;

              if (r1*r1 + i1*i1 > 4.0) {
                // 発散したらピクセルを塗りつぶす
                drawPixel(x, y, k);
                bDraw = true;
                break;
              }

              r0 = r1;
              i0 = i1;
            }

            // 発散しなかった場合
            if (!bDraw) {
               drawPixel(x, y, 0);
            }
          }
        }
    }

    // キャンバスのサイズを変更する
    function changeCanvasSize() {
      var w = parseInt(document.canvasSizeForm.elements[0].value, 10);
      var h = parseInt(document.canvasSizeForm.elements[1].value, 10);

      if (w < 1) {
        alert("WIDTH : [" + w + "] is too small");
        return;
      }
      if (w > MAX_WIDTH) {
        alert("WIDTH : [" + w + "] is too large");
        return;
      }
      if (h < 1) {
        alert("HEIGHT : [" + h + "] is too small");
        return;
      }
      if (h > MAX_HEIGHT) {
        alert("HEIGHT : [" + h + "] is too large");
        return;
      }

      if (g_bIsInit == true) {
        // 一度初期化が行われた後である場合、以前の資源を解放
        for (var x=0; x<g_nWidth; x++) {
          for (var y=0; y<g_nHeight; y++) {
            g_canvas.removeChild(pixels[x][y]);
            pixels[x][y] = null;
          }
          pixels[x] = null;
        }
        pixels = null;

        g_bIsInit = false;

        g_canvas.style.width = w + 'px';
        g_canvas.style.height = h + 'px';
      } else {
        // 初期化が行われていない場合、サイズだけ変更
        var canv = document.getElementById('canvas');
        canv.style.width = w + 'px';
        canv.style.height = h + 'px';
      }

      g_nWidth = w;
      g_nHeight = h;

      // ついでにフラクタルを描画
      drawCanvas();
    }

    //]]>

    </script>

    <div id="canvas"
        style="position:relative; width:100px; height:100px; border:1px solid #000000;"></div>

    <P>
    <form name="fractal">
    <table border="0">
    <tr><td>実数開始位置</td><td> : </td><td><input name="r_start" value="-0.5"></input></td></tr>
    <tr><td>実数終了位置</td><td> : </td><td><input name="r_end" value="0.5"></input></td></tr>
    <tr><td>虚数開始位置</td><td> : </td><td><input name="i_start" value="-0.5"></input></td></tr>
    <tr><td>虚数終了位置</td><td> : </td><td><input name="i_end" value="0.5"></input></td></tr>
    <tr><td>Aの実数部分</td><td> :  </td><td><input name="a_r" value="-0.3"></input></td></tr>
    <tr><td>Aの虚数部分</td><td> : </td><td><input name="a_i" value="-0.63"></input></td></tr>
    </table>
    <input type="button" value="フラクタルを描画" onclick="drawCanvas()"></input>
    </form>
    </P>

    <P>
    <form name="canvasSizeForm">
    幅 : <input name="cwidth"></input>, 高さ : <input name="cheight"></input>
    <input type="button" value="描画ウィンドウのサイズを変更" onclick="changeCanvasSize()"></input>
    </form>
    </P>
  </body>
</html>


最後に

何度もやっていると動かなくなったりしますが、まあ、楽しんでいただければ幸いです。

うーん。JavaScriptは難しいですね。 どなたか、添削してもらえたらうれしいです。 まだまだ修行の足りなさを痛感する今日この頃です。

最近のエントリ

過去記事

過去記事一覧

IPv6基礎検定

YouTubeチャンネルやってます!