Canvasでライフゲームを作ってみた
Canvasでライフゲームを作ってみた。ライフゲームが何かは知っていたし、いくつかの有名なパターンの動きも見たことがあったはずだけど、自分で適宜セルの生死をトグルしたり、パターンをロードしたりできると、思ったよりも面白かった。そして、iPadやAndroidでも、全く同じコードがちゃんと動くというのが改めてすごいなと思ったりした。
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 BootstarpをRailsで使う場合、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 PipelineにHAMLのファイルをジャカジャカ突っ込んでオプションで切り替えられるようにすることだろうか。gemfileにhaml-railsが書いてあれば、自動判別でERBではなくHAMLを生成するとか。
Enumerable#injectって分かりやすいのかな
CSVって、RFC4180とかに仕様がまとまってるんだとマイルドに驚きつつ、RubyのCSVクラスをのぞいてみた。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
周囲の世界が歪む錯視を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に回転系コマンドとともに転送している、のではないかという気がする。