SVG:曲線の表現

作成日 : 2001-01-21
最終更新日 :

SVG の曲線

SVG で規定されている曲線は次の 3 種類のみである。

  1. 楕円弧
  2. 3 次のベジエ曲線(3 次曲線)
  3. 2 次のベジエ曲線(放物線)

だから任意の曲線を描くには、曲線上の点を密にとって折れ線近似をするか、最初の 3 種類の曲線で近似するしかない。 下の図は、緑色が 2 次のベジエ曲線(放物線)である。 橙色は、制御点から通過点に引いた線で、これは通過点における接線になっていることに注意する(もともとそのようにベジエ曲線が作られている)。

上記を具体的に説明する。仮に描きたい放物線が `y(x) = -px^2 + q` と表されるとする。さて、上記の図では、 x 軸との交点は `(sqrt(q/p),0)`、 y 軸との交点は `(0, q)` である。このとき、`(sqrt(q/p), 0)` における放物線の接線の傾きは `-2p sqrt(q/p) = -2sqrt(pq)` であり、 したがって接線の方程式は、`y = - 2 sqrt(pq) (x - sqrt(q/p))` であることがわかる。制御点の y 座標は q である。x 座標は、
`q = - 2 sqrt(pq) (x - sqrt(q/p))`
を `x` について解いて、
`x = 1/2 sqrt(q/p)`
を得る。上記の図ではピクセル単位で、`q = 300, sqrt(q/p) = 300` であり、したがって制御点は (150, 300) にとればよいことになる。

なお、SVG での曲線として制御点(x1, y1)を利用して(x, y)まで3Dスプライン曲線または2Dスプライン曲線を作成できる、 としているページがある。たとえば、 SVGの基本演習(リンク切れ、www.envinfo.uee.kyoto-u.ac.jp) などである。 しかし、SVG で作成できるのは 3D ベジエ曲線それから2Dベジエ曲線のみである。狭い意味でのスプライン曲線、 すなわち制御点をすべて通るタイプの曲線は SVG では描けない。 SVG のドラフトでは path データの R コマンドで Catmull-Rom スプライン曲線を描くようになっているが、 現在実装されていない(少なくとも 2022 年 2 月現在の Seamonkey と Vivaldi では)。

なお、放物線が2次のベジエ曲線と同じであることはつい最近知った。これを利用して、 巨人の星の物理学にある軌跡の図を書くことができた。 また、3 次のベジエ曲線で放物線を作る方法もある。これについては、 池田洋介さんの イラストレーターで正確な放物線を作図する方法(iky.no-ip.org、現在リンク切れ)を参照のこと。

SVG でどうやって書けばいいのかわからない曲線

SVG での書き方がわからない直線がある。

たとえば、サインカーブ、つまり正弦曲線がある。当然、多項式近似しかできないので、書けない。 `sin x` を `x = 0` のまわりでテイラー展開して 3 次の項までとれば、`sin x ~= x - (x^3)/6` の結果は得られるが、 だからといってこれを使ってきれいなサインカーブが書けるかというと、書けない。コサインであっても位相がずれるだけである。 どこまで近似できるかはわからない。

また、指数関数、対数関数も不可能である。

双曲線も描画できない。同じ円錐曲線ではあるのに、 楕円や放物線ができて双曲線ができないのは不公平な気がするが、 仕方がない。

SVG による曲線の近似

最初に、SVG で描けるのは楕円弧と 3 次多項式の曲線といった。ではほかの曲線が書けないのか。 近似的な方法に挑戦してみる。

正弦曲線

1 O π/2

<path stroke="red" stroke-width="1" fill="none" d="M -157,100 C -57,100, 57,-100, 157,-100" />
これはhttps://stackoverflow.com/questions/13932704/how-to-draw-sine-waves-with-svg-js をもとに改変した。
なぜ制御点の位置が(-57, 100) と (57, -100)なのか。それは下記を見てほしい。
参考 : HMMNRST 氏による「ベジェ曲線の制御点と端点の曲率」
https://qiita.com/HMMNRST/items/39f77e5051df2928eb93

指数曲線

長らく考えてみたが、近似的に書く意味が見出せなくなったのであきらめた。

結び目

SVG を使って結び目を書こうとしたが、うまく書けない。複数の点を順番に、滑らかにつないで曲線にして、 書き始めた最初の点を最後の点に重ねる、人間なら普通にできることだが、コンピュータにはうまくできない。 特に、SVG で書こうとしたときに、どうすべきかわからない。以下、考えたことを書いてみる。

SVG の曲線は、楕円弧か、3 次のベジエ曲線か、2 次のベジエ曲線である。 楕円弧は指定した点を通る書き方が難しい。ベジエ曲線は制御点の指定が難しいが、 制御点さえ指定すればなめらかな曲線が描ける。 まず、制御点の指定が少なくて住む 2 次ベジエ曲線で描画することを考える。

2 次ベジエ曲線では、隣接する点を結ぶときに、制御点を 1 つ設定する必要がある。 ただし、T コマンドを使えば、 新たな制御点は前の制御点との対称の位置に自動的に指定されるので、 手間は最初の制御点を設定するだけで済む。問題は、最初の制御点の位置である。 制御点の位置によっては、 始点 = 終点が滑らかではなく、角ばってしまう。 以下は、Q コマンドを 1 回だけ使い、 残りは T コマンドを使って指定した点を結ぶ 2 次ベジエ曲線を示す。大きな黒丸は始点 = 終点を表わす。

なめらかな例

かどばる例


ではどのような場合になめらかになって、どのような場合にかどばるのかを考えてみる。 以下、`a_i, p_i` はベクトルとする。 点列が `a_1, a_2, cdots, a_n` のように表わされているとする。 `a_1` と `a_2` の間を結ぶ曲線を描くための制御点がはじめに `p_1` で指定されているものとする。次の制御点 `p_2` は 点 `a_2` に関して 点 `p_1` と対称の位置にあるので、 `a_2 = 1/2 (p_1 + p_2)` すなわち `p_2 = 2a_2 - p_1` である。以下、`p_3, p_4, cdots ` も順次定まる。 そして、最後に得られた制御点 `p_n` が得られる。 さて、点 `a_1` と `a_2` を結ぶ曲線の `a_1` における方向ベクトルは、 `p_1` によって定まり、 制御点 `p_1` の性質から `p_1-a_1` である。一方、点 `a_1` と `a_n` を結ぶ曲線の `a_1` における方向ベクトルは `p_n` によって定まり、 `p_n-a_1` であり、点 `a_1` での曲線がなめらかになるためには、 両者の方向ベクトルが平行でなければならない。そこで、`k` を 0 でない実数として、 `p_1-a_1 = k(p_n - a_1)` が成り立つように `p_1` を定める必要がある。 具体的に、`a_i = (b_i, c_i), p_i = (q_i, r_j)` と座標表示されていれば、 次の式が成り立てばよい。ここで、分母は 0 ではないとする。

`(q_1 - b_1)/(b_1 - 2b_2 - cdots - 2b_n) = (r_1 - c_1)/(c_1 - 2c_2 - cdots - 2c_n)`
上の式から、`q_1` を定めれば `r_1` が定まること、あるいは `r_1` を定めれば `q_1` が定まることがわかる。

この式にしたがって `q_1` を固定して `r_1` を求めて曲線を描いても、 期待する曲線にならないばかりか、 実際には始点=終点で滑らかにならなかったり、曲線の乱れが激しくなることがある。 特に、隣接する点間の距離の相違が著しいときに顕著だ。 この理由としては、制御点が `p_1` から `p_2, p_3, cdots, p_n` と伝播するにしたがって、 制御点が `a_i` と `a_(i+1)` の中間あたりに来ることが少なくなり、 その結果曲線が歪んでしまうためと考えられる。

ということは、2 次ベジエ曲線を使い、かつ現制御点を前制御点の対称点とする方法は適用が難しそうだ。

では、2 次ベジエ曲線で制御点を逐次明示的に定める方法を考えてみよう。制御点を明示的に定めるには、 仮に 点 `a_i` を滑らかな曲線でつなぐことができたとすれば、 点 `a_i` の接線と 点 `a_(i+1)` の接線の交点にすればいいわけだ。各点の接線でそれらしいものを考えよう。 ここで、`i` を固定する。 たとえば、点 `a_1` での接線としては、ベクトル `a_1 - a_0` とベクトル `a_2 - a_1` の平均、 すなわち、`1/2 ((a_1-a_0) + (a_2-a_1)) ` が考えられる。このベクトルの方向は `a_2-a_0` に等しい。同様に、ベクトル `a_2` の接線の方向ベクトルは、`a_3-a_1` がもっともらしい。 これらのことから、`a_1` と `a_2` を結ぶ 2 次ベジエ曲線の制御点 `p_1` は、`a_2-a_0` と `a_3 - a_1` が 1 次独立であれば、`s, t` を実数として次の式が成り立つ。

`p_1 = a_1 + s(a_2-a_0) = a_2 + t(a_3-a_1)`
これを解くために、`d_i = d_(i+1)-d_(i-1)` などと表わし、`a_i` の `x` 成分や `y` 成分をそれぞれ `a_(ix), a_(iy)` などと書く。 すると次の式が得られる。
`[(a_(1x)),(a_(1y))] + s [(d_(1x)),(d_(1y))] = [(a_(2x)),(a_(2y))] + t [(d_(2y)),(d_(2y))]`
`[[d_(1x), -d_(2x)], [d_(1y), -d_(2y)] ] [(s),(t)]=[(-a_(1x)+a_(2x)),(-a_(1y)+a_(2y))]`
`[(s),(t)] = 1/D [(-d_(2y), d_(2x)),(-d_(1y),d_(1x))] [(-a_(1x)+a_(2x)),(-a_(1y)+a_(2y))], `
`D = - d_(1x) d_(2y) + d_(2x) d_(1y)`
`a_2-a_0` と `a_3 - a_1` が 1 次独立であるという条件は、上記の行列式 `D` がゼロでないということに対応している。 では`a_3-a_1` と `a_2 - a_0` が 1 次独立でないときはどうするかというと、 その場合は `p_1 = 1/2(a_1+a_2)` としていいだろう。

ということで、以上の式を計算してみた。入力となる点の座標はすべて 0 以上 100 以下の整数であるとする。 求める `p_1` について、パラメータ `s` から計算した `p_1` を `p_(1s)` とし、 パラメータ `t` から計算した `p_1` を `p_(1t)` とする。両者は一致するはずである。

`a_i``a_0``a_1``a_2``a_3`
`a_(ix)`
`a_(iy)`

`d_(ix)` `d_(1x)` `d_(2x)` `-a_(1x)+a_(2x)`
`d_(iy)` `d_(1y)` `d_(2y)` `-a_(1y)+a_(2y)`
D: s: t:
`p_(isx)`: `p_(itx)`:
`p_(isy)`: `p_(ity)`:

数式の表現

数式の記述は ASCIIMathML を、表現は MathJax を用いている。

まりんきょ学問所コンピュータの部屋マーク付け言語手習い > SVG: 曲線の表現


MARUYAMA Satosi