このページではC言語で「円」や「楕円」を描画する方法について解説します。
扇形の描画についても追記しました(2021/11/20)
円を描画するために必要な「ビットマップデータ」や「点の描画方法」については下のページで解説していますので、まだ読んでない方は事前に下のページを読むことをオススメします。
C言語で図形を描画する円の描画も結局は線の描画同様に「点」の描画を円の形になるよう座標を制御してに実行するだけです。ですので、特に点の描画については上のページで理解しておいていただけると本ページの内容も理解しやすくなると思います。
Contents
円を描画する
まずは「円」を描画します。点の描画さえできれば、円の方程式を使えば簡単に描画可能です!
円を描画する考え方
「円内の座標に点を描画する」のが円の描画の考え方です。
つまり、円の原点からの距離が円の半径以内である座標に点を描画していきます。では具体的にその座標はどの座標になるのでしょうか?ここについて解説していきます。
ある座標 (x
, y
) が座標 (x1
, y1
) を中心とした半径 radius
の円上に存在するかどうかは、下記の式で判断することが可能です。習ったことがある方なら懐かしさを感じるかもしれません。円の方程式というやつです。
(x - x1) * (x - x1) + (y - y1) * (y - y1) = radius * radius
要は、左辺では座標 (x
, y
) と円の中心座標 (x1
, y1
) との距離(の2乗)を計算しています。そして、その距離が半径 radius
と一致する(つまり円の方程式が成り立つ)ということは、座標 (x
, y
) は中心座標 (x1
, y1
)・半径 radius
の円上に存在するということになります。
また、その距離が半径よりも小さい場合は、その座標 (x
, y
) は円の内側に存在すると言えます。
つまり、ビットマップデータ上の全座標 (x, y) に対して下の式が成立するかどうかを確認し、成立する場合にその座標に点を描画することを繰り返せば、内側に色を塗った円を描画することができます。
(x - x1) * (x - x1) + (y - y1) * (y - y1) <= radius * radius
スポンサーリンク
円を描画する関数
円を描画する関数は下記のようになります。
void drawCircle(
unsigned char *data, /* ビットマップデータ */
unsigned int width, /* ビットマップの横幅 */
unsigned int height, /* ビットマップの高さ */
unsigned int radius, /* 円の半径 */
unsigned int x1, /* 始点のx座標 */
unsigned int y1, /* 始点のy座標 */
unsigned char r, /* 赤の輝度値 */
unsigned char g, /* 青の輝度値 */
unsigned char b /* 緑の輝度値 */
){
unsigned int x, y;
int dx, dy;
unsigned char *p;
for(y = 0; y < height; y++){
for(x = 0; x < width; x++){
/* 始点と(x, y)座標の距離を計算 */
dx = (int)x - (int)x1;
dy = (int)y - (int)y1;
/* 円を描画 */
if((dx * dx) + (dy * dy) <= radius * radius){
p = data + y * width * 3 + x * 3;
p[0] = r;
p[1] = g;
p[2] = b;
}
}
}
}
例えば、中心座標を (500
, 300
) とした半径 100
の円を描画するためには下記のように drawCircle
関数を実行します。第4引数で半径を、第5引数と第6引数で中心座標を指定しています。
drawCircle(
bitmap.data,
bitmap.width,
bitmap.height,
100,
500, 300,
0xFF, 0x00, 0x00
);
上記の drawCircle
関数のように、このページに載せている関数は、下記ページで紹介した main
関数から呼び出すのがオススメです
libpng をインストールすれば PNG 出力して画像のプレビューも簡単にできます
このページで紹介する描画結果も上記の main
関数で出力した output.png
をプレビューしたものになります
円の描画結果
上記のように drawCircle
を呼び出した際に出力される output.png
は下のようになります。
楕円を描画する
次は「楕円」を描画します。こちらも楕円の方程式を用いれば描画できます。
スポンサーリンク
楕円を描画する考え方
楕円の描画の考え方も、基本的には円の描画と同じです。要は、楕円の内側に存在する座標に点を描画します。
下図のような、座標 (0
, 0
) を中心とし、x 軸上の楕円の線までの距離を rx
、y
軸上の楕円の線までの距離を ry
とした楕円について考えてみましょう(この距離 rx
と ry
は便宜上 x
軸上の半径と y
軸上の半径と呼ばせていただきます)。
この時、下記式を満たす座標 (x
, y
) は、楕円上に存在することになります(楕円の方程式)。
(x * x) / (rx * rx) + (y * y) / (ry * ry) = 1
さらに、楕円の中心を下の図のように (x1
, y1
) に移動させてみましょう。
この時、下記式を満たす座標 (x
, y
) は、中心を座標 (x1
, y1
) とした楕円上に存在することになります。
(x - x1) * (x - x1) / (rx * rx) + (y - y1) * (y - y1) / (ry * ry) = 1
さらに、下記式を満たす座標 (x
, y
)) は中心を座標 (x1
, y1
) とした楕円内に存在することになります。
(x - x1) * (x - x1) / (rx * rx) + (y - y1) * (y - y1) / (ry * ry) <= 1
つまり、中心を座標 (x1
, y1
)、x
軸方向の半径を rx
、y
軸方向の半径を ry
とした楕円の描画(内側は塗りつぶし)は、ビットマップデータ上の全座標 (x, y) に対して上の式が成立するかどうかを確認し、成立する場合にその座標に点を描画していくことで描画できることになります。
楕円を描画する関数
楕円を描画する関数は下記のようになります。
void drawEllipse(
unsigned char *data, /* ビットマップデータ */
unsigned int width, /* ビットマップの横幅 */
unsigned int height, /* ビットマップの高さ */
unsigned int rx, /* x軸状の楕円の半径 */
unsigned int ry, /* y軸状の楕円の半径 */
unsigned int x1, /* 始点のx座標 */
unsigned int y1, /* 始点のy座標 */
unsigned char r, /* 赤の輝度値 */
unsigned char g, /* 青の輝度値 */
unsigned char b /* 緑の輝度値 */
){
unsigned int x, y;
int dx, dy;
unsigned char *p;
for(y = 0; y < height; y++){
for(x = 0; x < width; x++){
/* 始点と(x, y)座標の距離を計算 */
dx = (int)x - (int)x1;
dy = (int)y - (int)y1;
/* 楕円を描画 */
if((double)(dx * dx) / (double)(rx * rx) + (double)(dy * dy) / (double)(ry * ry) <= 1){
p = data + y * width * 3 + x * 3;
p[0] = r;
p[1] = g;
p[2] = b;
}
}
}
}
例えば、中心座標を (500
, 250
) 、x
軸上の半径を 200
、y
軸上の半径を 100
の楕円を描画するためには下記のように drawEllipse
関数を実行します。第4引数と第5引数で x
軸上の半径と y
軸上の半径を、第5引数と第6引数で楕円の中心座標を指定しています。
drawEllipse(
bitmap.data,
bitmap.width,
bitmap.height,
200, 100,
500, 250,
0xFF, 0x00, 0x00
);
楕円の描画結果
上記のように drawEllipse
関数を呼び出した時に出力される output.png
は下のようになります。
スポンサーリンク
扇形を描画する
最後は「扇形」を描画していきたいと思います。扇形は 円を描画する で紹介した円の描画の考え方を応用することで描画することができます。
扇形を描画する考え方
円の描画は円内に存在する座標に対して点を描画をすることで実現できました。
それに対して扇形の描画は、円内かつ、円の中心から伸びる2つの直線の間に存在する座標に対して点を描画することで実現できます。
なので、扇形を描画するためには、座標 (x
, y
) がこの2つの直線の間に存在するのかどうかを判断しながら処理を行なっていく必要があります。
ちょっと難しそうではありますが、これは角度を用いれば簡単に判断できます。
まず、座標 (x
, y
) から円の中心を結ぶ直線と x
軸とのなす角の角度を angle
、さらに2つの直線が x
軸とのなす角の角度をそれぞれ start
と end
とします(なす角は全て “正方向になす角” です)。
y 軸の正方向が下方向のため、角度の正方向が時計回りになっている点に注意が必要です(数学で扱う座標とは縦軸の向きが反対)。
このように角度で表せば、この3つの角度の大小関係の比較により、座標 (x
, y
) がこの2つの直線の間に存在するかどうかが判断できます。
すなわち、下記を満たす場合、座標 (x
, y
) は2つの直線の間に存在すると判断できます。
start <= angle <= end
さらに、座標 (x
, y
) から円の中心を結ぶ直線と x
軸とのなす角の角度に関しては、下記ページで紹介している atan2
関数を利用することで簡単に求めることができます。
なので、扇形は下記のような手順で描画することができることになります。
まず引数で2つの直線の角度 start
と end
を受け取り、さらに atan2
関数を利用して座標 (x
, y
) に対する角度 angle
を求めます。
続いて、3つの角度の大小関係を比較し、座標 (x
, y
) が2つの直線に存在するのかどうかを判断します。もし存在しないのであればその座標に点を描画する必要はありません。
存在する場合、さらに円内にその座標 (x
, y
) が存在するかどうかを判断し、存在する場合のみ、座標 (x
, y
) に点を描画します。
これらを全ての座標に対して実行すれば、扇形を描画することができます。
ただ、扇形を描画する際には何点かの注意点があります。
まず atan2
関数の返却値の角度の単位はラジアンです。ですので、引数で受け取る角度の単位が “度” の場合、単位を一方に統一してから比較を行う必要があります。この単位を統一するためには、下記ページで解説している「ラジアン ⇔ 度」の単位変換が必要になります。
また、atan2
関数の返却値は -π
〜 π
です。引数で受け取る角度が正の値のみである場合などは 0
〜 2π
で扱えるよう、返却値が負の値の場合は +2π
した方が両方の角度の比較がしやすいと思います。
さらに、前述の通り下記の判断式で角度の大小関係を比較するのですが、引数で受け取る start
よりも end
の方が小さい場合、そのまま下記式で判断すると上手く判定できません。必ず不成立することになります。
start <= angle <= end
このような場合は end
に対して +360
or +2π
をして角度の大小関係が start <= end
を満たすようにしてから判断を行う必要があります。で、これを行うと end
の角度が 360
or 2π
以上になるので、atan2
関数の返却値もこの辺りも考慮しつつ判断を行う必要があることになります(atan2
関数の返却値に 2π
を足してから比較するなど)。
扇形を描画する関数
扇形を描画する関数は下記のようになります。atan2
を利用しているので math.h
のインクルードが必要です。
ここまでの説明で用いてきた angle
が、下記関数で使用している変数 prad
に相当します(単位はラジアン)。
void drawSector(
unsigned char *data, /* ビットマップデータ */
unsigned int width, /* ビットマップの横幅 */
unsigned int height, /* ビットマップの高さ */
unsigned int radius, /* 円の半径 */
unsigned int x1, /* 始点のx座標 */
unsigned int y1, /* 始点のy座標 */
unsigned int start, /* 扇形の開始角度(単位は度) */
unsigned int end, /* 扇形の終了角度(単位は度) */
unsigned char r, /* 赤の輝度値 */
unsigned char g, /* 青の輝度値 */
unsigned char b /* 緑の輝度値 */
){
unsigned int x, y;
int dx, dy;
unsigned char *p;
double srad, erad, prad;
/* 引数の角度を0〜359の値に丸め、さらにラジアンに変換 */
srad = PI / 180 * (start % 360);
erad = PI / 180 * (end % 360);
/* 必ずsrad<=eradが成立するようにする */
if (srad > erad) {
erad += 2 * PI;
}
for(y = 0; y < height; y++){
for(x = 0; x < width; x++){
/* 円の中心と(x, y)座標の差を計算 */
dx = (int)x - (int)x1;
dy = (int)y - (int)y1;
/* 円の中心と(x,y)座標を結ぶ直線の角度を取得 */
prad = atan2(dy, dx);
/* 0〜2πの値に丸める */
if (prad < 0) {
prad += 2 * PI;
}
/* 角度sradと角度pradの直線の間に存在するかどうかを判断 */
if (prad < srad || prad > erad) {
/* srad<=prad<=eradを満たさなかった場合 */
if (erad >= 2 * PI) {
/* eradが2π以上の場合はpradも2π足した状態でも判断する */
if (prad + 2 * PI < srad || prad + 2 * PI > erad) {
/* 2つの直線の間に存在しないので次の座標の描画に移る */
continue;
}
} else {
/* 2つの直線の間に存在しないので次の座標の描画に移る */
continue;
}
}
/* 座標(x,y)が半径radiusの円内に存在するかどうかを判断 */
if((dx * dx) + (dy * dy) <= radius * radius){
/* 座標(x,y)は円内に存在する&2つの直線の間に存在するので点を描画 */
p = data + y * width * 3 + x * 3;
p[0] = r;
p[1] = g;
p[2] = b;
}
}
}
}
例えば、中心座標を (500
, 300
)、半径 100
、扇形の開始角度 180
度、終了角度 45
度の扇形を描画するためには下記のように drawSector
関数を実行します。第4引数で半径を、第5引数と第6引数で中心座標を、第7引数と第8引数で開始角度と終了角度をそれぞれ指定しています。
drawSector(
bitmap.data,
bitmap.width,
bitmap.height,
100,
500, 300,
180, 45,
0xFF, 0x00, 0x00
);
スポンサーリンク
扇形の描画結果
上記のように drawSector
関数を呼び出した時に出力される output.png
は下のようになります。
角度の正方向が数学等で扱う座標と逆で気持ち悪いという場合は、drawSector
関数の atan2
関数の戻り値に -
を付加してやれば良いです。
prad = -atan2(dy, dx);
この場合、上記の引数で drawSector
関数を実行した時に出力される output.png
は下のようになります。
まとめ
このページでは円と楕円を描画する方法の解説と円を描画する関数、楕円を描画する関数の紹介を行いました。
昔ならった無理やり覚えた円の方程式や楕円の方程式も、プログラミングで実際に円や楕円を描画してみると、楽しみながら方程式を理解できたのではないかと思います。
図形を描画したりするためには数学的要素が必要だったりしますが、プログラミングで実際に図形を描画するとすぐに結果が確認できて楽しいですし、数学についての理解も進んでオススメですよ!