直線ツール

直線ツール
前回までで自由線の描画が出来たので、次はペイントソフトの直線ツールのように、左ドラッグで始点と終点を指定して、その間を直線で描画してみます。

ボタンが押された位置が始点、ボタンが離された位置が終点となります。
ボタンを押している間は 始点〜現在位置 までが仮の直線で引かれ、ボタンを離した時点でどんな直線が描画されるかが確認できるようになっています。
スクリーンショット
ソースコード
006_line.c
#include "sptk.h"

#define WIDTH  300
#define HEIGHT 300
#define PIXCOL 0x0000ff

SPTK_POINT start,last;

void drawline(uint32_t col,int time)
{
    SPTK_RECT rc;
    
    rc.x1 = start.x;
    rc.y1 = start.y;
    rc.x2 = last.x;
    rc.y2 = last.y;

    sptk_image_line(sptk_window_get_image(), rc.x1, rc.y1, rc.x2, rc.y2, col);
    sptk_update_rect(NULL, &rc, time);
}

void winhandle(SPTK_EVENT *ev)
{
    switch(ev->type)
    {
        case SPTK_EVENT_BTTDOWN:
            if(ev->mouse.btt == SPTK_MOUSEBTT_LEFT)
            {
                start.x = ev->mouse.x;
                start.y = ev->mouse.y;
                
                last = start;
                
                drawline(SPTK_COL_XOR, 0);
                            
                sptk_grab(NULL);
            }
            break;
        case SPTK_EVENT_BTTUP:
            if(ev->mouse.btt == SPTK_MOUSEBTT_LEFT && sptk_isgrab())
            {
                sptk_ungrab();
                
                drawline(PIXCOL, -1);
            }
            break;
        case SPTK_EVENT_MOUSEMOVE:
            if(sptk_isgrab())
            {
                drawline(SPTK_COL_XOR, -1);
                
                last.x = ev->mouse.x;
                last.y = ev->mouse.y;
                
                drawline(SPTK_COL_XOR, 0);
            }
            break;
    
        case SPTK_EVENT_WINDOW_CLOSE:
            sptk_quit();
            break;
    }
}

int main()
{
    sptk_init("test", WIDTH, HEIGHT);
    
    sptk_window_set_handle(winhandle);
    
    sptk_run();

    return 0;
}
解説
始点と終点
直線の始点位置を SPTK_POINT start、終点位置を SPTK_POINT last に格納します。

SPTK_POINT は以下のように定義されています。

typedef struct _SPTK_POINT
{
    int x,y;
}SPTK_POINT;
drawline() 関数
drawline() は、start から last の位置までを指定した色で直線描画し、更新する関数です。

今回は、イメージへの直線描画には sptk_image_line() を使います。
sptk_image_line() は、ブレゼンハムで直線を描画します。

なお、色に SPTK_COL_XOR (0xffffffff) を指定した場合、イメージ上の色と XOR 演算します。
XOR 演算について
XOR 演算」は、「排他的論理和」と言われる、val1 ^ val2 のビット演算です。

XOR 演算に関しては以下のような特徴があります。
「ある値に XOR した値を、同じ値で XOR すると、元の値に戻る」

val = 0x12345678;
val ^= 0xffffffff;  //0xedcba987
val ^= 0xffffffff;  //0x12345678

XOR 演算では、各ビットの演算結果は以下のようになります。

0 XOR 00
0 XOR 11
1 XOR 01
1 XOR 10

特徴は、「ビットが 1 同士なら 0 になる」ということです。
画像処理における XOR
画像処理においては、今回の直線ツールのように、一時的な描画を行う際に XOR がよく使われます。

それはなぜかというと、イメージ上に直接 XOR 描画しても、再度同じ XOR 描画を行えば元のイメージの状態に戻すことができるため、作業用のイメージを用意する必要ないからです。

例えば、キャンバス上にすでに描画された内容を維持したまま、一時的な描画を行いたいとします。
もしも XOR が使えない場合は、以下のような手順が必要となります。

(1) 元のイメージを残しておくために作業用のイメージを作成し、そこに元のイメージをコピーする。
(2) 描画先イメージに一時的な内容の描画を行う。
(3) 一時的な内容を戻したい場合、コピーした元イメージからその範囲を転送して、元に戻す。
(4) 一時的な描画が終わったら、元イメージを使って元に戻す。

このように、一時的な描画を行う前の状態のイメージを保存しておかなければならないので、メモリが余計に必要になります。

ペイントソフトにおいては、このようなメモリ確保は避けたいところなので、直接イメージ上に描画しても元の状態に戻せる XOR というのは非常に便利なのです。

ただ、XOR に一つ欠点があるとすれば、灰色、特に 0x808080 付近の色だと、XOR した後の値が元の値とほぼ変わらないので、色の変化が薄くなるということです。
0x808080 を XOR すると 0x7f7f7f となり、RGB 値が1しか違わない色となります。
ボタン押し時
start と last に、現在位置をセットします。

そして、現在位置に XOR で点を描画します。
drawline() で直線を描画していますが、(x1,y1) == (x2,y2) の場合は点だけ描画されます。

なぜここで点を描画しておかなければならないかというと、カーソル移動時に常に「XOR 消去」と「新しい XOR 描画」を行っているからです。
カーソル移動時は最初に、前回の直線を XOR で描画して元に戻していますが、もしボタン押し時に何も描画していなかった場合は、直線の始点は XOR 描画されていない状態であるため、「XOR 消去」を行う時に、逆に「XOR 描画」を行うことになってしまいます。
カーソル移動時
まず、現在描画されている XOR の線を消すために、前回の直線を XOR で描画します。
これでイメージは元に戻ります。

ちなみに、drawline() の引数 time に -1 を指定しているのは、更新範囲だけ追加するという意味です。
直後にまた drawline() を呼ぶことになるので、更新タイミングは次の drawline() で指定します。

そして、last に現在の位置を入れて、新しい XOR の線を引くために、また XOR で直線を描画します。
ボタン離し時
ボタンを離した時点で直線の始点と終点が確定するので、drawline() で、実際に描画したい色で直線を描画します。

今回の場合は、残っている XOR 線の上にそのまま上書き描画しても問題ないので、XOR 消去の処理を省いていますが、実際にペイントソフトなどを作る場合は、先に XOR 線を消去した後、実際の直線描画を行ってください。