円を描く

(3)サンプル・プログラム

ライン・ルーチンの場合と同様に、以前とは異なる内容に変更したいと思います。x68k用に作成したサンプルのプログラムは以下のリンク先からダウンロードできます。

zip形式(gcircle.zip)

サンプル・プログラムでは、a( x - x0 )2 + b( y - y0 )2 = r2の各パラメータを入力することで画面に楕円を描画します。IOCSコールで使用できるルーチンでは、偏平率を 0から 65280の数値で指定し(256で偏平率は 1、即ち真円)、横長の楕円なら横方向、縦長の場合は縦方向の半径を与えるようになっていますが、サンプル版では純粋に楕円方程式の各パラメータを渡すようになっているため円の大きさがどの程度になるか予測しづらくなってます。また、IOCS版にある描画開始・終了角度もありません。

この章では、円や楕円を描画するプログラムにいくつかの機能を追加して、いくつかの応用的な利用ができるようにしたいと思います。すでに、線分描画ルーチンの章で、点描画ルーチンを関数オブジェクトにすることによる描画方法の切り替え方法については説明してありますので、ここでは円弧描画ルーチン専用の特殊機能について示します。


1) 楕円の塗りつぶし

まずは、楕円を塗りつぶすアルゴリズムの紹介です。実現方法は非常に簡単で、楕円が左右対称であることを利用して、円周上の一点のみを描画する代わりに両端を線分で結ぶようにすれば塗り潰しができます。

以下に、点描画を関数オブジェクト化した上で塗りつぶし機能を追加した楕円描画のサンプル・プログラムを示します。

/*
  Coord : 座標定義用構造体
*/
template<class T> struct Coord
{
  T x;
  T y;

  // コンストラクタ
  Coord( T _x = 0, T _y = 0 ) : x( _x ), y ( _y ) {}

  bool operator==( const Coord& c ) const { return( x == c.x && y == c.y ); }

  double distance( const Coord& c ) const
  {
    return( sqrt( pow( (double)( c.x - x ), 2 ) + pow( (double)( c.y - y ), 2 ) ) );
  }
};

/*
  GPSetBase : 点描画関数オブジェクト用 基底(抽象)クラス
*/
class GPSetBase
{
public:
  virtual void operator()( const Coord<int>& c ) = 0;
  virtual ~GPSetBase() {}
};

:
(様々な点描画用関数オブジェクト)
:

/*
  GEllipse : 楕円描画用クラス
*/
class GEllipse
{
  Coord<int> origin;     // 中心座標
  unsigned int r;        // 半径
  Coord<unsigned int> k; // 係数
  bool fillMode;         // 塗りつぶしモード

public:

  // コンストラクタ
  GEllipse( const Coord<int>& o, unsigned int _r, const Coord<unsigned int>& _k, bool _fillMode = false )
    : origin( o ), r( _r ), k( _k ), fillMode( _fillMode ) {}

  // 楕円描画関数
  void operator()( GPSetBase& pset );
};

/*
  楕円描画関数

  a( x - origin.x )^2 + b( y - origin.y )^2 = r^2を描画する

  GPSetBase& pset : 点描画用関数オブジェクト
*/
void GEllipse::operator()( GPSetBase& pset )
{
  if ( r == 0 ) return; // 半径が0なら描画しない

  Coord<int> c0( (int)( (double)r / sqrt( (double)( k.x ) ) ), 0 ); // 描画開始座標
  double d = sqrt( (double)( k.x ) ) * (double)r;
  int F = (int)( -2.0 * d ) +     k.x + 2 * k.y;
  int H = (int)( -4.0 * d ) + 2 * k.x     + k.y;
  bool drawn = false; // 対象のY座標がすでに描画済みか(塗りつぶしモードの場合に有効)

  while ( c0.x >= 0 ) {
    // 塗りつぶしモード
    if ( fillMode && ( ! drawn ) ) {
      for ( int x = origin.x - c0.x ; x <= origin.x + c0.x ; ++x ) {
        pset( Coord<int>( x, origin.y - c0.y ) );
        if ( c0.y != 0 ) pset( Coord<int>( x, origin.y + c0.y ) );
      }
    // 通常の円弧描画
    } else {
      pset( Coord<int>( origin.x + c0.x, origin.y + c0.y ) );
      if ( c0.x != 0 ) pset( Coord<int>( origin.x - c0.x, origin.y + c0.y ) );
      if ( c0.y != 0 ) {
        pset( Coord<int>( origin.x + c0.x, origin.y - c0.y ) );
        if ( c0.x != 0 ) pset( Coord<int>( origin.x - c0.x, origin.y - c0.y ) );
      }
    }
    if ( F >= 0 ) {
      --( c0.x );
      F -= 4 * k.x * c0.x;
      H -= 4 * k.x * c0.x - 2 * k.x;
      drawn = true;
    }
    if ( H < 0 ) {
      ++( c0.y );
      F += 4 * k.y * c0.y + 2 * k.y;
      H += 4 * k.y * c0.y;
      drawn = false;
    }
  }
}

内容としては、前回紹介した楕円描画のルーチンに塗りつぶしモードを追加しただけですが、点描画を関数オブジェクト化したり、座標値を構造体で渡すなど、細かい部分で変更されている箇所があります。塗りつぶしモードでは、描画対象の Y座標がすでに描画された領域かどうかをチェックして、すでに描画されていたら処理をスキップするようにしています。これは、二度塗りを避けることでパフォーマンスを向上させる以上に重要な意味があり、例えば色反転描画を行う場合、塗りつぶしが繰り返されると正常に反転できない(元に戻ってしまう)場合が発生するため、それを防ぐ目的でこのような処理を行っています。通常の円弧描画において、対称な座標に対して描画する場合も同様に、二度描画するのを防ぐために c0の値をチェックしています。

座標を表す構造体ではテンプレート(template)が利用されています。テンプレートは、一言で言うと型をパラメータとして扱う手法で、データ型に依存しないプログラミング(ジェネリック・プログラミング; generic programming)を実現することを目的とした機能になります。テンプレートは C++独自の機能でしたが、JavaC#では「ジェネリクス(Generics)」という名称で似たような機能が最近追加されるようになりました。なお、テンプレートを利用したライブラリとしては STL(Standard Template Library)が有名で、テンプレートの有用性を示した好例といえます。
テンプレートがいかに便利かは、次の例を見れば簡単に理解できると思います。

template<class T> T max( const T& t1, const T& t2 )
{
  return( ( t1 > t2 ) ? t1 : t2 );
}

void f()
{
  max( 1, 2 );
  max( 't', 'u' );
  max( 2.3, 3.1 );
  max<double>( 1, 2.1 );
  max<int>( 'a', 23 );
}

maxは、引数として渡された二つの値を比較して、大きい方を返すテンプレート関数です。データ型の引数として Tが渡されています。このように記述しておけば、データ型が比較用の演算子">"を利用することができる限り、コンパイラが各データ型専用の関数を自動的に作成してくれるので、各データ型用の max関数を用意する必要がなくなります。同じような例で、比較や集計を行う関数や、値を交換する関数でも有用であることが理解できると思います。テンプレートは構造体やクラスに対しても利用できるので、特にコンテナクラス(コレクションクラス)ではテンプレートがその威力を発揮します。

template<class T> class m_array
{
  T* tp;

public:

  m_array( unsigned int sz ) { tp = (T*)malloc( sz * sizeof( T ) ); }
  void change_size( unsigned int sz ) { tp = (T*)realloc( tp, sz * sizeof( T ) ); }
  T& operator[]( unsigned int i ) { return( tp[i] ); }
  ~m_array() { free( tp ); }
};

void g()
{
  m_array<char> charArray( 100 );
  m_array<int> intArray( 100 );
  m_array<double> dblArray( 100 );
}

テンプレートは、静的な型チェックを必要とする C++でジェネリック・プログラミングを実現させるための強力な手段になる反面、直感的でわかりやすいプログラミングが困難になり、コンパイラが吐き出すエラーの内容も慣れるまでに時間がかかるため、開発が非常に難しいという問題があります。また、パラメータとしての型の数が増えればその分実行ファイルが大きくなるため、安易に使うと実行ファイルのサイズが急激に大きくなるといった現象も発生します。そのため、積極的に利用する人もいれば、全く利用しない人もいるようです。
STLを利用する限り、テンプレートを避けてプログラムを作成することはできません。また、STLなしで C++を利用したプログラムを作成するのも大変なので、内容を理解しておくことはある程度必要になります。しかし、あらゆる関数やクラスに対してジェネリック・プログラミングを実現させようとしてテンプレートを大量に使うと、思わぬ落し穴が待ち受けていることにもなりかねないので、個人的には「ほどほどに利用する」のが一番かと考えています。


2) 円の中心を求める

円の描画には中心と半径をパラメータとして渡す必要があります。マウスなどのポインティングデバイスを使って円を描画する操作を考えたとき、中心と円周上の一点で描画する円を決める方法だけではなく、二点を直径とする円を描いたり、円周上のいくつかの点を選択して描画するなどのやり方を利用したい場合もあります。
二点を直径とする円を描画する場合、選択した二点の中点を求めれば中心が得られ、二点の距離の半分が半径となるので、パラメータを求めるのは簡単にできますが、円周上の点を選択して描画する場合、いくつの点を選択すればひとつの円を決めることができるのでしょうか。
平面上の二点(p1,p2)を通る円の書き方は無限にありますが、円の中心は、p1p2を結んだ線分の垂直二等分線上に必ずあります。円周上の点である二点は円の中心から等しい距離にあり、二点からの距離が等しい点の集合は、二点を結んだ線分の垂直二等分線になるからです。ここで、二点を結ぶ直線上にない一点 p3を決めると、例えば p1p3を通る円の中心も、二点を結ぶ線分の垂直二等分線にあることになります。従って、二本の垂直二等分線の交点が、三点を通る円の中心ということになります。直線の交点は、それらが平行でない限り必ず一点のみで交わるので、円もただひとつに決まります。この円を、三点を頂点とする三角形の外接円といい、その中心を外心といいます。

三角形の外接円

任意の三点から円の中心を求めるサンプル・プログラムを以下に示します。

/*
  double型のデータを小数点以下四捨五入してint型として返す
*/
int roundHalfUp( double d )
{
  if ( d > 0 )
    d += 0.5;
  else
    d -= 0.5;

  return( (int)d );
}

/*
  三点間の距離の中で最大のものを抽出してその中点と距離の半分を求める
  (三点が同一線分上にあった場合の中心と半径を求める)

  const Coord<int>& c1, c2, c3 : 三点の座標
  unsigned int& d : 中点と両端の点との距離

  戻り値 : 距離が最大になる二点の中点
*/
Coord<int> GCircle::calcMidPoint( const Coord<int>& c1, const Coord<int>& c2, const Coord<int>& c3, unsigned int& d )
{
  double d12 = c1.distance( c2 );
  double d23 = c2.distance( c3 );
  double d31 = c3.distance( c1 );

  if ( d12 > d23 && d12 > d31 ) {
    d = (unsigned int)( ( d12 + 1 ) / 2 );
    return( Coord<int>( ( c1.x + c2.x + 1 ) / 2, ( c1.y + c2.y + 1 ) / 2 ) );
  } else if ( d23 > d31 && d23 > d12 ) {
    d = (unsigned int)( ( d23 + 1 ) / 2 );
    return( Coord<int>( ( c2.x + c3.x + 1 ) / 2, ( c2.y + c3.y + 1 ) / 2 ) );
  } else {
    d = (unsigned int)( ( d31 + 1 ) / 2 );
    return( Coord<int>( ( c3.x + c1.x + 1 ) / 2, ( c3.y + c1.y + 1 ) / 2 ) );
  }
}

/*
  二点を結ぶ線分の垂直二等分線に対する交点と傾きを求める

  const Coord<int>& c1, c2 : 線分の両端の点
  Coord<double>& m : 線分の中点
  double& a : 垂直二等分線の傾き

  戻り値 : 線分のY座標が一致した場合はfalseを返す
*/
bool GCircle::calcPerpendicularBisector( const Coord<int>& c1, const Coord<int>& c2, Coord<double>& m, double& a )
{
  if ( c1.y == c2.y ) return( false );

  m = Coord<double>( (double)( c1.x + c2.x ) / 2, (double)( c1.y + c2.y ) / 2 );
  a = (double)( c1.x - c2.x ) / (double)( c2.y - c1.y );

  return( true );
}

/*
  三角形の外心と半径を求める

  const Coord<int>& c1, c2, c3 : 三角形の座標
  unsigned int& _r : 半径

  戻り値 : 中心座標 (求められない場合は c1〜c3でできる線分の中点を返す)
*/
Coord<int> GCircle::calcOrigin( const Coord<int>& c1, const Coord<int>& c2, const Coord<int>& c3, unsigned int& _r )
{
  // 二点間の中点と傾きを求める
  Coord<double> m1, m2;
  double a1, a2;

  if ( c1.y == c2.y && c1.y == c3.y )
    return( calcMidPoint( c1, c2, c3, _r ) ); // 水平線上にある場合は求められない

  if ( c1.y == c2.y ) {
    calcPerpendicularBisector( c2, c3, m1, a1 );
    calcPerpendicularBisector( c3, c1, m2, a2 );
  } else if ( c2.y == c3.y ) {
    calcPerpendicularBisector( c1, c2, m1, a1 );
    calcPerpendicularBisector( c3, c1, m2, a2 );
  } else {
    calcPerpendicularBisector( c1, c2, m1, a1 );
    calcPerpendicularBisector( c2, c3, m2, a2 );
  }

  if ( a1 == a2 )
    return( calcMidPoint( c1, c2, c3, _r ) ); // 直線上に並んでいる場合は求められない

  double x = ( a1 * m1.x - a2 * m2.x - m1.y + m2.y );
  double y = ( a1 * a2 * ( m1.x - m2.x ) - a2 * m1.y + a1 * m2.y );
  _r = (unsigned int)( sqrt( pow( ( ( a1 - a2 ) * (double)( c1.x ) - x ) / ( a1 - a2 ), 2 ) +
                             pow( ( ( a1 - a2 ) * (double)( c1.y ) - y ) / ( a1 - a2 ), 2 ) ) + 0.5 );

  x /= a1 - a2;
  y /= a1 - a2;

  Coord<int> ans( roundHalfUp( x ), roundHalfUp( y ) );

  return( ans );
}

指定した三点が同一直線上に並んだ場合、二点を結ぶ線分の垂直二等分線は平行となり、円の中心を求めることはできなくなります。その場合は、三点によって形成される線分の中点を円の中心とし、端点との距離を半径とする円を描画するようにしてあります。その場合の処理関数が calcMidPointです。
三角形の外心と半径を求める関数は calcOriginです。二点 A(Xa, Ya)と B(Xb, Yb)から成る線分 ABの垂直二等分線の傾きは、線分 ABの逆数に -1を掛けたものになるので

- ( Xa - Xb ) / ( Ya - Yb )

から計算することができます。三点から成る線分は三つあるので、その中から二つを選んで、それぞれの中点と、垂直二等分線の傾きを calcPerpendicularBisectorで求めます。このとき、線分が水平線である場合は垂直二等分線が垂直線となって、傾きを求めることができなくなるため、そのような二点は選ばないようにしてあります。二辺の中点と垂直二等分線の傾きがそれぞれ M1(X1, Y1), M2(X2, Y2), a1, a2であったとき、それぞれの垂直二等分線は

y = a1( x - X1 ) + Y1
y = a2( x - X2 ) + Y2

なので、その交点(つまり外心)は

ox = ( a1X1 - a2X2 - Y1 + Y2 ) / ( a1 - a2 )
oy = ( a1a2( X1 - X2 ) - a2Y1 + a1Y2 ) / ( a1 - a2 )

となり、外心と三点のうちの一つとの距離が半径となります。


3) 扇形の描画

円弧全体ではなく、円弧の一部を描いたり、扇形に塗りつぶしを行いたい場合はどうすればいいでしょうか。円周上の二点を始点・終点として描画する場合、対称性を利用して一度に描画することができなくなります。そこで、点を描画する直前に描画対象であるかをチェックする形で対応することにします。

角度からの座標算出

始点と終点は、楕円の中心と結んだ線分の傾き(角度)で表すことにします。X軸の正の方向が 0で時計回りに値が増加するとして、16ビットの符号無し整数値(065535)で角度を表し、65536 = 0(一回転)と見なします。角度θとなる線分の傾きは tanθなので、楕円の中心を (x0,y0)、角度を t(0t65535)としたときの直線の方程式は

y = tan( 2πt / 65536 )( x - x0 ) + y0

と表すことができます。しかし、θが ±π/2のときは、tanθ = ±∞となってしまうため、事前にチェックして場合分けを行う必要があります。
この直線と楕円 a( x - x0 )2 + b( y - y0 )2 = r2 との交点は、tan( 2πt / 65536 ) = mとしたとき

x = ±sqrt( r2 / ( a + bm2 ) ) + x0
y = ±sqrt( (mr)2 / ( a + bm2 ) ) + y0

になります。ここで各成分の符号は tの値によって決まります。

txy
0 〜 16383++
16384 〜 32767-+
32768 〜 49151--
49152 〜 65535+-

始点と終点のそれぞれの座標値が求められれば、それらを通る直線の方程式が得られます。直線を、描画するかどうか判定するための境界線として使えば、任意の範囲のみの円弧を描画することができるようになります。あとは、境界線のどちら側を描画するかを判定する必要がありますが、これは始点から終点に向かうベクトル(vx,vy)の向きから決めることができます。

vxvy直線のどちら側を描画するか
++上側
+-上側
-+下側
--下側

画面上では「上側」「下側」のようになりますが、グラフィック画面の Y座標は下側が正なので、「上側」は、Xの値が等しい場合、Yの値が直線上の点の Y座標よりも「小さい」ことになり、逆に「下側」は「大きい」ことに注意してください。ベクトル成分の符号がどちらも等しい場合、直線の傾きは必ず正になります。ベクトル成分が正ならば、始点は終点よりも左上(X,Y座標成分の小さくなる側)に位置することになり、ベクトル成分が逆に負ならば、始点は終点よりも右下(X,Y座標成分の大きくなる側)に位置することになります。ベクトルの成分が異なっている場合も同様の方法で始点と終点の位置関係が決定します。上の表から明らかなように、X成分だけで、どちら側を描画すればいいかを決定することができます。

/****************************************************************
 GClipByLine : 指定した直線より上側(または下側)だけを点描画する
****************************************************************/
class GClipByLine : public GPSetBase
{
  GPSetBase* pset; // 点描画時の関数オブジェクト

  double a;    // 傾き
  double b;    // 切片
  bool isVert; // 直線は垂直か ?

public:

  bool isUp; // 直線の上側を描画するならtrue

  double gradient() const { return( a ); }  // 傾きを返す
  double intercept() const { return( b ); } // 切片を返す

  // コンストラクタ
  GClipByLine( GPSetBase& _pset, const Coord<int>& c1, const Coord<int>& c2, bool _isUp );

  void operator()( const Coord<int>& c ); // 描画処理
};

/*
  コンストラクタ

  const GPSetBase& _pset : 点描画に使う関数オブジェクト
  const Coord<int>& c1, c2 : 直線上の二点
  bool _isUp : 直線の上側を描画するときはtrue
*/
GClipByLine::GClipByLine( GPSetBase& _pset, const Coord<int>& c1, const Coord<int>& c2, bool _isUp )
  : pset( &_pset ), isUp( _isUp )
{
  // 直線が垂直となる場合の処理
  if ( isVert = ( c1.x == c2.x ) ) {
    b = c1.x;
    return;
  }

  a = (double)( c1.y - c2.y ) / (double)( c1.x - c2.x );
  b = (double)( c1.y ) - a * (double)( c1.x );
}

/*
  描画処理

  const Coord<int>& c : 描画位置
*/
void GClipByLine::operator()( const Coord<int>& c )
{
  if ( isVert ) {
    if ( ( isUp && (double)( c.x ) >= b ) || ( ( ! isUp ) && (double)( c.x ) <= b ) )
      (*pset)( c );
  } else if ( ( isUp && (double)( c.y ) >= (double)( c.x ) * a + b ) ||
      ( ( ! isUp ) && (double)( c.y ) <= (double)( c.x ) * a + b ) ) {
    (*pset)( c );
  }
}

/************************
 GEllipse用メンバ関数
************************/

/*
  X軸からの角度t(0〜65536)にある点の座標を求める

  unsigned int t : 角度

  戻り値 : 求めた座標値
*/
Coord<int> GEllipse::getCoordFromAngle( unsigned int t )
{
  const unsigned int tMax = 65536;
  const unsigned int t2 = tMax >> 2;
  const unsigned int t3 = tMax >> 1;
  const unsigned int t4 = ( tMax >> 2 ) * 3;

  t %= tMax;

  if ( t == t2 ) // Y軸上正方向
    return( Coord<int>( origin.x, (int)( (double)r / sqrt( k.y ) ) + origin.y ) );
  if ( t == t4 ) // Y軸上負方向
    return( Coord<int>( origin.x, (int)( (double)r / sqrt( k.y ) ) * -1 + origin.y ) );

  double powM = pow( tan( 2 * M_PI * (double)t / (double)tMax ), 2 ); // tから求められる傾きの二乗
  Coord<int> c;

  c.x = (int)( (double)r / sqrt( (double)( k.x ) + (double)( k.y ) * powM ) );
  c.y = (int)( (double)r * sqrt( powM ) / sqrt( (double)( k.x ) + (double)( k.y ) * powM ) );

  if ( t >= t2 && t < t4 ) c.x *= -1;
  if ( t >= t3 ) c.y *= -1;

  c.x += origin.x;
  c.y += origin.y;

  return( c );
}

/*
  GClipByLineの作成

  直線によるクリッピングに利用

  GPSetBase& pset : 点描画用関数オブジェクト
  const Coord<int>& sc, ec : 開始・終了点

  戻り値 : GClipByLineオブジェクト
*/
GClipByLine GEllipse::createGClipByLine( GPSetBase& pset, const Coord<int>& sc, const Coord<int>& ec )
{
  bool isUp = ec.x - sc.x <= 0;
  GClipByLine gClipByLine( pset, sc, ec, isUp );

  return( gClipByLine );
}

/*
  GClipByLineの作成

  直線によるクリッピングに利用

  GPSetBase& pset : 点描画用関数オブジェクト
  unsigned int sAngle, eAngle : 開始・終了点のなす角度

  戻り値 : GClipByLineオブジェクト
*/
GClipByLine GEllipse::createGClipByLine( GPSetBase& pset, unsigned int sAngle, unsigned int eAngle )
{
  return( createGClipByLine( pset, getCoordFromAngle( sAngle ), getCoordFromAngle( eAngle ) ) );
}

点描画時に判定を行うことにしたため、判定処理を持った点描画用関数オブジェクトとして GClipByLineを用意しています。これによって、円を描画する処理に判定機能を追加する必要がなくなります。GClipByLineを構築する際に、描画の始点と終点の他、点描画時の関数オブジェクトも渡すようにしてあります。よって、点を描画する処理方法も外部から指定することができます。また、描画する点が直線のどちら側にあるかを判定しているだけなので、円弧の描画に限らず、他の図形の描画でも利用することが可能であり、汎用的な使いかたができます。
GEllipseのメンバ関数 getCoordFromAngleは、与えられた角度から円弧上の点の座標を求めるためのものです。また、createGClipByLineは、始点と終点を元に GClipByLineオブジェクトを作成するためのメンバ関数で、角度指定と座標指定の二種類が用意されています。座標指定の場合は、その座標が円弧上になくても関係なく動作します。

円弧の塗りつぶしを行う場合はこの方法は使えません。直線を境界とした範囲の塗りつぶししかできないため、円弧の形にならないからです。ソリッド・スキャン・コンバージョンを利用して対応するのが最も効率的ですが、ここではもっと単純に、やはり点描画時の判定によって実現するようにしたいと思います。
円弧を描く場合は開始・終了角度を指定するのが一般的なので、描画しようとしている点が指定された角度の範囲内にあるかを判定すれば実現できることになります。以下に、角度による判定機能付きの点描画クラスのサンプル・プログラムを示します。

/****************************************************************
 GClipByAngle : 指定した角度内だけを点描画する
****************************************************************/
class GClipByAngle : public GPSetBase
{
  static const unsigned int tMax = 65536; // 角度の最大値(=2π)

  GPSetBase* pset; // 点描画時の関数オブジェクト

  Coord<int> origin; // 中心点
  unsigned int sArg; // 開始角度
  unsigned int eArg; // 終了角度

  double getAngleFromCoord( const Coord<int>& c ) const; // 座標から角度を求める

public:

  bool isOut; // 範囲外を描画するならtrue

  // コンストラクタ
  GClipByAngle() : pset( 0 ), isOut( false ) {}
  GClipByAngle( GPSetBase& _pset, const Coord<int>& o, int sa, int ea );
  GClipByAngle( GPSetBase& _pset, const Coord<int>& o, const Coord<int>& c1, const Coord<int>& c2 );

  void operator()( const Coord<int>& c ); // 描画処理
};

/*
  コンストラクタ

  const GPSetBase& _pset : 点描画に使う関数オブジェクト
  const Coord<int>& o : 中心点の座標
  int sa, ea : 開始・終了角度
*/
GClipByAngle::GClipByAngle( GPSetBase& _pset, const Coord<int>& o, int sa, int ea )
  : pset( &_pset ), origin( o ), isOut( false )
{
  while ( sa < 0 ) sa += tMax;
  while ( ea < 0 ) ea += tMax;
  sa %= tMax;
  ea %= tMax;

  sArg = (unsigned int)sa;
  eArg = (unsigned int)ea;
}

/*
  コンストラクタ

  const GPSetBase& _pset : 点描画に使う関数オブジェクト
  const Coord<int>& o : 中心点の座標
  const Coord<int>& c1, c2 : 描画範囲の座標
*/
GClipByAngle::GClipByAngle( GPSetBase& _pset, const Coord<int>& o, const Coord<int>& c1, const Coord<int>& c2 )
  : pset( &_pset ), origin( o ), isOut( false )
{
  double a1 = getAngleFromCoord( c1 );
  double a2 = getAngleFromCoord( c2 );
  if ( c1.x > c2.x ) {
    sArg = (unsigned int)a2;
    eArg = (unsigned int)a1;
  } else {
    sArg = (unsigned int)a1;
    eArg = (unsigned int)a2;
  }
}

/*
  指定された座標から、X軸からの角度(0〜65536)を求める

  const Coord<int>& c : 指定座標

  戻り値 : 求めた角度
*/
double GClipByAngle::getAngleFromCoord( const Coord<int>& c ) const
{
  Coord<int> rel( c ); // 中心からの相対座標
  rel -= origin;

  double angle = ( rel.x != 0 ) ?
    atan( (double)( rel.y ) / (double)( rel.x ) ) :
    ( rel.y >= 0 ) ? M_PI / 2 : -M_PI / 2;

  if ( rel.x < 0 ) angle += M_PI;
  if ( angle < 0 ) angle += M_PI * 2;
  angle = angle * tMax / ( M_PI * 2 );

  return( angle );
}

/*
  描画処理

  const Coord<int>& c : 描画位置
*/
void GClipByAngle::operator()( const Coord<int>& c )
{
  double angle = getAngleFromCoord( c );

  if ( sArg > eArg ) {
    if ( ( ( ! isOut ) && ( angle < sArg && angle > eArg ) ) ||
         ( (   isOut ) && ( angle >= sArg || angle <= eArg ) ) )
      (*pset)( c );
  } else {
    if ( ( ( ! isOut ) && ( angle < sArg || angle > eArg ) ) ||
         ( (   isOut ) && ( angle >= sArg && angle <= eArg ) ) )
      (*pset)( c );
  }
}

/************************
 GEllipse用メンバ関数
************************/

/*
  GClipByAngleの作成

  角度によるクリッピングに利用

  GPSetBase& pset : 点描画用関数オブジェクト
  int sAngle, eAngle : 開始・終了角度
  const Coord<int>& c1, c2 : 描画範囲を示す点の座標

  戻り値 : GClipByLineオブジェクト
*/
GClipByAngle GEllipse::createGClipByAngle( GPSetBase& pset, int sAngle, int eAngle )
{
  GClipByAngle gClipByAngle( pset, origin, sAngle, eAngle );

  return( gClipByAngle );
}
GClipByAngle GEllipse::createGClipByAngle( GPSetBase& pset, const Coord<int>& c1, const Coord<int>& c2 )
{
  GClipByAngle gClipByAngle( pset, origin, c1, c2 );

  return( gClipByAngle );
}

指定した点と中心を結んだ線分が X軸となす角度は、線分の傾きを aとすると arctan( a )で求めることができます。ここで、arctanは正接(tan)の逆関数になります。arctanの値域は -π/2から π/2の間になるので、これは x > 0の領域に対する角度を表すことになります。よって、x < 0のときは、求めた角度にπを加える必要があります。また、求めた角度が負の値だった場合、2πを加えて正数に変換します。こうすることによって、0から 2πまでの範囲の角度を求めることができます。あとは、求めた角度が範囲内にあるかどうかをチェックして、範囲内なら点を描画するようにすれば、扇形の塗りつぶしが実現できます。

ちなみに、これは円弧の描画にも利用することができます。また、円に限らず例えば正多角形に対してある範囲だけを描画するような用途にも利用できます。しかし、処理速度が遅いという欠点があります。特に塗りつぶしを行う場合、描画する点が劇的に多くなるため、その度に判定を行うのは非常に効率が悪く、パフォーマンスはあまり期待できなくなります。効率を重視するのであれば、ソリッド・スキャン・コンバージョンを利用した方がいいでしょう。


線分描画と円弧描画をテストするためのプログラムをアップロードしておきます。ご自由におつかいください。Vine Linux 4.2上で動作確認をしています(Gtk+2が必要です)。
メニューバー上から "Selection"で描画する形の選択、"PSet"で点描画方法の選択、"Random Draw"でランダムに描画をする処理ができます。"Selection"で描画する形の選択することで、マウスを使って線分や円などを画面上に描画することができます。

tar-gzip形式 (DrawingSample.tar.gz)

◆◇◆更新履歴◆◇◆


[Go Back]前に戻る [Back to HOME]タイトルに戻る
inserted by FC2 system