ベクタグラフィックスその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を追加すると始点と終点が接続されてループするようになる。