ベクタグラフィックスその4

現状ではパスは1つだけでOreVGクラスで扱っているが、NanoVGはサブパスを複数持つような構造になっている。これはたぶん閉じたり閉じなかったりする複数のパスを一まとめにしてグラデーションしたりするためにそうなっているんじゃないかと思うのだが、今回はこのサブパス機能を追加してみよう。でもグラデーションはずっと先。まだ色も指定できないし。

SubPath

サブパスはそれぞれ別々に点の配列とclosedフラグを持つ。サブパスが生成されるのはMoveToコマンドのタイミングとなる。このへんはHTML5Canvasと同様である。また、線の接続が途切れるので頂点配列もサブパス単位になる。まずはSubPathクラスを作る。

  class SubPath
    attr_accessor :points, :closed, :verts
    
    def initialize
      @points = []
      @closed = false
      @verts = []
    end

    # ぶつ切りの始点
    def butt_cap_start(p0, dv, w, d)
      pv = p0 - dv * d
      dlv = dv.rperp * w
      @verts << pv + dlv - dv
      @verts << pv - dlv - dv
      @verts << pv + dlv
      @verts << pv - dlv
    end
  
    # ぶつ切りの終点
    def butt_cap_end(p0, dv, w, d)
      pv = p0 + dv * d
      dlv = dv.rperp * w
      @verts << pv + dlv
      @verts << pv - dlv
      @verts << pv + dlv + dv
      @verts << pv - dlv + dv
    end
  
    # 丸い始点
    def round_cap_start(p0, dv, w, ncap)
      dlv = dv.rperp
      ncap.times do |i|
        a = i / (ncap - 1.0) * Math::PI
        @verts << p0 - dlv.rotate(a) * w
        @verts << p0
      end
      @verts << p0 + dlv * w
      @verts << p0 - dlv * w
    end
    
    # 丸い終点
    def round_cap_end(p0, dv, w, ncap)
      dlv = dv.rperp
      @verts << p0 + dlv * w
      @verts << p0 - dlv * w
      ncap.times do |i|
        a = i / (ncap - 1.0) * -Math::PI
        @verts << p0
        @verts << p0 - dlv.rotate(a) * w
      end
    end
  end

考えるのが面倒だったので始点終点の処理はこっちに移動した。他のいろんな処理も移動すべきな気はするが、そのうち気が向いたらやる(いい加減)。

flatten_paths

MoveToコマンドを処理するタイミングでSubPathを生成する。OreVGクラスには@subpathsインスタンス変数を追加して配列を入れておく。また、従来の@pointsを扱う場面はすべてサブパス内のpointsを使うように変更する。これはcalculate_joinsやexpand_strokeも同様となるので省略する。

  private def flatten_paths
    @commands.each do |cmd|
      case cmd
      when CmdMoveTo
        @subpaths << SubPath.new # 新規サブパス追加
        @subpaths.last.points << Point.new(cmd)
      when CmdLineTo
        @subpaths.last.points << Point.new(cmd)
      when CmdClose
        @subpaths.last.closed = true
      end
    end

    @subpaths.each do |subpath|
      # 最初と最後の点が同じ位置だったら最後を捨ててループしていることにする
      if subpath.points[0] == subpath.points[-1]
        subpath.points.pop
        subpath.closed = true
      end
  
      # 点の次の線の向きを計算
      # ループしない場合は最後の点の情報は使われない
      ([subpath.points.last] + subpath.points).each_cons(2) do |p0, p1|
        p0.dv = (p1.v - p0.v).normalize
      end
    end
  end

render_stroke

render_strokeも同様にサブパス単位になるが、ここでサブパス単位にわけるとサブパスが変わる際に線が強制的に途切れるようになる。

  private def render_stroke
    @subpaths.each do |subpath|
      subpath.verts.each_cons(3).with_index do |ary, i|
        if i.even?
          p1, p0, p2 = ary
        else
          p0, p1, p2 = ary
        end
        triangle(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y)
      end
    end
  end

結果

vg = OreVG.new(50)
vg.move_to(4, 4)
vg.line_to(45, 8)
vg.move_to(20, 15)
vg.line_to(44, 35)
vg.line_to(10, 40)
vg.width = 5
vg.line_cap = :round
vg.close_path
vg.stroke
Window.loop do
  Viewer.draw(vg.image, vg.triangles, vg.subpaths.map{|sp|sp.points}.flatten)
end

このようなコードで

という感じの描画ができるようになる。現時点ではコードがややこしくなるだけでメリットは無いが、まあ、最終的にはおそらく必要になるのでいつ実装してもたいして違いは無い。
今回のコードはこちら