(追記:2014-3-3) Gvizについてのまとめ頁を作りました。

Gvizの目次 - Rubyの世界からGraphvizの世界にこんにちは!


Graphvizには「osage」という変わった名前のレイアウトがあります。これは多数のノードを整列して配置します。GraphvizのRubyラッパー「gviz」を使って100個のノードを整列表示するには次のようにします1

graph.ru

global layout:'osage'
nodes shape:'square', width:'0.8'
100.times { |i| node i.to_id, label:i }

save :osage

ターミナルでgvizコマンドを実行し生成されたDOTファイルを開くと、次のグラフが得られます。

invader noshadow

0と99は反転していますが、1から順番にノードが整列配置されているのがわかります。

前回の投稿ではGraphvizのこの能力を使ってカラータイルを作りましたが、今更ながらこれは正に「ドットマトリクス」であるなあと気付いたのでした。ドットマトリクスといえば、そう「ドット絵」です。

そんなわけで…

今回は、Graphvizで「スペースインベーダー」を描いてみるという、アホな企画です。良い子の皆さんは真似しないでくださいね。

ところで、スペースインベーダー、知ってますか?

スペースインベーダーのドットマトリクス

まずは、ネットから次のようなインベーダーの画像を取得します。

invader noshadow_noround

そして、この画像に基いて二次元配列データを作ります。座標値または座標レンジを使って画が存在するドットを指定していきます(ここでは横幅を12ドットとしています)。

kani_map =
  [
    [3, 9],
    [4, 8],
    [3..9],
    [2, 3, 5..7, 9, 10],
    [1..11],
    [1, 3..9, 11],
    [1, 3, 9, 11],
    [4, 5, 7, 8],
  ]

Pieceオブジェクトの生成

次に、Pieceクラスを用意して、このドットデータを持ったPieceオブジェクトを生成するようにします。デフォルトで画があるところにはtrue、無いところにはfalseをセットする二次元配列を、mapインスタンス変数で保持させます。

invader.rb

class Piece
  attr_reader :name, :map
  def initialize(name, map, filling=true, width=12)
    @name, @width, @filling = name, width, filling
    @map = build_map(map)
  end

  private
  def build_map(map)
    map.map do |row|
      row.inject(Array.new(@width){ !@filling }) do |mem, val|
        start, length = conv2st_len(val)
        mem.fill(@filling, start, length)
      end
    end
  end

  def conv2st_len(val)
    case val
    when Fixnum then [val, 1]
    when Range  then [val.begin, val.size]
    else raise "Accept only Fixnum or Range"
    end
  end
end

build_mapメソッドは、すべてがfalse値の二次元配列を生成し、渡されたmapデータをArray#fillを使って埋め込むようにします。conv2st_lenメソッドは、座標値と座標レンジを統一的に扱えるように前処理します。

このPieceクラスを使って、先のデータに基づいたkaniオブジェクトを生成します。

kani_map =
  [
    [3, 9],
    [4, 8],
    [3..9],
    [2, 3, 5..7, 9, 10],
    [1..11],
    [1, 3..9, 11],
    [1, 3, 9, 11],
    [4, 5, 7, 8],
  ]

kani = Piece.new(:kani, kani_map)

kani.map # => [[false, false, false, true, false, false, false, false, false, true, false, false], [false, false, false, false, true, false, false, false, true, false, false, false], [false, false, false, true, true, true, true, true, true, true, false, false], [false, false, true, true, false, true, true, true, false, true, true, false], [false, true, true, true, true, true, true, true, true, true, true, true], [false, true, false, true, true, true, true, true, true, true, false, true], [false, true, false, true, false, false, false, false, false, true, false, true], [false, false, false, false, true, true, false, true, true, false, false, false]]

Pieceオブジェクトの描画

さて、データの準備ができたので、Graphvizで描画してみます。

graph.ru

require "./invader"

kani_map =
  [
    [3, 9],
    [4, 8],
    [3..9],
    [2, 3, 5..7, 9, 10],
    [1..11],
    [1, 3..9, 11],
    [1, 3, 9, 11],
    [4, 5, 7, 8],
    [],[],[],[]
  ]

kani = Piece.new(:kani, kani_map)

global layout:'osage'
nodes shape:'square', width:'0.8', style:'filled'

kani.map.each_with_index do |row, i|
  row.each_with_index do |val, j|
    id = "#{i}_#{j}"
    color = val ? '#00FFFF' : '#000000'
    node id.to_id, label:'', color:color, fillcolor:color
  end
end

save :invader

osageレイアウトでは縦横のドット数が異なると配列がズレてしまうので、kani_mapでは横配列数(12ドット)に合わせて縦配列数を調整しています。eachでkaniオブジェクトのmapを展開し、各ドットの値(trueまたはfalse)に応じてノードの色を変えます。

出力です。

invader noshadow_noround

Graphviz上にスペースインベーダーが現れました。



でも、カニ一匹じゃ、寂しいです..よね..

もっと広いキャンバスに、インベーダー表示したいですよね。

もう少しがんばって..みますか..

Canvasオブジェクトの生成

Pieceオブジェクトを配置するCanvasオブジェクトを用意します。Canvasクラスを定義しましょう。

class Canvas
  include Enumerable
  def initialize(size, filling=false)
    @map = Array.new(size) { Array.new(size) { filling } }
  end

  def each(&blk)
    @map.each(&blk)
  end

  def fill(x, y, piece)
    piece.map.each_with_index do |row, i|
      @map[y+i][x, row.size] = row
    end
    @map
  end
end

Canvasクラスは、任意サイズの二次元配列を保持します。そしてCanvas#fillメソッドにより、Canvasオブジェクト上の任意の位置に、Pieceオブジェクトのマップを埋め込めるようにします。

さあ、Canvas上にkaniを埋め込んで表示します。

require "./invader"

kani_map =
  [
    [3, 9],
    [4, 8],
    [3..9],
    [2, 3, 5..7, 9, 10],
    [1..11],
    [1, 3..9, 11],
    [1, 3, 9, 11],
    [4, 5, 7, 8],
  ]

kani = Piece.new(:kani, kani_map, '#00FFFF')

canvas = Canvas.new(120)
canvas.fill(50, 50, kani)

global layout:'osage'
nodes shape:'square', width:'0.8', style:'filled', penwidth:4

canvas.map.each_with_index do |row, i|
  row.each_with_index do |color, j|
    id = "#{i}_#{j}"
    color ||= '#000000'
    node id.to_id, label:'', color:color, fillcolor:color
  end
end

save :invader

ここでは、Piece.newの第3引数に色を直接渡しています。

出力です。

invader noshadow_noround

よし。

じゃあ、全員集合といきましょうか。

require "./invader"

ika_map = 
  [
    [5, 6],
    [4..7],
    [3..8],
    [2, 3, 5, 6, 8, 9],
    [2..9],
    [4, 7],
    [3, 5, 6, 8],
    [2, 4, 7, 9],
  ]

kani_map =
  [
    [3, 9],
    [4, 8],
    [3..9],
    [2, 3, 5..7, 9, 10],
    [1..11],
    [1, 3..9, 11],
    [1, 3, 9, 11],
    [4, 5, 7, 8],
  ]

tako_map =
  [
    [4..7],
    [1..10],
    [0..11],
    [0..2, 5, 6, 9..11],
    [0..11],
    [3, 4, 7, 8],
    [2, 3, 5, 6, 8, 9],
    [0, 1, 10, 11],
  ]

wall_map =
  [
    [3..12],
    [2..13],
    [1..14],
    [0..15],
    [0..15],
    [0..15],
    [0..15],
    [0..15],
    [0..15],
    [0..4, 11..15],
    [0..3, 12..15],
  ]

battery_map =
  [
    [6],
    [5..7],
    [5..7],
    [2..10],
    [1..11],
    [1..11],
    [1..11],
  ]

fire_map =
  [
    [1, 2],
    [0, 1],
    [1],
    [1, 2],
    [0, 1],
    [1],
    [1],
  ]

ika, kani, taco, wall, battery, fire =
  [
    [:ika, ika_map, '#00FF00'],
    [:kani, kani_map, '#00FFFF'],
    [:tako, tako_map, '#FF00FF'],
    [:wall, wall_map, '#FF0000'],
    [:battery, battery_map, '#00FFFF'],
    [:fire, fire_map, '#FFFF00'],
  ].map { |name, map, color| Piece.new(name, map, color) }


canvas = Canvas.new(120)

6.step(110, 16).each { |i| canvas.fill(i, 12, ika) }
6.step(110, 16).each { |i| canvas.fill(i, 28, kani) }
6.step(110, 16).each { |i| canvas.fill(i, 44, taco) }
12.step(110, 28).each { |i| canvas.fill(i, 85, wall) }
canvas.fill(23, 105, battery)
canvas.fill(28, 64, fire)


global layout:'osage'
nodes shape:'square', width:'0.8', style:'filled', penwidth:4

canvas.map.each_with_index do |row, i|
  row.each_with_index do |color, j|
    id = "#{i}_#{j}"
    color ||= '#000000'
    node id.to_id, label:'', color:color, fillcolor:color
  end
end

save :invader



どうだ。



invader noshadow_noround

ヒャホー!


「スペースインベーダー、Graphviz侵略ス。」




=== Ruby関連電子書籍100円で好評発売中! ===

M’ELBORNE BOOKS

start_ruby ruby_object trivia

  1. patchworkというレイアウトでも同様のことができます。


blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS