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

今回は3次ベジェ曲線を作る。ベジェ曲線というのは始点終点であるアンカー2つと制御点を使って曲線を描画するアルゴリズムである。具体的にはベジェ曲線 - Wikipediaの下のほうの作図法を見てもらうとわかりやすい。また、3次ベジェ曲線体験ツールを作ったのでこれを動かしてもらえると実際どのように描画されるのかが目で見てわかる。アンカー、ハンドル、スライダーをドラッグようにしてあるのでどうすればどうなるかが理解しやすい。ちょっと小さくて使いにくいけど。
あと、ベジエ曲線について - s.h’s pageを見てもらえばいろいろと具体的なことがわかると思う。

tesselate_bezier

ベジェ曲線は任意の位置で2つのベジェ曲線に分割することができるので、NanoVGではこの特性を利用して再帰で二等分していって、直線で表現できるレベルまで細かくする。後は分割された点を配列に格納して直線として描画するだけである。この分割する関数がtesselate_bezierで、このようになる。

    def tesselate_bezier(x1, y1, x2, y2, x3, y3, x4, y4, level, type)
      return if level > 10
    
      dx = x4 - x1
      dy = y4 - y1
      d2 = ((x2 - x4) * dy - (y2 - y4) * dx).abs
      d3 = ((x3 - x4) * dy - (y3 - y4) * dx).abs
    
      if (d2 + d3) * (d2 + d3) < 0.25 * (dx * dx + dy * dy)
        point = Point.new(Vector.new(x4, y4))
        point.corner = type
        @points << point
        return
      end
  
      x12 = (x1 + x2) * 0.5
      y12 = (y1 + y2) * 0.5
      x23 = (x2 + x3) * 0.5
      y23 = (y2 + y3) * 0.5
      x34 = (x3 + x4) * 0.5
      y34 = (y3 + y4) * 0.5
      x123 = (x12 + x23) * 0.5
      y123 = (y12 + y23) * 0.5
      x234 = (x23 + x34) * 0.5
      y234 = (y23 + y34) * 0.5
      x1234 = (x123 + x234) * 0.5
      y1234 = (y123 + y234) * 0.5
    
      tesselate_bezier(x1, y1, x12, y12, x123, y123, x1234, y1234, level + 1, false)
      tesselate_bezier(x1234, y1234, x234, y234, x34, y34, x4, y4, level + 1, type)
    end
  end

大きな曲線を描画するとものすごく細かくなってしまうので、とりあえず10回で再帰を諦めるようになっている。最後の引数はcornerフラグに使われる。ベジェ曲線の途中では線の接続処理はしないという意味になる。このメソッドはSubPathクラスに追加した。ちなみに3次ベジェ曲線体験ツールの描画もこのメソッドを使っている。

ベジェ曲線描画用のコマンドクラスは

  class CmdBezierTo < Struct.new(:h1x, :h1y, :h2x, :h2y, :x, :y);end

となる。ユーザが呼ぶメソッドは

  def bezier_to(h1x, h1y, h2x, h2y, x, y)
    append_commands(CmdBezierTo.new(h1x, h1y, h2x, h2y, x, y))
    self
  end

で、flatten_pathsは

  private def flatten_paths
    @commands.each do |cmd|
      case cmd
      when CmdMoveTo
        @subpaths << SubPath.new # 新規サブパス追加
        point = Point.new(cmd)
        point.corner = true
        @subpaths.last.points << point
      when CmdLineTo
        point = Point.new(cmd)
        point.corner = true
        @subpaths.last.points << point
      when CmdBezierTo
        last = @subpaths.last.points.last.v
        @subpaths.last.tesselate_bezier(last.x, last.y, cmd.h1x, cmd.h1y, cmd.h2x, cmd.h2y, cmd.x, cmd.y, 0, true)
      when CmdClose
        @subpaths.last.closed = true
      end
    end

結果

ベジェ曲線を描画できるようになった。

また、

  def ellipse(cx, cy, rx, ry)
    append_commands(CmdMoveTo.new(cx-rx, cy))
    append_commands(CmdBezierTo.new(cx-rx, cy+ry*KAPPA90, cx-rx*KAPPA90, cy+ry, cx, cy+ry))
    append_commands(CmdBezierTo.new(cx+rx*KAPPA90, cy+ry, cx+rx, cy+ry*KAPPA90, cx+rx, cy))
    append_commands(CmdBezierTo.new(cx+rx, cy-ry*KAPPA90, cx+rx*KAPPA90, cy-ry, cx, cy-ry))
    append_commands(CmdBezierTo.new(cx-rx*KAPPA90, cy-ry, cx-rx, cy-ry*KAPPA90, cx-rx, cy))
    append_commands(CmdClose.new)
    self
  end

このようなメソッドで楕円を描画できるようにもなる。KAPPA90は

  KAPPA90 = 0.5522847493

と定義される定数で円に非常に近いベジェ曲線を描くための係数である。

今回のソースはこちら