…というのは調べて分かった建前で、ふと必要になったから調べただけです。
Paths - SVG | MDN から引用。
上部分と下部分に分けて描画します (左右でもいい)。 ここでは横の半径が 40、縦の半径が 30 の楕円を描きます (rx=40, ry=30)。 現在の位置と x, y (終点) と縦横の半径の整合性が取れてないと正しく描画されません。
M10,10 A40,30 0,1,0 90,10
M90,40 A40,30 0,1,0 10,40
実際のコードが生成するパスは以下のようになります (CX,CY = 円の中心の X,Y 座標)。
M (CX - rx) CY A rx ry 0 1 0 (CX + rx) CY A rx ry 0 1 0 (CX - rx) CY
実際のコードは以下の通りです (該当の箇所)。
ここではパスを表す二次元配列を返しています。全て半角空白かカンマで join すると SVG のパス文字列が出来上がります。
param.figure
が "circle" なら真円で、"ellipse" なら楕円のパス文字列を生成します。
drawCircle(param) { const rx = param.rx; const ry = param.figure === 'circle' ? rx : param.ry; return [ ['M', CX - rx, CY], ['A', rx, ry, 0, 1, 0, CX + rx, CY], ['A', rx, ry, 0, 1, 0, CX - rx, CY], ]; }
Illustrator でも使われているらしい方法。
なぜ4本かというと
Paths - SVG | MDN から引用。
M10,40 C10,23.43 17.90,10 40,10
M10,10 C32.09,10 50,23.43 50,40
M50,10 C50,26.56 32.09,40 10,40
M50,40 C27.90 40 10 26.56 10 10
実際のコードが生成するパスは以下のようになります。
M (CX - rx) CY C (CX - rx) (CY - KAPPA * ry) (CX - KAPPA * rx) (CY - ry) CX (CY - ry) C (CX + KAPPA * rx) (CY - ry) (CX + rx) (CY - KAPPA * ry) (CX + rx) CY C (CX + rx) (CY + KAPPA * ry) (CX + KAPPA * rx) (CY + ry) CX (CY + ry) C (CX - KAPPA * rx) (CY + ry) (CX - rx) (CY + KAPPA * ry) (CX - rx) CY
ここで KAPPA という謎の変数が出現しますが、これは 0.55228... (= (-1 + sqrt(2)) / 3 * 4) という値です。 詳しくは
実際のコードは以下の通りです (該当の箇所)。
drawCircle(param) { const rx = param.rx; const ry = param.figure === 'circle' ? rx : param.ry; return [ ['M', CX - rx, CY], ['C', CX - rx, CY - KAPPA * ry, CX - KAPPA * rx, CY - ry, CX, CY - ry], ['C', CX + KAPPA * rx, CY - ry, CX + rx, CY - KAPPA * ry, CX + rx, CY], ['C', CX + rx, CY + KAPPA * ry, CX + KAPPA * rx, CY + ry, CX, CY + ry], ['C', CX - KAPPA * rx, CY + ry, CX - rx, CY + KAPPA * ry, CX - rx, CY], ]; }
Paths - SVG | MDN から引用。
なぜ8本かという記事はありませんでしたが、おそらく4本の時と同じく誤差の問題だと思います。
Approximating Cubic Bezier Curves in Flash MX
の「Flash MX」にある「4 Quadratic curves (4本の2次ベジェ曲線)」の図を見ると少し歪んでるのが目で見ても分かるぐらいなので。 4本の2次ベジェ曲線 実際の円
M50,10 Q50,22.42 38.28,31.21
M38.28,31.21 Q26.56,40 10,40
M40,40 Q33.43,40 21.71,31.21
M21.71,31.21 Q10,22.42 10,10
M10,40 Q9.99,27.57 21.71,18.78
M21.71,18.78 Q33.43,10 49.99,10
M10,10 Q26.56,9.99 38.28,18.78
M38.28,18.78 Q50,27.57 50,39.99
実際のコードが生成するパスは以下のようになります。
M (CX + rx) CY Q (controlX(theta) + CX) (controlY(theta) + CY) (anchorX(theta) + CX) (anchorY(theta) + CY) Q (上と同じ引数のものが7個続く)theta はそれぞれの角度 (ラジアン) です。例えば第1象限 (下) だったら 1/4 * π、第1象限 (上)だったら 1/2 * π といった値です。 controlX, controlY, anchorX, anchorY はそれぞれ theta を渡すと制御点と終点の座標を返します。
実際のコードは以下の通りです (該当の箇所)。
/** * http://www.fumiononaka.com/TechNotes/Flash/FN0506002.html */ drawCircle(param) { const rx = param.rx; const ry = param.figure === 'circle' ? rx : param.ry; const SEGMENTS = 8; const ANGLE = 2 * Math.PI / SEGMENTS; const anchorX = theta => rx * Math.cos(theta); const anchorY = theta => ry * Math.sin(theta); const controlX = theta => anchorX(theta) + rx * Math.tan(ANGLE / 2) * Math.cos(theta - Math.PI / 2); const controlY = theta => anchorY(theta) + ry * Math.tan(ANGLE / 2) * Math.sin(theta - Math.PI / 2); return [ ['M', CX + rx, CY], ... this.range(1, SEGMENTS).map(index => { const theta = index * ANGLE; return ['Q', controlX(theta) + CX, controlY(theta) + CY, anchorX(theta) + CX, anchorY(theta) + CY]; }) ]; } range(from, to) { const d = to - from; return [...Array(d + 1).keys()].map(n => n + from); }