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

端の処理がbuttだけなのでroundとsquareを追加しよう。ついでにclose_pathでループを作れるようにもしてみる。

ループ処理

NanoVGではnvgClosePath関数を呼ぶことでそのパスのclosedフラグを立てる。これが立っていると最初の点と最後の点が自動的に接続されてループを構成するようになる。まずCloseコマンドを作る。

  class CmdMoveTo < Vector;end
  class CmdLineTo < Vector;end
  class CmdClose;end
  class Point < Struct.new(:v, :dv, :dmv);end

クラスがコマンドなのか何なのかわからなくなってしまうので頭にCmdをつけるようにした。ユーザが呼ぶメソッドはclose_path。

  def close_path
    append_commands(CmdClose.new)
    self
  end

flatten_pathsはこのようになる。

  private def flatten_paths
    @commands.each do |cmd|
      case cmd
      when CmdMoveTo
        @points << Point.new(cmd)
      when CmdLineTo
        @points << Point.new(cmd)
      when CmdClose
        @closed = true
      end
    end

    # 最初と最後の点が同じ位置だったら最後を捨ててループしていることにする
    if @points[0] == @points[-1]
      @points.pop
      @closed = true
    end

    # 点の次の線の向きを計算
    # ループしない場合は最後の点の情報は使われない
    ([@points.last] + @points).each_cons(2) do |p0, p1|
      p0.dv = (p1.v - p0.v).normalize
    end
  end

最初と最後の点が同じ場合の処理は必要なのかどうなのかがわからないが、NanoVGに入っているので入れておく。expand_strokeはこう。

  private def expand_stroke(w)
    calculate_joins

    if @closed # ループしている場合
      # 頂点生成
      (@points + [@points.first]).each do |p0|
        @verts << p0.v + p0.dmv * w
        @verts << p0.v - p0.dmv * w
      end
  
    else # していない場合
      ncap = curve_divs(w, Math::PI, 0.25)

      # 始点の処理
      p0 = @points[0]
      case @line_cap
      when :butt
        butt_cap_start(p0.v, p0.dv, w, -0.5)
      when :square
        butt_cap_start(p0.v, p0.dv, w, w - 1)
      when :round
        round_cap_start(p0.v, p0.dv, w, ncap)
      end
  
      # 中間の頂点生成
      @points[1..-2].each do |p0|
        @verts << p0.v + p0.dmv * w
        @verts << p0.v - p0.dmv * w
      end
  
      # 終点の処理
      p0 = @points[-2]
      p1 = @points[-1]
      case @line_cap
      when :butt
        butt_cap_end(p1.v, p0.dv, w, -0.5)
      when :square
        butt_cap_end(p1.v, p0.dv, w, w - 1)
      when :round
        round_cap_end(p1.v, p0.dv, w, ncap)
      end
    end
  end

端の処理の追加が既に入ってしまっているが、@closedを見て大きく2つに分岐する。ループする場合は端の処理は必要無い。

round

expand_strokeの中でroundとsquareの分岐が追加されていた。square対応のためにやはりbuttの処理は始点と終点をわける必要があった。わけて引数を変えるだけでsquareは対応できるのだが、roundはさすがにそうもいかない。まずexpand_strokeの中で呼んでいるcurve_divsから。

  # 弧の点の数を算出する
  private def curve_divs(r, arc, tol)
    da = Math.acos(r / (r + tol)) * 2
    [2, (arc / da).ceil].max
  end

そういえばNanoVGで端の処理にあるaaという引数は省略した。こいつはDevicePixelRatioというものが影響するパラメータで、どうやら高解像度の画面(Retinaディスプレイとか)に対応するためのものらしい。が、実際NanoVGでこの値をいじると半透明になったりしてよくわからないし俺には関係無いので省いている。んで、ここに出てくるtolというのもその関連の値で、まあ、高解像度じゃない場合は0.25になるのでそれで固定にしてある。必要そうなら後で追加する。
次にround_cap_startとend。

  # 丸い始点
  private 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
  
  # 丸い終点
  private 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

NanoVGのコードは例によって見た目ややこしい計算式になっているのだが、実際のところ回転処理をしているだけなのでVectorに回転用メソッドを追加して回転させるようにした。ちなみにNanoVGのコードはこんな感じ。

static NVGvertex* nvg__roundCapStart(NVGvertex* dst, NVGpoint* p, float dx, float dy, float w, int ncap, float aa)
{
	int i;
	float px = p->x;
	float py = p->y;
	float dlx = dy;
	float dly = -dx;
	NVG_NOTUSED(aa);
	for (i = 0; i < ncap; i++) {
		float a = i/(float)(ncap-1)*NVG_PI;
		float ax = cosf(a) * w, ay = sinf(a) * w;
		nvg__vset(dst, px - dlx*ax - dx*ay, py - dly*ax - dy*ay, 0,1); dst++;
		nvg__vset(dst, px, py, 0.5f,1); dst++;
	}
	nvg__vset(dst, px + dlx*w, py + dly*w, 0,1); dst++;
	nvg__vset(dst, px - dlx*w, py - dly*w, 1,1); dst++;
	return dst;
}

nvg__vsetの引数の後ろの2つが省略されているが、これはバックエンドに渡すテクスチャ座標で、vは1固定。実際Strokeの場合の描画処理ではvは使っていなくて、uの使いどころもアンチエイリアス版のバックエンドで端を検出するのに使われるだけである。今回はとりあえず必要無いかな〜と思って省略している。必要そうなら後で追加する。

結果

今回のコードはこちら。動かすとこんな感じになる。

close_pathを追加すると始点と終点が接続されてループするようになる。