多角形を描くには、三角関数を活用する
以前「そのまま使えるランダム画像」を作成しましたが、星を描くのに実はかなり煩雑な計算式で無理やり頂点を算出しておりました。
何かもっと効率がよく、かつ、応用の利く計算式は書けないものだろうかと思索を巡らせていたら、そういえば高校数学で習ってたなぁと・・・。
そう思ってネットを彷徨いながら数式とにらめっこして、ようやく思い出した三角関数なのでした。
多角形の各頂点は、多角形の外接円の円周上にあります(当たり前ですが)。
円周上の点の座標は、円心を原点とした場合に「(rcosθ, rsinθ) r: 半径」で表すことができます。
画像のキャンバスは、左上が「(0, 0)」であり、右へ行くほどxが、下へ行くほどyが、それぞれ増加していきます。
従って、画像のキャンバス座標に置き換えるには、「(r + rcosθ, r - rsinθ) r: 半径」となります。
「(rcosθ, rsinθ) r: 半径」によって得られる0度のときの座標は、「x = r、y = 0」となりますので、0度から順番に座標を算出していくと、どうしても1番目の頂点が右になる多角形が描かれてしまいます。頂点の数が奇数の多角形の場合に、横を向いた感じになるというわけです。
そこで、1番目の頂点を上に持ってくるには、θに90度プラスするか、xとyを逆転するかのいずれかとなります。
従って、「(r + rsinθ, r - rcosθ) r: 半径」となります。
あとは、「θ = 360 / v v: 頂点の数」に、「0, 1, ... , v - 1」を順次掛けていけば、各頂点の座標を特定することができます。
プログラムでは、弧度法を使用する
ほとんどのプログラム言語には、三角関数が標準で備わっております。
C#も例外ではなく、System.Mathクラスのstaticメソッドとして用意されております。
var __sin = Math.Sin(角度);
という構文ですが、角度として指定する値は、日常生活ではおよそ縁のない「弧度法」を使用します。
弧度法は、その角度で切った円弧の長さを半径で割った値で角度を表す方法で、180度がπとなります。
360度が2π、90度がπ/2というわけです。
πは、同じくSystem.Mathクラスの定数として用意されております。
従って、先ほどの「θ = 360 / v v: 頂点の数」は、「θ = 2π / v v: 頂点の数」に置き換えられ、これに「0, 1, ... , v - 1」を順次掛けていくことになります。
星形を考える
例えば、正5角形の頂点を、1, 3, 5, 2, 4の順に結ぶと、誰もが知っている星を描くことができます。
この星は、対角線の交点を頂点とした10角形でもありますが、C#では、正5角形の頂点の順番を並べ替えて塗りつぶすことでも描くことが可能です。
これは、正7角形、正9角形、正11角形などの頂点の数が奇数の多角形で、奇数番目を順に結んだあとで偶数番目を順に結ぶ形をとることで可能になります。
ところが、正4角形、正6角形、正8角形、...などの頂点の数が偶数の多角形では、この方法は使えません。
頂点の数が奇数の多角形では対角線の交点という「明確な座標」が決まっているのに対し、偶数の多角形では決まっていないという問題もあります。
「何が答えかわからないものは描かない」という立場に立って、ここでは頂点の数が偶数の多角形の星形は、描かないことにいたします。
System.Drawing.Pointクラス配列の落とし穴
クラスを配列にすれば、大抵この落とし穴にはまるものではありますが。
System.Drawing.Pointクラスの配列には、個々のSystem.Drawing.Pointクラスのポインタが配列として記録されているだけですから、“=”で繋いでコピーしただけではデータを丸ごとコピーしたことにはなりません。
ポインタをコピーすれば、コピー前後のポインタが指し示す実データの在り処は同じであり、その実データを書き換えれば両方とも書き換え後の実データを参照するというわけです。
例えば、配列aを配列bに“=”で繋いでコピーして、配列bの何番目かの座標を変えてみると、配列aも更新されていることがわかると思います。
b = a; ではなく、b = a.ToArray(); とすることで、しっかり中身をコピーすることができます。
まとめ
本項のまとめですが、次の構造体コードをパクっていただくのが最も手っ取り早いと思います。
毎度のことではございますが、みなさまご自身の判断と責任において、ご自由にご活用ください。
弊社は、みなさまがご利用された結果発生した損害について、一切の責任を負いません。
public struct PolygonPoints
{
public readonly int Radius;
public readonly int VertexCount;
public readonly bool Star;
private System.Drawing.Point[] __vertexPoints;
public PolygonPoints (int Vertexes, int Radius, bool Star)
{
this.Radius = Radius;
this.VertexCount = Vertexes;
this.Star = this.VertexCount >= 5 && this.VertexCount % 2 == 1 ? Star : false;
this.__vertexPoints = new System.Drawing.Point[this.VertexCount];
for (int i = 0; i < this.VertexCount; i++)
{
this.__vertexPoints[i] = new System.Drawing.Point(
this.Radius + Convert.ToInt32(Math.Round(this.Radius * Math.Sin(Math.PI * 2 / this.VertexCount * i))),
this.Radius - Convert.ToInt32(Math.Round(this.Radius * Math.Cos(Math.PI * 2 / this.VertexCount * i))));
}
if (Star)
{
var __newVertexesPoints = new System.Drawing.Point[this.VertexCount];
var j = 0;
for (int i = 0; i < this.VertexCount; i += 2)
__newVertexesPoints[j++] = this.__vertexPoints[i];
for (int i = 1; i < this.VertexCount; i += 2)
__newVertexesPoints[j++] = this.__vertexPoints[i];
this.__vertexPoints = __newVertexesPoints;
}
}
public System.Drawing.Point[] VertexPoints()
{
return this.__vertexPoints;
}
public System.Drawing.Point[] VertexPoints(System.Drawing.Point Offset)
{
var __retPoints = this.__vertexPoints.ToArray();
for (int i = 0; i < __retPoints.Length; i++)
__retPoints[i].Offset(Offset);
return __retPoints;
}
public System.Drawing.Point[] VertexPoints(int OffsetX, int OffsetY)
{
var __retPoints = this.__vertexPoints.ToArray();
for (int i = 0; i < __retPoints.Length; i++)
__retPoints[i].Offset(OffsetX, OffsetY);
return __retPoints;
}
public override string ToString()
{
return this.ToString(this.__vertexPoints);
}
public string ToString(System.Drawing.Point Offset)
{
return this.ToString(this.VertexPoints(Offset));
}
public string ToString(int OffsetX, int OffsetY)
{
return this.ToString(this.VertexPoints(OffsetX, OffsetY));
}
private string ToString(System.Drawing.Point[] _vertexPoints)
{
var __ret = new System.Text.StringBuilder();
foreach (var __vertexPoint in _vertexPoints)
__ret.AppendLine(__vertexPoint.ToString());
return __ret.ToString();
}
}