Canvasでライフゲームを作ってみた

Canvasでライフゲームを作ってみたライフゲームが何かは知っていたし、いくつかの有名なパターンの動きも見たことがあったはずだけど、自分で適宜セルの生死をトグルしたり、パターンをロードしたりできると、思ったよりも面白かった。そして、iPadAndroidでも、全く同じコードがちゃんと動くというのが改めてすごいなと思ったりした。

JavaScriptにはクラスはないし、オブジェクトはある意味シングルトン。今の場合、クラスもプロトタイプも不要だろうと思って、ゲームのグリッドはWorldオブジェクト、CanvasのグリッドはGridオブジェクトに突っ込んだ。GridはWorldからしか呼ばなくて、Canvas周りの座標変換やらは全部Gridに任せているのだけど、可視性のコントロールはどうやってやればいいんだろう。

動的にサイズが決まる2次元配列を作るのが、どうも良くやり方が分からない。

	setup: function(size) {
	    this.size = size;
	    this.land = (function(size) {
		var a = [];
		for (var i = 0; i < size; i++){
		    a.push(new Array(size));
		}
		return a;
	    })(size);

と、forで1つ1つ配列をプッシュ。なんか、、、つらい。2次元配列を使ったのがそもそもの間違いなのか、jQueryの$.mapの挙動にもびっくりした。2次元配列をmapで操作するとき、Rubyなら、

[[1, 2], [3, 4]].map {|e| [e[0] * 2, e[1] * 2]}
 => [[2, 4], [6, 8]]

と書くだろうと思って、jQueryで以下のように書くとflattenされる。あれ?

var arr = [[1, 2], [3, 4]];
$.map(arr, function (e) { return new Array(e[0]*2, e[1]*2)}));
// => [2, 4, 6, 8]

for i in itemsで、関数の引数にインデックスが渡るのも不便な気がするのだけど、そんなもんなのか。ていうか、Arrayのときにfor in は禁句らしい。for inはプロパティを全部走査するので、Array.prototypeにプロパティが追加されていたら、

> Array.prototype.hoge = function() {};
[Function]
> for (var i in "abc".split("")) {console.log(i)}
0
1
2
hoge
undefined
>

のようになってしまう。うがが。

ひと通り書き終わってから、ネット上にあるCanvasによるライフゲームJavaScript実装をあれこれ眺めていて思ったけど、多くの人はもっとCライクに書いているように思った。ぼくはどうも名前を付けたがる。例えばWorld.landに2次元配列でセルの生死の状態を入れているけど、そのセルの操作は、

	liveCell: function(cell) {
	    this.land[cell[0]][cell[1]] = true;
	},

	// take 2 dimentional array as an argument
	liveCells: function(cells) {
	    for (var i in cells)
		this.liveCell(cells[i]);
	},
:
:
    for (var i in offsprings) this.liveCell(offsprings[i]);

と名前を付けている。これは、

    for (var i in offsprings) this.land[offsprings[i][0][offsprings[i][1]] = true;

と書けばいいことでもある。

パターンデータの定義を除いたコードは以下。パターン定義は、パターンファイルをRubyで処理してJavaScriptの二次元配列リテラルなんかを生成した。結局、Rubyかよ、みたいな……。

$(function() {
    var canvas = document.getElementById('board'),
	ctx = canvas.getContext('2d');

    var World = {
	setup: function(size) {
	    this.size = size;
	    this.countupGeneration(0);
	    // create a 2 dimentional array
	    this.land = (function(size) {
		var a = [];
		for (var i = 0; i < size; i++){
		    a.push(new Array(size));
		}
		return a;
	    })(size);
	    Grid.init(this.size);
	    this.draw();
	},

	clearCells: function() {
	    this.setup(this.size);
	},

	killCell: function(cell) {
	    this.land[cell[0]][cell[1]] = false;
	},

	liveCell: function(cell) {
	    this.land[cell[0]][cell[1]] = true;
	},

	// take 2 dimentional array as an argument
	liveCells: function(cells) {
	    for (var i in cells)
		this.liveCell(cells[i]);
	},

	toggleCellCoord: function(xpos, ypos) {
	    var x, y;
	    x = Math.floor(xpos / (canvas.width  / this.size));
	    y = Math.floor(ypos / (canvas.height / this.size));
	    if (this.land[x][y])
		this.killCell([x, y]);
	    else
		this.liveCell([x, y]);
	    this.draw();
	},

	draw: function() {Grid.refresh();},

	countupGeneration: (function() {
	    var gen = 0;
	    return (function(n) {
		gen = (n == 0) ? 0 : gen + 1;
		$("#generation").html(gen);
	    })
	})(),

	tick: function () {
	    var cells = [],
	        offsprings = [],
	        deadpool = [],
	        n;
	    for (var x = 0; x < this.size; x++) {
		for (var y = 0; y < this.size; y++) {
		    n = 0;
		    cells = this.neighbors([x, y]);
		    for (var i in cells) {
			if (this.land[cells[i][0]][cells[i][1]]) n++;
		    }
		    if (this.land[x][y]) {
			if ((n <= 1) || (n >= 4)) deadpool.push([x, y]);
		    } else {
			if (n === 3) offsprings.push([x, y]);
		    }
		}
	    }
	    for (var i in offsprings) this.liveCell(offsprings[i]);
	    for (var i in deadpool)   this.killCell(deadpool[i]);
	    this.offsprings = [];
	    this.deadpool = [];
	    this.countupGeneration();
	    Grid.refresh();
	},

	// stitch the edges so that this world has no ends.
	wrapEdges: function (pos) {
	    var x, y;
	    x = pos[0] % this.size;
	    y = pos[1] % this.size;
	    if (x === -1) x = this.size - 1;
	    if (y === -1) y = this.size - 1;
	    return [x, y];
	},

	neighbors: function (cell) {
	    var d = [[0, -1], [0, 1], [-1, 0], [1, 0],
		    [-1, -1], [1, -1], [1, 1], [-1, 1]];
	    var n = [];
	    var x, y;
	    for (var i in d) {
		x = cell[0] + d[i][0];
		y = cell[1] + d[i][1];
		n.push(this.wrapEdges([x, y]));
	    }
	    return n;
	}
    }

    var Grid = {
	init: function(numberOfCells) {
	    this.x = this.y = numberOfCells;
	    this.w = canvas.width;
	    this.h = canvas.height;
	    this.size = this.w / this.x;
	},
	gridDraw: function() {
	    ctx.strokeStyle = "rgb(230, 230, 230)";
	    for(var x = 0; x < this.w; x += this.size) {
		ctx.beginPath();
		ctx.moveTo(x, 0);
		ctx.lineTo(x, this.h);
		ctx.stroke();
	    }
	    for(var y = 0; y < this.w; y += this.size) {
		ctx.beginPath();
		ctx.moveTo(0, y);
		ctx.lineTo(this.w, y);
		ctx.stroke();
	    }
	},
	refresh: function() {
	    canvas.width = canvas.width;
//	    this.gridDraw();
	    var x, y;
	    ctx.fillStyle = "rgb(0, 0, 0)";
	    for (var x = 0; x < World.size; x++) {
		for (var y = 0; y < World.size; y++) {
		    if (World.land[x][y]) {
			ctx.fillRect(x * this.size, y * this.size, this.size, this.size);
		    }
		}
	    }
	}
    };

    // client code
    var Shape = {
	glider: [[0, 0], [1, 0], [2, 0], [0, 1], [1, 2]],
	blinker: [[5, 5], [6, 5], [7, 5]]
    };
    var timerId;
    var size = 80, latency;
    var setParams = function() {
	latency = document.getElementById('latency').value;
    };

    // set the initial state

    setParams();
    World.setup(30);
    World.liveCells(Shape.blinker);
    World.liveCells(Shape.glider);
    World.draw();

    // setup buttons

    $("#start").click(function() {
	if (!timerId) {
	    timerId = (function() {
		setParams();
		return setInterval(function(){ World.tick();}, latency);
	    })();
	}
    });
    $("#stop").click(function() {
	clearInterval(timerId);
	timerId = undefined;
    });
    $("#clear").click(function() {
	World.clearCells();
    });
    $("#board").click(function(e) {
	World.toggleCellCoord(e.clientX - canvas.offsetLeft,
			      e.clientY - canvas.offsetTop);
    });

    // setup pattern buttons

    $.each(Patterns, function(idx, val) {
	$("#patterns").prepend("<input type=\"button\" value=\""
			      + val["name"] + "\" "
			      + "id=\"shape" + idx + "\""
			      + ">");
	$("#shape" + idx).each(function() {
	    $(this).click(function() {
		var data = [];
		var w = h = max = 0;
		for (i in val["data"]) {
		    w = val["data"][i][0] > w ? val["data"][i][0] : w;
		    h = val["data"][i][1] > h ? val["data"][i][1] : h;
		}
		World.setup(size);
		$.each(val["data"], function(i) {
		    data.push([val["data"][i][0] + Math.floor((size - w) / 2),
			       val["data"][i][1] + Math.floor((size - h) / 2)]);
		});
 		World.liveCells(data);
		World.draw();
	    });
	});
    });
});

JavaScriptのslice(0)はRubyのObject#dup相当のイディオム

CoffeeScriptのサンプルとして、doccoのソースコードを眺めていたら、以下のような謎のslice(0)があった。

# Run the script.
# For each source file passed in as an argument, generate the documentation.
sources = process.ARGV.sort()
if sources.length
  ensure_directory 'docs', ->
    fs.writeFile 'docs/docco.css', docco_styles
    files = sources.slice(0)
    next_file = -> generate_documentation files.shift(), next_file if files.length
    next_file()

arr.slice(0)は、イデックスがゼロのところから残り全部を切り出すということで、これは単にオブジェクトを複製しているだけらしい。破壊的操作をしたくないケースや、配列っぽいけど配列じゃないargumentsなどのオブジェクトを配列に変換するなどの目的で使われる。RubyのArray#sliceはArray#[]なので、引数が1つだと、1要素だけ引っ張ってくるので動作が違う。

jQueryには、こんなのがある。

if ( !all ) {
	namespaces = event.type.split(".");
	event.type = namespaces.shift();
	namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join("\\.(?:.*\\.)?") + "(\\.|$)");
}

sort()が破壊的だから、その前に複製している。そうか、JavaScriptのarray.sort()は破壊的なのか……。と思って、MDNのArray.prototype.sort()の解説を見たら、こう書いてあった。

If compareFunction is not supplied, elements are sorted by converting them to strings and comparing strings in lexicographic ("dictionary" or "telephone book," not numerical) order. For example, "80" comes before "9" in lexicographic order, but in a numeric sort 9 comes before 80.

比較関数を渡さないと、文字列に変換して辞書順オーダーでソート! まじか。

$ node
> a = [1, 8, 12]
> a.sort()
[ 1, 12, 8 ]
> a
[ 1, 12, 8 ]

オー、ノー……。

$ coffee
coffee> a = [12,8,1]
[ 12, 8, 1 ]
coffee> a.sort((a,b) -> a - b)
[ 1, 8, 12 ]

なるほどなぁ。

Emacsの中からrdefsを呼ぶ

Rubyのコードを読んでるとき、全体の構造をぱっと把握したいことがある。RDocやSDoc、あるいはYARDといったドキュメント生成ツールもいいけど、こういうツールはソースコードを読むためのものというよりも、APIを調べるためのものなので、クラスやメソッドが記述順やファイル配置とは無関係に整理されてしまう。使うときにはいいけど、学習目的で読むというときには素に近いほうがいい。

コードの構成そのままに、単にクラスやモジュール、メソッドの定義を引っ張り出して眺めたいときには、青木峰郎氏が作ったrdefsが使える。これをEmacsで呼べるように以下のように書いた。

;;; extract-ruby-defs with rdefs
(defun ruby-defs ()
  (interactive)
  (let ((obuf (current-buffer)))
    (setq buf (get-buffer-create "*rdefs*"))
    (set-buffer buf)
    (erase-buffer)
    (set-buffer obuf)
    (call-process-region
     (point-min)
     (point-max) "/path/to/bin/rdefs" nil buf)
    (switch-to-buffer-other-window buf)
    (ruby-mode)))

RailsでTwitter Bootstrapを使う

Twitter BootstarpRailsで使う場合、Twitter Bootstrap for Rails 3.1 Asset Pipelineというgemが便利。

便利なのだけど、CSSやHTMLの生成に使うテンプレート言語のデフォルトが好みじゃない場合にはどうするのがいいのだろうか。具体的にはERB、LESSという組み合わせじゃなくて、HAML、SCSSにしたい。

ちょっと検索すると、SASS化したり、HAML化されたTwitter Bootstrapはある。ということは混ぜて使えばオッケーなのか。

RailsのGeneratorって、そういえばどう実現されてるのかと思って、Rails::Generatorsあたりを眺めてみた。何となくコード生成って黒魔術っぽく感じていたけど、基本的にファイルに文字列をinterpolateしつつコピーしたりしているだけということが分かった。

RailsのGeneratorで使われてるThorっていうのは、何だっけと思って調べたら、Yehuda Katzが2008年にリリースしたRakeに代わるスクリプティングサポートライブラリだった。RakeとSakeとかRubyスクリプティング環境って実はイマイチじゃね?フルスタックのスクリプティングライブラリがほしいよね。ということで、Thorを作ったよとYehudaはブログで言っている。それを眺めたJose Valimが、これってRailsのGeneratorに使えるんじゃね? copy_fileとかさえ実装すればさ。ということで、Google Summer of CodeのプロジェクトとしてRails3に向けて実装したもののようだ(ブログ)。

Twitter Bootstrapのgeneratorは、こんな感じ。

module Bootstrap
  module Generators
    class InstallGenerator < Rails::Generators::Base
      source_root File.expand_path("../templates", __FILE__)
      desc "This generator installs Twitter Bootstrap to Asset Pipeline"

      def add_assets
       if File.exist?('app/assets/javascripts/application.js')
	      insert_into_file "app/assets/javascripts/application.js", "//= require twitter/bootstrap\n", :after => "jquery_ujs\n"
	     else
	      copy_file "application.js", "app/assets/javascripts/application.js"
	     end
       if File.exist?('app/assets/stylesheets/application.css')
	      insert_into_file "app/assets/stylesheets/application.css", " *= require twitter/bootstrap\n", :after => "require_self\n"
	     else
	      copy_file "application.css", "app/assets/stylesheets/application.css"
	     end
  	  end

Rails::Generators::Baseは、Thor::Groupを継承している。Thor::Groupは、メソッド定義の順に従って、まとめてタスク(=メソッド)を実行するクラス。insert_into_fileとかで、「ここに1行記述を追記してちょ」というようなことができるらしい。なるほど。

RailsのGeneratorって、こう、いろいろとgemに付属してきて便利なんだけど、結局のところテンプレートファイルを展開するだけだから、ほかのGeneratorと組み合わせると上手くいかないことが多いように思う。この辺を組み合わせ自由にしてDRYな仕組みができれば素晴らしそうだけど、原理的に難しそう。生成したコードを後からerb2hamlとかhtml2hamlとかで変換するのが正解っぽい。

やれるとしたら、Twitter Bootstrap for Rails 3.1 Asset PipelineHAMLのファイルをジャカジャカ突っ込んでオプションで切り替えられるようにすることだろうか。gemfileにhaml-railsが書いてあれば、自動判別でERBではなくHAMLを生成するとか。

正月早々Railsの素振りをしてみたりして、今年はまたRailsも少しやってみたいと思っている。

Enumerable#injectって分かりやすいのかな

CSVって、RFC4180とかに仕様がまとまってるんだとマイルドに驚きつつ、RubyCSVクラスをのぞいてみた。FasterCSVと呼ばれていたものが正式にRubyに入り、1.9系のRubyではインターフェースが変わっているという話。CSVライブラリのソースコードには、冗長といっていいほどコメントが豊富にあるし、Rubyでライブラリを実装するお手本のような感じもある気がする。つらつら読んでいて気になったのは、CSV::Row#to_hashにある、次のようなコード。

def to_hash
  # flatten just one level of the internal Array
  Hash[*@arr.inject(Array.new) { |ary, pair| ary.push(*pair) }]
end

# CSV.new([["a", 2], ["b", 3], ["c", 5]]).to_hash
#   => {"a"=>2, "b"=>3, "c"=>5}

もともとCSV::RowクラスがArrayとHashの両方の性質を備えているとはいえ、この節操のないArrayとHashの混ざり具合は、どうなんだろうか。スプラッタ演算子の濫用も気になる。というか、パッと見て意味が分からなかった。

これは、

def to_hash
  hash = { }
  @arr.each {|pair| hash[pair.first] = pair.last}
  hash
end

と書くといいんじゃないかしら。injectを使うにしても、injectすべきなのは、hashのほう。

def to_hash
  @arr.inject(Hash.new) {|h, pair| h[pair.first] = pair.last; h}
end

しかし、こうやって他人のソースコードのスタイルにケチを付けるのって、ケチな話だよなと思ったりもする。どうでもいいっちゃどうでもいいし、それより、役立つものを作れよという話なのかも。

そういえば、Hash["a", 2, "b", 3] => {"a"=>2, "b"=>3}というHashのコンストラクタも知らなかったけど、結構使われてるのだろうか。なんとなくRubyっぽくない。前から順番にハッシュ作るのか。引数の個数が偶数だとArgumentErrorで落ちる。

hash.cには、

static VALUE
rb_hash_s_create(int argc, VALUE *argv, VALUE klass)
{
    :
    :
    if (argc % 2 != 0) {
	rb_raise(rb_eArgError, "odd number of arguments for Hash");
    }

    hash = hash_alloc(klass);
    for (i=0; i<argc; i+=2) {
        rb_hash_aset(hash, argv[i], argv[i + 1]);
    }

とある。なるほど。

Hash[k1, v1, k2, v2...]というのがどのぐらい使わてるのか、Ruby本体で調べてみた。

find hoge/lib/ruby/1.9.1 -iname '*rb' |xargs grep 'Hash *\['

とやった結果は以下のとおり。スプラッタ演算子と相性は良さそう。しかし、ほとんどがtk関連かな。そしてよく見ると、resolv.rbは違うな。drb.rbの例もえぐいなあ。

.//csv.rb:      Hash[*@row.inject(Array.new) { |ary, pair| ary.push(*pair) }]
.//csv.rb:    meta = Hash[*csv.shift]
.//drb/drb.rb:      families = Hash[*infos.collect { |af, *_| af }.uniq.zip([]).flatten]
.//multi-tk.rb:      Hash[*_lst2ary(ip._eval("::safe::interpConfigure " +
.//psych/visitors/to_ruby.rb:          members = Hash[*o.children.map { |c| accept c }]
.//psych/visitors/to_ruby.rb:            h = Hash[*members]
.//psych/visitors/to_ruby.rb:          h = Hash[*o.children.map { |c| accept c }]
.//psych/visitors/to_ruby.rb:          h = Hash[*o.children.map { |c| accept c }]
.//psych/visitors/to_ruby.rb:          h = Hash[*o.children.map { |c| accept c }]
.//psych/visitors/to_ruby.rb:          h = Hash[*o.children.map { |c| accept c }]
.//psych/visitors/to_ruby.rb:        h = Hash[*node.children.map { |c| accept c }]
.//resolv.rb:        return ClassHash[[type_value, class_value]] ||
.//resolv.rb:          ClassHash[[type_value, class_value]] = c
.//resolv.rb:          ClassHash[[s::TypeValue, ClassValue]] = c
.//resolv.rb:          ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
.//resolv.rb:          ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
.//resolv.rb:          ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
.//resolv.rb:          ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
.//rubygems/specification.rb:  #   Hash[*spec.authors.zip(spec.emails).flatten]
.//rubygems/specification.rb:  #   Hash[*spec.authors.zip(spec.emails).flatten]
.//syck/types.rb:                        seq.add( Hash[ *v ] )
.//syck/types.rb:                        seq.add( Hash[ *v ] )
.//test/unit.rb:          @workers_hash = Hash[@workers.map {|w| [w.io,w] }] # out-IO => worker
.//tk/font.rb:    Hash[TkFont.actual(fnt, option)]
.//tk/font.rb:    Hash[TkFont.actual_displayof(fnt, option)]
.//tk/font.rb:    h = Hash[TkFont.metrics(fnt)]
.//tk/font.rb:    h = Hash[TkFont.metrics_displayof(fnt, win, option)]
.//tk/font.rb:              Hash[*tk_split_simplelist(tk_call('font', 'configure',
.//tk/font.rb:            Hash[*(tk_split_simplelist(tk_call('font', 'configure',
.//tk/font.rb:            Hash[*(tk_split_simplelist(tk_call('font', 'configure',
.//tk/font.rb:    Hash[actual(option)]
.//tk/font.rb:    Hash[actual_displayof(win, option)]
.//tk/font.rb:    Hash[latin_actual(option)]
.//tk/font.rb:    Hash[latin_actual_displayof(win, option)]
.//tk/font.rb:    Hash[kanji_actual(option)]
.//tk/font.rb:    Hash[kanji_actual_displayof(win, option)]
.//tk/font.rb:    Hash[latin_configinfo(slot)]
.//tk/font.rb:    Hash[kanji_configinfo(slot)]
.//tk/font.rb:    h = Hash[metrics(option)]
.//tk/font.rb:    h = Hash[metrics_displayof(win, option)]
.//tk/font.rb:    h = Hash[latin_metrics(option)]
.//tk/font.rb:    h = Hash[latin_metrics_displayof(win, option)]
.//tk/font.rb:    h = Hash[kanji_metrics(option)]
.//tk/font.rb:    h = Hash[kanji_metrics_displayof(win, option)]
.//tk/variable.rb:      #Hash[*tk_split_simplelist(INTERP._eval("global #{@id}; array get #{@id}"))]
.//tk/variable.rb:      Hash[*tk_split_simplelist(INTERP._invoke('array', 'get', @id))]
.//tk/variable.rb:        Hash[*tk_split_simplelist(INTERP._eval(Kernel.format('global %s; array get %s', @id, @id)))]
.//tk/variable.rb:        #Hash[*tk_split_simplelist(_fromUTF8(INTERP._invoke_without_enc('array', 'get', @id)))]
.//tk.rb:      Hash[*tk_split_simplelist(INTERP._invoke_without_enc('array', 'get',
.//tk.rb:      Hash[*tk_split_simplelist(INTERP._invoke('array', 'get', 'env'))]
.//tk.rb:      Hash[*tk_split_simplelist(INTERP._invoke('array', 'get', 'auto_index'))]
.//tk.rb:      Hash[*tk_split_simplelist(INTERP._invoke('array', 'get',
.//tkextlib/blt/container.rb:      Hash[*simplelist(tk_send_without_enc('find', '-command', pat))]
.//tkextlib/blt/container.rb:      Hash[*simplelist(tk_send_without_enc('find', '-name', pat))]
.//tkextlib/blt/tree.rb:      Hash[*simplelist(tk_call('::blt::tree', 'get', tagid(node)))]
.//tkextlib/blt/watch.rb:          Hash[*(info.flatten)]
.//tkextlib/blt/winop.rb:    Hash[*list(tk_call('::blt::winop', 'colormap', win))]
.//tkextlib/tile/style.rb:      Hash[*(simplelist(tk_call(TkCommandNames[0], 'map', style)))].each{|k, v|
.//tkextlib/tile/treeview.rb:          ret = Hash[*(tk_split_simplelist(tk_call_without_enc(*(__item_confinfo_cmd(tagid(tagOrId)))), false, false))].to_a.collect{|conf|
.//tkextlib/tile/treeview.rb:          ret = Hash[*(tk_split_simplelist(tk_call_without_enc(*(__item_confinfo_cmd(tagid(tagOrId)))), false, false))].to_a.collect{|conf|
.//tkextlib/tile.rb:        images = Hash[*TkComm.simplelist(Tk.tk_call(cmd, imgdir, pat))]
.//tkextlib/tile.rb:        images = Hash[*TkComm.simplelist(Tk.tk_call('array', 'get', 'images'))]
.//tkextlib/treectrl/tktreectrl.rb:          Hash[*(TkComm.list(val))].each{|k, v|
.//tkextlib/treectrl/tktreectrl.rb:    Hash[*list(tk_send('item', 'dump', item))]
.//tkextlib/treectrl/tktreectrl.rb:      Hash[*simplelist(tk_send('style', 'layout', style, elem))].each{|k, v|
.//tkextlib/winico/winico.rb:      h = Hash[*list(inf)]
.//uri/common.rb:  # p Hash[ary]            # => {"a"=>"2", "b"=>"3"}

Railsはどうかと思ったら、結構使われてる。中でごにょごにょとmapした結果からHashを作るというケースが多いらしい。

~/src/rails (master) find ./ -iname '*rb' |xargs grep 'Hash *\[' |wc -l
      73
~/src/rails (master) find ./ -iname '*rb' |xargs grep 'Hash.new' |wc -l
     111

Ruby 1.9.2でrequire 'sqlite3'は遅かった

単語帳を作るために英辞郎CD-ROMを買った。sqlite3に突っ込んで、Rubyで引けるようにして、Emacsでメジャーモードを作った。require 'sqlite3'が異様に遅い。Ruby 1.9.2から1.9.3にアップデートしたら遅いのが解決した。何だったんだろうか。うーん、「異様に遅い」といっても、1秒前後なので、そんなもんなのか。

周囲の世界が歪む錯視をCanvasで

Canvasでアニメーションによる錯視を作ってみた。20〜30秒ほど中心を見つめ、視野全体で螺旋が渦巻く感じを得た後に周囲の部屋を見渡すと、数秒間ほど柱なんかがグニャグニャと歪んで見える。人によって見える程度がかなり違う模様。


http://dl.dropbox.com/u/296/log_spiral/log_spiral.html

この錯視は20年ほど前にテレビで見たもので、ずっと名前がわからなかった。錯視のホームページを作っている立命館大学の北岡先生にメールでお伺いしたところ、どうも「運動残効」(motion aftereffect)とか「滝の錯視」(waterfall illusion)という名前で呼ばれるもので、19世紀から知られている図案だと分かった。

ともあれ、こんな単純なアニメーションを作るだけでも、いろいろと良い勉強になった。

1つ、確認できなくて気になったのは、「canvas.width=canvas.width」というイディオム。アニメーションのために描画済みのcanvasをクリアすることができる。このいかにもひどいやり方で描画済みコンテンツを消す方法が、実は遅いという話。fillRectで背景色で塗りつぶすほうが速いという。回転・縮小の座標変換のパラメータは自前でリセットする必要があるので、アニメーションによっては速くならないことはあるというのだけど、パフォーマンス差は確認できなかった。

もう1つ、setIntevalではなくて、requestAnimationFrameというAPIが実装されつつあって、タブにフォーカスがあるときだけコールバックしてくれるようなものがあるらしい。

もう1つ、回転するアニメーションなので、描画済みのものをビットマップとしてアングルを付けて描画したほうが、毎回線分として描くよりも速そうだと思って以下のように、dataURLを使って書いた。

    var img = (function() {
	draw();
	var img = new Image();
	var _imageURL = canvas.toDataURL("image/png");
	img.src = _imageURL
	return img;
    })();

    function rotate() {
    :
    :
        timerId = setInterval(function(){
            canvas.width = canvas.width;
            ctx.translate(d, d);
            ctx.rotate(Math.PI*(angle/180));
            ctx.drawImage(img, -d, -d);
    :
    :
        }, 100);
    }

ぜんぜん速くなかった。GPUで回転したら速そうだけど、実際のところ、img.srcには文字列にエンコードされた効率の悪いビットマップ画像が入っているし、ブラウザのJSエンジンのヒープメモリ上にあって、そのメモリイメージをフレームごとにGPUに回転系コマンドとともに転送している、のではないかという気がする。