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

端の処理はあるが接続点の処理が無いので今回はこれを作る。

各種フラグ

まずPointクラスに項目を追加して各種フラグを持てるようにする。

  class Point < Struct.new(:v, :dv, :dmv, :len, :left, :inner_bevel, :bevel, :corner);end

lenは線の長さでフラグではないが、その次の4つはフラグとなる。それぞれ、左回りフラグ、接続点が形状にめり込むフラグ、接続点の処理をするフラグ、接続点の処理が可能フラグ、みたいな意味合いとなる。calculate_joinsの中でcorner以外の3つを設定する。cornerはベジェ曲線を構成するときに使うので今回はflatten_pathsの中でtrue固定にしている。

  private def calculate_joins(w, line_join, miter_limit)
    iw = w > 0 ? 1.0 / w : 0
  
    @subpaths.each do |subpath|
      # 点の両側の線の中間を算出する
      ([subpath.points.last] + subpath.points).each_cons(2) do |p0, p1|
        p1.dmv = (p0.dv.rperp + p1.dv.rperp) * 0.5
        dmr2 = p1.dmv.x * p1.dmv.x + p1.dmv.y * p1.dmv.y
        if dmr2 > 0.000001
          scale = 1.0 / dmr2
          if scale > 600.0
            scale = 600.0
          end
          p1.dmv *= scale
        end

        # 左回りフラグ
        cross = p1.dv.x * p0.dv.y - p0.dv.x * p1.dv.y
        if cross > 0
          p1.left = true
        end

        # 交点が算出できないフラグ?
        limit = [1.01, [p0.len, p1.len].min * iw].max
        if dmr2 * limit * limit < 1.0
          p1.inner_bevel = true
        end

        # join処理が必要フラグ
        if p1.corner
          if dmr2 * miter_limit * miter_limit < 1.0 or line_join == :bevel or line_join == :round
            p1.bevel = true
          end
        end
      end
    end
  end

expand_stroke

頂点を生成するところでこのフラグとOreVGのインスタンス変数@line_joinを見て丸い接続点(round)と切り捨てる接続点(bevel)の2種類を呼び分ける。ループするほうだけ抜粋。

  private def expand_stroke(w, line_cap, line_join, miter_limit)
    ncap = curve_divs(w, Math::PI, 0.25)

    calculate_joins(w, line_join, miter_limit)

    @subpaths.each do |subpath|
      if subpath.closed # ループしている場合
        # 頂点生成
        ([subpath.points.last] + subpath.points).each_cons(2) do |p0, p1|
          if p1.inner_bevel or p1.bevel
            if line_join == :round
              subpath.round_join(p0, p1, w, w, ncap)
            else
              subpath.bevel_join(p0, p1, w, w)
            end
          else
            subpath.verts << p1.v + p1.dmv * w
            subpath.verts << p1.v - p1.dmv * w
          end
        end

        subpath.verts << subpath.verts[0]
        subpath.verts << subpath.verts[1]

join系メソッドの引数でwが2個あるのは無駄に見えるが、これは塗りつぶし系の処理で呼ぶときに違う値を入れて使うっぽい。

bevel_join

大きく左回りと右回りにロジックが分かれていて、bevelフラグが立っているかどうかで更に分かれる。左右のロジックの違いは符号と頂点順だけである。

    # bevel join
    def bevel_join(p0, p1, lw, rw)
      dlv0 = p0.dv.rperp
      dlv1 = p1.dv.rperp

      if p1.left
        lv0, lv1 = choose_bevel(p1.inner_bevel, p0, p1, lw)
        @verts << lv0
        @verts << p1.v - dlv0 * rw

        if p1.bevel
          @verts << lv0
          @verts << p1.v - dlv0 * lw
          @verts << lv1
          @verts << p1.v - dlv1 * lw
        else
          lv = p1.v - p1.dmv * rw
          @verts << p1.v
          @verts << p1.v - dlv0 * rw
          @verts << rv
          @verts << rv
          @verts << p1.v
          @verts << p1.v - dlv1 * rw
        end
  
        @verts << lv1
        @verts << p1.v - dlv1 * rw
      else
        rv0, rv1 = choose_bevel(p1.inner_bevel, p0, p1, -rw)
        @verts << p1.v + dlv0 * lw
        @verts << rv0
  
        if p1.bevel
          @verts << p1.v + dlv0 * lw
          @verts << rv0
          @verts << p1.v + dlv1 * lw
          @verts << rv1
        else
          lv = p1.v + p1.dmv * lw
          @verts << p1.v + dlv0 * lw
          @verts << p1.v
          @verts << lv
          @verts << lv
          @verts << p1.v + dlv1 * lw
          @verts << p1.v
        end
  
        @verts << p1.v + dlv1 * lw
        @verts << rv1
      end
    end
  end

bevelフラグが立ってないのにbevel_joinが呼ばれるってどゆこと?って感じだが、inner_bevelが立っていると呼ばれる。line_joinが:miterの時にinner_bevelが立つとここに来るわけだ。なのでif bevelのelse側は:miter指定時の形状がめり込んでるときの特殊処理と考えればたぶん正解だろう。:bevel指定時のinner_bevelはthen側だが、この場合はchoose_bevel内で細工がされているようだ。このへんはNanoVGのコードをそのまま持ってきたような状態なのできちんと理解できているかどうか怪しい。choose_bevelはこんな感じ。

    def choose_bevel(bevel, p0, p1, w)
      if bevel
        [
          p1.v + p0.dv.rperp * w,
          p1.v + p1.dv.rperp * w
        ]
      else
        [
          p1.v + p1.dmv * w,
          p1.v + p1.dmv * w
        ]
      end
    end

round_join

こっちは:miter指定の時に来ないのでちょっと簡単だがその代わりに回転する処理があるのでそのぶん面倒になる。

    def round_join(p0, p1, lw, rw, ncap)
      dlv0 = p0.dv.rperp
      dlv1 = p1.dv.rperp
      if p1.left
        lv0, lv1 = choose_bevel(p1.inner_bevel, p0, p1, lw)
        a0 = Math.atan2(-dlv0.y, -dlv0.x)
        a1 = Math.atan2(-dlv1.y, -dlv1.x)
        a1 -= Math::PI*2 if a1 > a0
        
        @verts << lv0
        @verts << p1.v - dlv0 * rw
        
        n = (((a0 - a1) / Math::PI) * ncap).ceil
        n = 2 if n < 2
        n = ncap if n > ncap
        
        n.times do |i|
          u = i / (n - 1.0)
          a = a0 + u * (a1 - a0)
          rx = p1.v.x + Math.cos(a) * rw
          ry = p1.v.y + Math.sin(a) * rw
          @verts << p1.v
          @verts << Vector.new(rx, ry)
        end

        @verts << lv1
        @verts << p1.v - dlv1 * rw
      else
        rv0, rv1 = choose_bevel(p1.inner_bevel, p0, p1, -rw)
        a0 = Math.atan2(dlv0.y, dlv0.x)
        a1 = Math.atan2(dlv1.y, dlv1.x)
        a1 = Math::PI*2 if a1 < a0
        
        @verts << p1.v + dlv0 * rw
        @verts << rv0
        
        n = (((a1 - a0) / Math::PI) * ncap).ceil
        n = 2 if n < 2
        n = ncap if n > ncap
        
        n.times do |i|
          u = i / (n - 1.0)
          a = a0 + u * (a1 - a0)
          lx = p1.v.x + Math.cos(a) * rw
          ly = p1.v.y + Math.sin(a) * rw
          @verts << Vector.new(lx, ly)
          @verts << p1.v
        end

        @verts << p1.v + dlv1 * rw
        @verts << rv1
      end
    end

結果

面倒だったので:round指定の場合の結果だけ。

miterとかの仕組みはHTML5Canvasと同様なのでこのへんのサイトlineJoinとかmiterLimitとかを調べるとよくわかると思う。そもそもNanoVGはHTML5CanvasライクなAPIなのでかなり似ている。
今回のコードはこちら