実用的なキャンバス(2)・基準位置

イメージの基準位置
前回までのキャンバス描画では、イメージの左上 (0,0) の位置を基準として描画してきました。
イメージの左上の位置を基準とすると、拡大縮小時もその位置が基準となります。

試しに、前回のプログラムを実行して、イメージの左上位置が画面内にある状態で拡大縮小してみてください。
イメージの (0,0) の位置は、キャンバス内ではずっと同じ位置にあると思います。

これが、現在の基準位置はイメージの左上位置にある、ということです。

ドット編集用のプログラムとして作るなら、基準位置は左上のままでも特に不便があるというわけではありませんが、中規模以上のペイントソフトとしては、イメージの基準位置は左上の位置で固定するのではなく、状況によって自由に変えられるようにした方が、キャンバスの扱いが便利になります。

今回は、キャンバス内でのイメージの基準位置を変更できるようにします。
ソースファイル
>> 019_canvas2.c

今回は、スクロールバーがあると処理が面倒になるので、スクロールバーを排除し、右ボタンドラッグでキャンバスをスクロールできるようにしました。
また、拡大縮小時は、キャンバスの中心位置が基準になるようにしています。
解説
基準位置とスクロール位置
今回は、SPTK_POINT ptImgBase にイメージの基準位置、SPTK_POINT ptScr に現在のスクロール位置を格納することにしています。

なお、キャンバス上においては、以下のような定義が成り立つものとします。

  • スクロール位置が (0,0) の時、キャンバスの中心には、イメージの基準位置のピクセルが来る。
  • キャンバス上のイメージは、イメージの基準位置を中心にして拡大・縮小を行う。

今回は、初期状態では、イメージの基準位置をイメージの中心にして、スクロール位置を (0,0) にしています。

ptImgBase.x = IMG_W / 2;
ptImgBase.y = IMG_H / 2;
ptScr.x = ptScr.y = 0;

ということは、キャンバス上では、イメージの中心位置がキャンバスの中心に来るように表示されます。
マウス操作時
今回は、左ドラッグで自由線描画、右ドラッグでキャンバス移動 (スクロール) を行うので、現在どの操作が実行されているかを示す変数 fdrag を用意しています。

この変数の値が DRAGF_FREELINE (1) の場合は自由線描画中、DRAGF_CANVASMOVE (2) の場合はキャンバス移動中であることを意味します。

「左ボタンでのドラッグ中に右ボタンが押される」という場合もあるので、ボタン押し時は、何も操作していない状態であるか (fdrag が DRAGF_NONE であるか) を確認しておく必要があります。
ボタンが離された時においても同様です。
座標変換 (キャンバス描画)
キャンバスにイメージの基準位置を適用するため、座標変換の部分に手を加えます。

まずは、キャンバス描画時。

sfx_left = (rc->x - CANVAS_W / 2 + ptScr.x) * incfxy + (ptImgBase.x << 18);
sfy = (rc->y - CANVAS_H / 2 + ptScr.y) * incfxy + (ptImgBase.y << 18);

今回は、キャンバスの中心位置が原点になるように計算します。
そうすれば、スクロール位置が (0,0) の時は、「キャンバスの中心位置 = イメージの基準位置」となるので、扱いやすくなります。

まずは、キャンバスの中心が座標 (0,0) になるように、キャンバス座標から CANVAS_W / 2, CANVAS_H / 2 を引きます。
これで、キャンバスの左・上側が負の値の座標、右・下側が正の値の座標になります。

その後、スクロール位置を適用します。

そして今回は、最後にイメージの基準位置を加えています。
拡大縮小が適用された後の座標は左・上側が負の値の座標になっているので、左上が (0,0) であるイメージ画像の座標にするため、基準位置を足します。
なお、<< 18 としているのは、「整数→固定小数点数」への変換です。
値は固定小数点数として扱っていますから、整数部分である基準位置の座標を右にシフトして、固定小数点数にしています。
座標変換
次は、「イメージ座標→キャンバス座標」への変換、「キャンバス座標→イメージ座標」への変換部分です。

void img_to_canvas(int *x,int *y)
{
    *x = (int)((*x - ptImgBase.x) * zoom / 100.0 + CANVAS_W / 2 - ptScr.x);
    *y = (int)((*y - ptImgBase.y) * zoom / 100.0 + CANVAS_H / 2 - ptScr.y);
}

void canvas_to_img(int *x,int *y)
{
    *x = (int)((*x - CANVAS_W / 2 + ptScr.x) * 100.0 / zoom + ptImgBase.x);
    *y = (int)((*y - CANVAS_H / 2 + ptScr.y) * 100.0 / zoom + ptImgBase.y);
}

なお、前回は整数で計算しましたが、今回は浮動小数点で計算する必要があります。

実際に整数計算にして試してみると分かりますが、もしこの部分が整数計算のままだと、拡大表示しながら画面の左・上側を描画した時に、描画される位置が 1px ずれます。

これはなぜかというと、今回はキャンバスの中心を基準にして計算するようにしているので、
canvas_to_img() の (*x - CANVAS_W / 2 + ptScr.x) の結果が負の値になる場合があります。

例えば、イメージの基準位置が (100,100) であるとして、もし (*x - CANVAS_W / 2 + ptScr.x) の結果が負の値で、かつ小数を持つ場合、
整数計算で行うと、「(int)-0.5 + 100 = 100」
浮動小数点計算で行うと、「-0.5 + 100 = 99.5」
という結果となります。

この結果を整数に変換すると、100 と 99 で、整数計算で行った場合は座標値が +1 されてしまいます。
整数計算の場合は、計算途中で小数点以下が切り捨てられてしまうので、負の値の場合は正しい結果を得られません。

というわけで、座標変換は浮動小数点計算で行ってください。
右ドラッグによるキャンバス移動
今回は、スクロールバーを排除したので、代わりに右ドラッグでキャンバス内をスクロールできるようにしました。

以下は、キャンバス移動の操作中にカーソルが移動した時の処理です。

void canvasmove_move(int x,int y)
{
    int sx,sy,xx,yy;
    
    sx = ptScr.x + ptLast.x - x;
    sy = ptScr.y + ptLast.y - y;
    
    /* 左上位置調整 */

    xx = -ptImgBase.x * zoom / 100 + CANVAS_W / 2 - SPACE_WH;
    yy = -ptImgBase.y * zoom / 100 + CANVAS_H / 2 - SPACE_WH;

    if(sx < xx) sx = xx;
    if(sy < yy) sy = yy;

    /* 右下位置調整 */

    xx = (IMG_W - ptImgBase.x) * zoom / 100 - CANVAS_W / 2 + SPACE_WH;
    yy = (IMG_H - ptImgBase.y) * zoom / 100 - CANVAS_H / 2 + SPACE_WH;
    
    if(sx > xx) sx = xx;
    if(sy > yy) sy = yy;
    
    /* セット */
    
    ptScr.x = sx;
    ptScr.y = sy;
    
    update_canvas(NULL, 5);

    ptLast.x = x;
    ptLast.y = y;
}

現在のスクロール位置は ptScr に格納されています。
まずは、現在のスクロール位置に、カーソル移動分を加えます。

これだけでは無限にスクロール移動できるようになってしまうので、スクロール位置の最小値と最大値を決めて、値を調整します。

まずは、スクロールの最小値 (左上位置の値) を決めます。
今回は、イメージの左上位置から一定の余白幅が付くようにしています。

まず、現在のキャンバスにおいて、イメージの左上位置 (0,0) が画面の左上に来る場合のスクロール値を求めたいので、「イメージ座標→キャンバス座標」の変換式を元に計算します。

/* イメージ座標→キャンバス座標 */
*x = (int)((*x - ptImgBase.x) * zoom / 100.0 + CANVAS_W / 2 - ptScr.x);
*y = (int)((*y - ptImgBase.y) * zoom / 100.0 + CANVAS_H / 2 - ptScr.y);

イメージ座標を (0,0) にして求めればいいので、スクロール値は以下のようになります。

xx = -ptImgBase.x * zoom / 100 + CANVAS_W / 2;
yy = -ptImgBase.y * zoom / 100 + CANVAS_H / 2;

なお、浮動小数点演算で行わずに整数演算で行っていますが、スクロールの制限値は大まかなものであり、別にきっちり求める必要もないので、今回はこのままにしておきます。

後は、この値に余白幅を加えて、- SPACE_WH で最小位置を左上に拡張します。
これでスクロールの最小値は求められました。

スクロールの最大値 (右下位置) についても、イメージ座標を右下位置にして同様の計算を行います。
表示倍率の変更時
今回はさらに、表示倍率が変更された時、現在画面の中心に表示されている部分が、拡大縮小後も画面の中心に来るように処理します。

前回までは、イメージの左上が基準位置となっていたので、拡大縮小後は画面上のイメージがバラバラの位置に飛んでしまい、今現在、イメージのどの部分にいるのかわからなくなっていました。

拡大縮小時は、画面の中心を常に据えて置くことで、キャンバスがさらに扱いやすくなります。

実際のコードは、以下のようになっています。
change_zoom() は、現在の表示倍率に inc を加えて、倍率を変更します。

void change_zoom(int inc)
{
    char m[16];
    int x,y,newzoom;
    
    newzoom = zoom + inc;
    
    if(newzoom > 1000) newzoom = 1000;
    else if(newzoom < 25) newzoom = 25;
    
    if(zoom != newzoom)
    {
        /* 現在のキャンバス中心のイメージ位置 */
        
        x = CANVAS_W / 2;
        y = CANVAS_H / 2;
        canvas_to_img(&x, &y);
        
        /* 倍率変更 */

        zoom = newzoom;
    
        sprintf(m, "%d%%", zoom);
        sptk_window_set_title(m);
        
        /* 基準位置を変更 */
        
        ptImgBase.x = x;
        ptImgBase.y = y;
        ptScr.x = ptScr.y = 0;

        update_canvas(NULL, 0);
    }
}

倍率が変更される場合、新しい倍率に変更する前に、まずは、現在のキャンバスの中心位置におけるイメージ座標を求めておきます。
その後、倍率を変更します。
そして、イメージの基準位置をさきほど求めた座標値に変更し、スクロール位置を (0,0) にリセットします。

これが、画面の中心を据え置くための処理です。

キャンバス上では、スクロール位置が (0,0) の時、画面の中心にはイメージの基準位置のピクセルが来るのですから、倍率変更前のイメージ位置を求めておいて、それを基準位置にすれば、画面中央にそのピクセルを持ってくることができます。

ちなみに、拡大表示時にちゃんと1ドットの中央が画面の中心に来るようにするには、イメージの基準位置を浮動小数点で扱う必要があります。
(基準位置が整数だと、「画面の中央=拡大されたドットの左上」となります)