Canvasでタートルオブジェクトを作ってドラゴンカーブを描く

JavaScriptの習作として、Cavansを使ってドラゴンカーブを描いてみた。若干なにかが違うような気がしつつ……。

ドラゴンカーブは、長い帯を半分に折って、さらに半分に折って、さらに……という操作を続けて、最後に折り目がそれぞれ直角になるように開いた図形。分かったような分からないような定義というか説明だけど、ある線分「|」を「>」というように真ん中で直角に折って、そしてそれぞれの2本をさらに直角に折るということを繰り返すことで作ると考えると分かりやすい。

次数をorderとすると、以下の関数でドラゴンカーブを作る描画操作を文字列として生成できる。Wikipediaによれば、こうした再帰的に操作の次数を上げていくことで幾何学図形を生成するのは、L-Systemという名称で呼ばれているらしい。

    function generateDragon(order) {
	var command = "F";
	for (var i = 1; i < order; i++) {
	    // Dragon curve generation rule
	    command = command.replace(/F/g, "F+F-F")
	}
	return command;
    }

Fは線を引っ張るという操作で、+は右に曲がる、-は左に曲がるという操作に相当する。すると、ドラゴンカーブは次元が上がるに従って、

F
F+F-F
F+F-F+F+F-F-F+F-F
F+F-F+F+F-F-F+F-F+F+F-F+F+F-F-F+F-F-F+F-F+F+F-F-F+F-F

というように、複雑な感じなっていくのが分かる。Canvasでは、カーソルを動かすようにして連続して折れ線を描けないし、線分の描画方向なんてことも覚えておいてくれないので、自前で簡易タートルグラフィックスを定義してみた。

    var Turtle = function (x, y, vector) {
	this.x = x;
	this.y = y;
	ctx.moveTo(x, y);
	this.step = 20;
	this.vector = vector;
    };
    $.extend(Turtle.prototype, {
	moveForward : function() {
	    this.x = this.x + this.vector[0];
	    this.y = this.y + this.vector[1];
	    ctx.lineTo(this.x, this.y);
	    ctx.stroke();
	},
	turnRight : function() {
	    this.vector = [-this.vector[1], this.vector[0]];
	},
	turnLeft : function() {
	    this.vector = [this.vector[1], -this.vector[0]];
	}
    });

JavaScriptはthisが鬱陶しい。なるほど、それでCoffeeScriptは@を使うのか。

Canvasでは、どっちにしてもコンテクストは複数持てないことが分かったので(なんだか残念な仕様)、ちょっと気持ち悪いけど、いきなりグローバルなコンテクストを操作している。Turtleオブジェクトも、どうせシングルトン的にしか生成しないのだから、prototypeにメソッドを書く必要はないのだけど、ちょっとprototypeって打ってみたかった。オブジェクト指向ってデータと手続きがまとめられるのが嬉しいという話だけど、どうもJavaScriptの文法では、その定義がパラパラっとバラけるのが奇妙な感じ。jQueryやUnderscore.jsにあるextendを使うと多少ましになる。

var Klass = function(x, y) {
    this.x = x;
    this.y = y;
}
Klass.prototype.methodA: function() {console.log(this.x)};
Klass.prototype.methodB: function() {console.log(this.y)};

というのは、

var Klass = function(x, y) {
    this.x = x;
    this.y = y;
}
$.extend(Klass.prototype, {
    methodA: function() {console.log(this.x)},
    methodB: function() {console.log(this.y)}
}

というのと等価。RubyならHash#mergeに相当するものが、なんで、jQueryもUnderscore.jsも、extendという名前なんだろうかと、ちょっと不思議に思ったけど、JavaScriptのObjectはハッシュそのもので、クラスの入れ物でもあるから、これはRubyでいえばObject#extendということだ。こういう名称って言語ごとに方言や流儀がいろいろあるけど、なるほどオブジェクトの拡張というのは、RubyJavaScriptもextendなのかーと妙に納得。

以下がドラゴンカーブを描くJavaScript

<!DOCTYPE html>
<html>
  <head>
    <title>Dragon curve</title>
    <script src="../jquery.js" type="application/javascript"></script>
    <script src="dragon.js" type="application/javascript"></script>
    <style type="text/css">
      canvas {
      border: 1px solid black;
      background-color: #eeeeee;
      }
    </style>
  </head>
  <body>
    <h1>Dragon curve</h1>
    <canvas width="640" height="480" id="dragon"></canvas>
    <form>
      <p>Order: </p>
      <input type="text" id="order" value="7" />
      <input type="button" id="button" value="draw" />
    </form>
  </body>
</html>
$(function(){
    var canvas = $("#dragon")[0],
	ctx = canvas.getContext("2d");

    $.extend(ctx, {
	strokeStyle: "rgb(255, 127, 0)",
	lineWidth: 1
    });

    var Turtle = function (x, y, vector) {
	this.x = x;
	this.y = y;
	ctx.moveTo(x, y);
	this.step = 20;
	this.vector = vector;
    };
    $.extend(Turtle.prototype, {
	moveForward : function() {
	    this.x = this.x + this.vector[0];
	    this.y = this.y + this.vector[1];
	    ctx.lineTo(this.x, this.y);
	    ctx.stroke();
	},
	turnRight : function() {
	    this.vector = [-this.vector[1], this.vector[0]];
	},
	turnLeft : function() {
	    this.vector = [this.vector[1], -this.vector[0]];
	}
    });

    $('#button').on('click', function() {draw()});

    function draw () {
	var order = $('#order').val();
	var command = generateDragon(order);
	canvas.width = canvas.width;  // clear the canvas
	ctx.translate(canvas.width / 2, canvas.height / 2);
	for (var i = 0; i < 4; i++) {
	    drawSequence(command, canvas.width / (order * 30));
	    ctx.rotate(Math.PI * (90 / 180));
	}
    }

    function drawSequence(command, f) {
	var ary = command.split('');
	var t = new Turtle(0, 0, [5, 0]);

	ctx.beginPath();
	while (ary.length > 0) {
	    switch(ary.shift()) {
	    case 'F':
		t.moveForward();
		break;
	    case '+':
		t.turnRight();
		break;
	    case '-':
		t.turnLeft();
		break;
	    }
	}
    }

    function generateDragon(order) {
	var command = "F";
	for (var i = 1; i < order; i++) {
	    // Dragon curve generation rule
	    command = command.replace(/F/g, "F+F-F")
	}
	console.log(command);
	return command;
    }
});

それにしても、jQueryが便利すぎて、つい使ってしまう。