HSVカラーサークル

HSVカラーサークル
今回は、以前やった HSV カラーマップを、色相を円状にした形で作ってみたいと思います。
スクリーンショット


サークル部分を左ドラッグで、色相を変更できます。
(彩度・明度部分は選択できません)
ソースコード
034_hsvcircle.c
#include <math.h>
#include "sptk.h"

#define WIDTH  200
#define HEIGHT 200
#define CIRCLE_RMIN 76
#define CIRCLE_RMAX 96
#define SVBOX_SIZE  100
#define PI  3.141592

SPTK_IMAGE *img;
int hsv_h = 0;

/** HSV -> RGB
 * h:0-359 s,v:0.0-1.0 */

uint32_t hsv_to_rgb(int h,double s,double v)
{
    double r,g,b,c1,c2,c3,t;
    int rr,gg,bb;

    if(s == 0)
        r = g = b = v;
    else
    {
        t  = ((h * 6) % 360) / 360.0;
        c1 = v * (1 - s);
        c2 = v * (1 - s * t);
        c3 = v * (1 - s * (1 - t));

        switch(h / 60)
        {
            case 0: r = v;  g = c3; b = c1; break;
            case 1: r = c2; g = v;  b = c1; break;
            case 2: r = c1; g = v;  b = c3; break;
            case 3: r = c1; g = c2; b = v;  break;
            case 4: r = c3; g = c1; b = v;  break;
            case 5: r = v;  g = c1; b = c2; break;
        }
    }
    
    rr = (int)(r * 255 + 0.5);
    gg = (int)(g * 255 + 0.5);
    bb = (int)(b * 255 + 0.5);

    return (rr << 16) | (gg << 8) | bb;
}

/** Hサークル描画 */

void draw_hcircle()
{
    int ix,iy,r,h;
    double xx,yy;
    uint32_t col;
    
    for(iy = 0; iy < HEIGHT; iy++)
    {
        yy = iy - HEIGHT / 2;
    
        for(ix = 0; ix < WIDTH; ix++)
        {
            xx = ix - WIDTH / 2;
        
            r = (int)sqrt(xx * xx + yy * yy);
            
            if(r < CIRCLE_RMIN || r >= CIRCLE_RMAX)
                continue;
            
            h = (int)(-atan2(yy, xx) * 180 / PI);
            h = (h + 360) % 360;
            
            col = hsv_to_rgb(h, 1, 1);
            
            sptk_image_setpixel(img, ix, iy, col);
        }
    }
}

/** SVボックス描画 */

void draw_svbox()
{
    int ix,iy;
    double s,v;
    uint32_t col;
    
    for(iy = 0; iy < SVBOX_SIZE; iy++)
    {
        v = (double)(SVBOX_SIZE - 1 - iy) / (SVBOX_SIZE - 1);
    
        for(ix = 0; ix < SVBOX_SIZE; ix++)
        {
            s = (double)ix / (SVBOX_SIZE - 1);
        
            col = hsv_to_rgb(hsv_h, s, v);
            
            sptk_image_setpixel(img,
                ix + (WIDTH - SVBOX_SIZE) / 2, iy + (HEIGHT - SVBOX_SIZE) / 2,
                col);
        }
    }
    
    sptk_update(NULL,
        (WIDTH - SVBOX_SIZE) / 2, (HEIGHT - SVBOX_SIZE) / 2,
        SVBOX_SIZE, SVBOX_SIZE, -1);
}

/** Hカーソル描画 */

void draw_hcursor(int time)
{
    int x,y,xx,yy;
    double rd;
    
    rd = -hsv_h * PI / 180;
    
    xx = CIRCLE_RMIN + (CIRCLE_RMAX - CIRCLE_RMIN) / 2;
    yy = 0;
    
    x = (int)(xx * cos(rd) - yy * sin(rd) + WIDTH / 2);
    y = (int)(xx * sin(rd) + yy * cos(rd) + HEIGHT / 2);
    
    sptk_image_box(img, x - 4, y - 4, 9, 9, SPTK_COL_XOR);
    sptk_update(NULL, x - 4, y - 4, 9, 9, time);  
}

/** カーソル位置から H 位置取得
 * return : サークルの範囲内なら1、範囲外なら0 */

int get_hpos(int x,int y,int *hdst)
{
    int r,h;
    
    x = x - WIDTH / 2;
    y = y - HEIGHT / 2;

    h = (int)(-atan2(y, x) * 180 / PI);
    h = (h + 360) % 360;
    
    *hdst = h;

    r = (int)sqrt(x * x + y * y);
    
    return (r >= CIRCLE_RMIN && r < CIRCLE_RMAX);
}

void winhandle(SPTK_EVENT *ev)
{
    int h;

    switch(ev->type)
    {
        case SPTK_EVENT_BTTDOWN:
            if(ev->mouse.btt == SPTK_MOUSEBTT_LEFT)
            {
                if(get_hpos(ev->mouse.x, ev->mouse.y, &h))
                {
                    draw_hcursor(-1);
                    hsv_h = h;
                    draw_svbox();
                    draw_hcursor(0);
                
                    sptk_grab(NULL);
                }
            }
            break;
        case SPTK_EVENT_BTTUP:
            if(ev->mouse.btt == SPTK_MOUSEBTT_LEFT && sptk_isgrab())
                sptk_ungrab();
            break;
        case SPTK_EVENT_MOUSEMOVE:
            if(sptk_isgrab())
            {
                draw_hcursor(-1);
                get_hpos(ev->mouse.x, ev->mouse.y, &hsv_h);
                draw_svbox();
                draw_hcursor(0);
            }
            break;
    
        case SPTK_EVENT_WINDOW_CLOSE:
            sptk_quit();
            break;
    }
}

int main()
{
    sptk_init("test", WIDTH, HEIGHT);
    
    sptk_window_set_handle(winhandle);
    
    img = sptk_window_get_image();
    
    draw_hcircle();
    draw_svbox();
    draw_hcursor(-1);
    
    sptk_run();

    return 0;
}
解説
今回の HSV カラーサークルによる色選択は、「位置から円の半径の長さを求める計算」「位置から角度を求める計算」「回転計算」の3つがわかっていれば、作ることが出来ます。
色相サークルの描画
まず、色相のサークルについて考えてみます。
ここでは、半径が CIRCLE_RMAX の円を描き、その円において、半径が「CIRCLR_RMINCIRCLE_RMAX」の範囲内の部分に、色相の色を置くことにします。

実際に色相サークルを描画する際は、描画先の X, Y でループを実行するので、色相のサークルを描画するためには、「任意の描画先位置における円の半径と角度」が必要です。
円の半径は、描画の範囲内かどうかを判定するのに使い、角度は、色相の値として使います。

以下が、実際のコードです。

void draw_hcircle()
{
    int ix,iy,r,h;
    double xx,yy;
    uint32_t col;
    
    for(iy = 0; iy < HEIGHT; iy++)
    {
        yy = iy - HEIGHT / 2;
    
        for(ix = 0; ix < WIDTH; ix++)
        {
            xx = ix - WIDTH / 2;
        
            r = (int)sqrt(xx * xx + yy * yy);
            
            if(r < CIRCLE_RMIN || r >= CIRCLE_RMAX)
                continue;
            
            h = (int)(-atan2(yy, xx) * 180 / PI);
            h = (h + 360) % 360;
            
            col = hsv_to_rgb(h, 1, 1);
            
            sptk_image_setpixel(img, ix, iy, col);
        }
    }
}

円の中心は、画面の中心とするので、描画座標から画面の半分を引いたものを、計算用の座標として使います。

まずは、その位置が描画する色相サークルの範囲内かどうかを判定する必要があります。
原点を (0, 0) とした場合の、位置 (x, y) までの線の長さ (半径の長さ) は、「(x * x + y * y) の平方根」を求めることで得られます。

r = (int)sqrt(xx * xx + yy * yy);

ここでは、「CIRCLE_RMINCIRCLE_RMAX」の範囲内であれば、描画範囲内とします。

描画範囲内の場合は、次に、その位置の円の角度を求めて、それを色相値とします。
原点を (0, 0) とした場合の、位置 (x, y) の角度は、アークタンジェント関数を使うことで求められます。
ここでは、atan2() 関数を使います。

h = (int)(-atan2(yy, xx) * 180 / PI);
h = (h + 360) % 360;

角度は atan2() で求められますが、返り値はラジアン単位になっているので、円の1周を 360 とする単位に変換します。
また、ここでは角度を符号反転していますが、これは、描画結果が反時計回りになるようにするためです。
あとは、色相の数値を 0〜359 の範囲にするため、+360 で負の値を正の値に直し、% 360 で範囲内に収めています。

これで色相の値が求められたので、HSV から RGB 値に変換し、点を描画します。
色相の現在位置をカーソルとして描画
次は、色相サークル上において、現在選択されている色相の位置に、カーソルを描画する必要があります。
カーソル描画は、以下の関数で行っています。

void draw_hcursor(int time)
{
    int x,y,xx,yy;
    double rd;
    
    rd = -hsv_h * PI / 180;
    
    xx = CIRCLE_RMIN + (CIRCLE_RMAX - CIRCLE_RMIN) / 2;
    yy = 0;
    
    x = (int)(xx * cos(rd) - yy * sin(rd) + WIDTH / 2);
    y = (int)(xx * sin(rd) + yy * cos(rd) + HEIGHT / 2);
    
    sptk_image_box(img, x - 4, y - 4, 9, 9, SPTK_COL_XOR);
    sptk_update(NULL, x - 4, y - 4, 9, 9, time);  
}

カーソルを描画するためには、現在の色相値から、カーソルの座標を得る必要があります。
「色相値=円の角度」ですから、カーソル描画位置の半径の長さと角度があれば、回転計算を行うことにより、座標を計算することができます。

3時の方向を0度とした場合、回転前の位置は、(半径の長さ, 0) です。
半径の長さは、ここでは、CIRCLE_RMINCIRCLE_RMAX の中間位置とします。
この座標を対象にして、色相の角度分を回転させることで、描画位置を得ます。

なお、ここでは、回転計算がわかりやすいようにコードを書いていますが、回転の Y 位置は 0 ですから、実際には Y の計算部分は省略できます。

x = xx * cos(rd) + WIDTH / 2;
y = xx * sin(rd) + HEIGHT / 2;
カーソル位置から色相位置を得る
あとは、色相サークルの操作として、クリックまたはドラッグされた位置から色相位置を得る必要があります。
get_hpos() で、カーソル位置から色相値を得ることができます。

int get_hpos(int x,int y,int *hdst)
{
    int r,h;
    
    x = x - WIDTH / 2;
    y = y - HEIGHT / 2;

    h = (int)(-atan2(y, x) * 180 / PI);
    h = (h + 360) % 360;
    
    *hdst = h;

    r = (int)sqrt(x * x + y * y);
    
    return (r >= CIRCLE_RMIN && r < CIRCLE_RMAX);
}

なお、ボタンが押された時には、そこが色相サークルの範囲内かどうかも判定する必要があるので、位置から円の半径を得て、判定を行い、範囲内なら 1 を返すようにしています。
ドラッグ中は、カーソル位置が範囲内/範囲外であるかに関わらず、常に角度を取得します。
(円の範囲外でも角度は求められるので)

この辺りの計算は、色相サークルの描画時とほぼ同じです。
まとめ
今回は、彩度・明度部分は選択できるようにしませんでしたが、形が四角形なので、簡単に処理できると思います。

ところで、ペイントソフトの中には、彩度・明度部分が三角形の形のものがありますが、あれは処理が大変そうなので、ここではやりません。
それに、個人的には、三角形よりも四角形の方が色が選択しやすいと思います。