実用的なキャンバス
前回までは、基本的なキャンバス描画の方法を解説しました。
今回は、ペイントソフトのように実用的なキャンバスを作ります。
キャンバス内には、イメージの周りに一定の余白を設けます。
そして、キャンバス内を左ドラッグで自由線を描画できるようにします。
今回は、ペイントソフトのように実用的なキャンバスを作ります。
キャンバス内には、イメージの周りに一定の余白を設けます。
そして、キャンバス内を左ドラッグで自由線を描画できるようにします。
スクリーンショット&ソースファイル
タイトルバーに現在の倍率が表示されます。
U キーで拡大、D で縮小できます。
ソースファイル: 018_canvas.c
(ソースコードのアーカイブから取得してください)
解説
layerimg が、描画対象となるレイヤイメージ。
blendimg が、背景とレイヤを合成した後のイメージ。
これは、キャンバスに描画するためのソース画像でもあります。
blendimg が、背景とレイヤを合成した後のイメージ。
これは、キャンバスに描画するためのソース画像でもあります。
更新
まず、更新用の関数はいくつかに分けました。
update_image() は、指定範囲のイメージを更新します。
背景 (ここではチェック柄) と、layerimg のレイヤイメージを合成した結果を、blendimg にセットします。
update_canvas() は、キャンバスを更新します。
blendimg のイメージをソース画像として、それをウィンドウのイメージに描画し、ウィンドウを更新させます。
update_all() は、イメージとキャンバスをまとめて更新します。
自由線描画時の更新用として使われます。
なぜこのように分ける必要があるかというと、例えば、スクロールバーによってキャンバス内をスクロールをする場合を考えてみてください。
スクロール時は、キャンバスの全体を更新する必要があるため、キャンバス描画を行う必要はありますが、レイヤイメージや合成結果のイメージは更新する必要がありませんから、blendimg の内容はそのままでいいわけです。
ということは、スクロール中はウィンドウイメージの更新をするだけで十分なので、レイヤの合成処理を行う必要はありません。
このように、更新時には、何を更新する必要があって、何を更新する必要がないのかを判断して、使い分けるべきです。
update_image() は、指定範囲のイメージを更新します。
背景 (ここではチェック柄) と、layerimg のレイヤイメージを合成した結果を、blendimg にセットします。
update_canvas() は、キャンバスを更新します。
blendimg のイメージをソース画像として、それをウィンドウのイメージに描画し、ウィンドウを更新させます。
update_all() は、イメージとキャンバスをまとめて更新します。
自由線描画時の更新用として使われます。
なぜこのように分ける必要があるかというと、例えば、スクロールバーによってキャンバス内をスクロールをする場合を考えてみてください。
スクロール時は、キャンバスの全体を更新する必要があるため、キャンバス描画を行う必要はありますが、レイヤイメージや合成結果のイメージは更新する必要がありませんから、blendimg の内容はそのままでいいわけです。
ということは、スクロール中はウィンドウイメージの更新をするだけで十分なので、レイヤの合成処理を行う必要はありません。
このように、更新時には、何を更新する必要があって、何を更新する必要がないのかを判断して、使い分けるべきです。
キャンバス描画
まずは、キャンバス描画のコードから見てみます。
今回は、とりあえずニアレストネイバーにしておきました。
元となるソース画像と、描画する範囲 (キャンバスの座標) を引数で指定します。
SPTK_RECTWH は、以下のようになっています。
今回は描画範囲の指定があるため、描画の左上位置は (rc->x, rc->y) なので、描画先であるウィンドウイメージのポインタもその位置を先頭にしています。
また、ループ開始時の sfx, sfy の値にも少し変更があります。
今までは、キャンバスの (0,0) の位置からの描画となっていましたが、今回は描画の左上位置が (rc->x, rc->y) なので、それに合わせています。
また、ここではキャンバスの余白部分も計算に入れています。
SPACE_WH は、キャンバス内の余白幅です。
上記の計算は、キャンバス座標をソース画像座標に変換する計算なので、- SPACE_WH でキャンバス位置から余白を引いて、左上をソース画像の (0,0) に合わせるように調整しています。
今回は、とりあえずニアレストネイバーにしておきました。
void draw_canvas(SPTK_IMAGE32 *srcimg,SPTK_RECTWH *rc) { int ix,iy,pitchd,sfx,sfy,sfx_left,incfxy,sx,sy; uint8_t *pdst; SPTK_PIX_RGBA *psrc; pdst = sptk_image_getptbuf(winimg, rc->x, rc->y); pitchd = winimg->pitch_dir - rc->w * winimg->bpp; incfxy = (1 << 18) * 100 / zoom; sfx_left = (rc->x - SPACE_WH + scrx) * incfxy; sfy = (rc->y - SPACE_WH + scry) * incfxy; for(iy = 0; iy < rc->h; iy++) { sfx = sfx_left; sy = sfy >> 18; for(ix = 0; ix < rc->w; ix++) { sx = sfx >> 18; if(sx < 0 || sx >= srcimg->w || sy < 0 || sy >= srcimg->h) sptk_image_setpixel_buf_rgb(winimg, pdst, 0xcc, 0xcc, 0xcc); else { psrc = srcimg->pixbuf + sy * srcimg->w + sx; sptk_image_setpixel_buf_rgb(winimg, pdst, psrc->r, psrc->g, psrc->b); } sfx += incfxy; pdst += winimg->bpp; } sfy += incfxy; pdst += pitchd; } }
元となるソース画像と、描画する範囲 (キャンバスの座標) を引数で指定します。
SPTK_RECTWH は、以下のようになっています。
typedef struct _SPTK_RECTWH { int x,y,w,h; }SPTK_RECTWH;
今回は描画範囲の指定があるため、描画の左上位置は (rc->x, rc->y) なので、描画先であるウィンドウイメージのポインタもその位置を先頭にしています。
また、ループ開始時の sfx, sfy の値にも少し変更があります。
sfx_left = (rc->x - SPACE_WH + scrx) * incfxy; sfy = (rc->y - SPACE_WH + scry) * incfxy;
今までは、キャンバスの (0,0) の位置からの描画となっていましたが、今回は描画の左上位置が (rc->x, rc->y) なので、それに合わせています。
また、ここではキャンバスの余白部分も計算に入れています。
SPACE_WH は、キャンバス内の余白幅です。
上記の計算は、キャンバス座標をソース画像座標に変換する計算なので、- SPACE_WH でキャンバス位置から余白を引いて、左上をソース画像の (0,0) に合わせるように調整しています。
自由線描画の処理
では、次に自由線描画の処理を見てみます。
ボタン押し時と離し時は特に変わりはありません。
以下は、カーソル移動時のコードです。
カーソル位置の座標は、描画エリアウィジェットに対する相対位置なので、つまりキャンバス座標ということです。
まずは、描画対象となるレイヤイメージへの直線描画位置を計算するため、カーソル座標をレイヤイメージの座標へ変換します。
ここでは、canvas_to_img() で X,Y の値を変換しています。
「キャンバス座標→イメージ座標」への変換ということは、キャンバス描画でやった座標変換の計算と同じなので、そのまま同じ計算式にします。
描画位置が求められたら、sptk_image32_line() で、レイヤイメージ上に直線を描画します。
その後は、描画された範囲のウィンドウ内容を更新して、画面上に表示させます。
まずは、更新されたイメージの範囲を求めます。
直線の太さは 1px ですから、直線の矩形をそのまま使えばいいので、
sptk_rect_swap() で、左上・右下が正しい値になるように X, Y を入れ替えた後、
sptk_rect_clip_wh() で、矩形範囲を (0,0)-(w-1,h-1) の範囲にクリッピングします。
直線の座標はイメージの範囲外に出ている場合があるので、クリッピングが必要です。
sptk_rect_clip_wh() の戻り値が 1 の場合は、矩形全体が範囲外上にあるので、更新処理は行いません。
戻り値が 0 の時のみ、更新処理を行います。
最後に、イメージの更新範囲を SPTK_RECTWH 構造体 (x, y, w, h) に変換して、update_all() でイメージとキャンバスを両方更新します。
ボタン押し時と離し時は特に変わりはありません。
以下は、カーソル移動時のコードです。
rc.x1 = ptLast.x; rc.y1 = ptLast.y; rc.x2 = ev->mouse.x; rc.y2 = ev->mouse.y; /* キャンバス -> イメージ座標 */ canvas_to_img(&rc.x1, &rc.y1); canvas_to_img(&rc.x2, &rc.y2); /* レイヤに描画 */ sptk_image32_line(layerimg, rc.x1, rc.y1, rc.x2, rc.y2, &drawcol); /* 更新 */ sptk_rect_swap(&rc); if(!sptk_rect_clip_wh(&rc, IMG_W, IMG_H)) { sptk_rect_to_rectwh(&rcwh, &rc); update_all(&rcwh, 2); } ptLast.x = ev->mouse.x; ptLast.y = ev->mouse.y;
カーソル位置の座標は、描画エリアウィジェットに対する相対位置なので、つまりキャンバス座標ということです。
まずは、描画対象となるレイヤイメージへの直線描画位置を計算するため、カーソル座標をレイヤイメージの座標へ変換します。
ここでは、canvas_to_img() で X,Y の値を変換しています。
void canvas_to_img(int *x,int *y) { *x = (*x - SPACE_WH + scrx) * 100 / zoom; *y = (*y - SPACE_WH + scry) * 100 / zoom; }
「キャンバス座標→イメージ座標」への変換ということは、キャンバス描画でやった座標変換の計算と同じなので、そのまま同じ計算式にします。
描画位置が求められたら、sptk_image32_line() で、レイヤイメージ上に直線を描画します。
その後は、描画された範囲のウィンドウ内容を更新して、画面上に表示させます。
まずは、更新されたイメージの範囲を求めます。
直線の太さは 1px ですから、直線の矩形をそのまま使えばいいので、
sptk_rect_swap() で、左上・右下が正しい値になるように X, Y を入れ替えた後、
sptk_rect_clip_wh() で、矩形範囲を (0,0)-(w-1,h-1) の範囲にクリッピングします。
直線の座標はイメージの範囲外に出ている場合があるので、クリッピングが必要です。
sptk_rect_clip_wh() の戻り値が 1 の場合は、矩形全体が範囲外上にあるので、更新処理は行いません。
戻り値が 0 の時のみ、更新処理を行います。
最後に、イメージの更新範囲を SPTK_RECTWH 構造体 (x, y, w, h) に変換して、update_all() でイメージとキャンバスを両方更新します。
描画時の更新
update_all() は、クリッピング済みのイメージの範囲を元に、レイヤ合成とキャンバス描画を行い、画面を更新します。
つまり、レイヤイメージ上に描画した後に実行しなければならない更新処理です。
レイヤ上に何か描画が行われた場合、まずはその範囲を再度レイヤ合成して、合成結果イメージを更新します。
そして、そのイメージの範囲に対応するキャンバスの範囲を、キャンバス描画で更新して、ウィンドウ上の範囲も更新させます。
なお、レイヤへの描画後に更新する必要があるのは、実際にイメージに描画された部分の範囲だけです。
例えば、1px の点を打って、それを更新するために、わざわざイメージ全体を更新していたのでは、処理の無駄です。
合成結果のイメージとウィンドウ用のイメージは、常に現在の状況を保持しているものとして、新しく描画された部分だけを更新してやれば、無駄を省くことができます。
以下は、実際のコードです。
update_image() は、指定されたイメージの範囲でレイヤ合成を行い、合成結果のイメージに描画します。
次に、キャンバスを更新するのですが、更新範囲として指定されている imgrc はイメージの座標なので、まずはイメージの範囲をキャンバスの範囲に変換する必要があります。
img_to_canvas_rect() が、範囲を変換する関数です。
img のイメージ範囲をキャンバス範囲に変換して、クリッピングし、canv に格納します。
[x, y, w, h] のイメージ座標をキャンバス座標に変換するためには、まず、範囲の左上・右下の2点をキャンバス座標に変換します。
その座標をクリッピング処理した後、格納先変数にセットします。
まず、左上・右下の2点をセットする際に、(x1, y1) にはそのまま (x, y) を代入しますが、(x2, y2) には実際には +1 させた値を代入しています。
これは、拡大表示時のための措置です。
例えば、100% 以上の倍率時に、(x + w - 1, y + h - 1) の位置をキャンバス座標に変換した場合、そのキャンバス位置は、拡大されたドットの左上の位置となります。
もしも右下をこのイメージ位置で変換した場合、描画された範囲の右端・下端のピクセルが更新範囲に入らないことになります。
今は、ドットの右下、つまりイメージ座標の次のピクセルまでの範囲を更新しなければならないので、イメージの右下の点は +1 する必要がある、というわけです。
そして、その2点を、img_to_canvas() で、イメージ座標からキャンバス座標へ変換しています。
計算式としては、「キャンバス座標→イメージ座標」の変換と逆の計算を行っています。
その後、キャンバス座標を念の為 1px 拡張しています。
これは、キャンバス描画時の計算の誤差などを考慮して、少し更新範囲を広げることで、確実に、描画された部分の内容がウィンドウ上に表示されるようにするための対策です。
座標の計算結果と、実際のキャンバス描画の内容は、必ずしも一致するとは限らないので、キャンバスの更新範囲は少し大きめに見積もった方が確実です。
そして、2点がキャンバスの範囲外にあるかどうかの判定と、クリッピングの処理を行った後、2点から幅・高さを計算し、格納先にセットします。
そうして求めたキャンバス範囲を元に、update_canvas() で実際にキャンバスの更新を行います。
つまり、レイヤイメージ上に描画した後に実行しなければならない更新処理です。
レイヤ上に何か描画が行われた場合、まずはその範囲を再度レイヤ合成して、合成結果イメージを更新します。
そして、そのイメージの範囲に対応するキャンバスの範囲を、キャンバス描画で更新して、ウィンドウ上の範囲も更新させます。
なお、レイヤへの描画後に更新する必要があるのは、実際にイメージに描画された部分の範囲だけです。
例えば、1px の点を打って、それを更新するために、わざわざイメージ全体を更新していたのでは、処理の無駄です。
合成結果のイメージとウィンドウ用のイメージは、常に現在の状況を保持しているものとして、新しく描画された部分だけを更新してやれば、無駄を省くことができます。
以下は、実際のコードです。
void update_all(SPTK_RECTWH *imgrc,int time) { SPTK_RECTWH rcCanv; update_image(imgrc); if(!img_to_canvas_rect(&rcCanv, imgrc)) update_canvas(&rcCanv, time); }
update_image() は、指定されたイメージの範囲でレイヤ合成を行い、合成結果のイメージに描画します。
次に、キャンバスを更新するのですが、更新範囲として指定されている imgrc はイメージの座標なので、まずはイメージの範囲をキャンバスの範囲に変換する必要があります。
img_to_canvas_rect() が、範囲を変換する関数です。
img のイメージ範囲をキャンバス範囲に変換して、クリッピングし、canv に格納します。
int img_to_canvas_rect(SPTK_RECTWH *canv,SPTK_RECTWH *img) { int x1,y1,x2,y2; /* 座標変換 */ x1 = img->x, y1 = img->y; x2 = img->x + img->w, y2 = img->y + img->h; img_to_canvas(&x1, &y1); img_to_canvas(&x2, &y2); /* 念の為 1px 広げる */ x1--, y1--; x2++, y2++; /* キャンバス範囲外 */ if(x1 >= CANVAS_W || y1 >= CANVAS_H || x2 < 0 || y2 < 0) return 1; /* 調整 */ if(x1 < 0) x1 = 0; if(y1 < 0) y1 = 0; if(x2 >= CANVAS_W) x2 = CANVAS_W - 1; if(y2 >= CANVAS_H) y2 = CANVAS_H - 1; /* セット */ canv->x = x1; canv->y = y1; canv->w = x2 - x1 + 1; canv->h = y2 - y1 + 1; return 0; }
[x, y, w, h] のイメージ座標をキャンバス座標に変換するためには、まず、範囲の左上・右下の2点をキャンバス座標に変換します。
その座標をクリッピング処理した後、格納先変数にセットします。
まず、左上・右下の2点をセットする際に、(x1, y1) にはそのまま (x, y) を代入しますが、(x2, y2) には実際には +1 させた値を代入しています。
これは、拡大表示時のための措置です。
例えば、100% 以上の倍率時に、(x + w - 1, y + h - 1) の位置をキャンバス座標に変換した場合、そのキャンバス位置は、拡大されたドットの左上の位置となります。
もしも右下をこのイメージ位置で変換した場合、描画された範囲の右端・下端のピクセルが更新範囲に入らないことになります。
今は、ドットの右下、つまりイメージ座標の次のピクセルまでの範囲を更新しなければならないので、イメージの右下の点は +1 する必要がある、というわけです。
そして、その2点を、img_to_canvas() で、イメージ座標からキャンバス座標へ変換しています。
void img_to_canvas(int *x,int *y) { *x = (*x * zoom / 100) + SPACE_WH - scrx; *y = (*y * zoom / 100) + SPACE_WH - scry; }
計算式としては、「キャンバス座標→イメージ座標」の変換と逆の計算を行っています。
その後、キャンバス座標を念の為 1px 拡張しています。
これは、キャンバス描画時の計算の誤差などを考慮して、少し更新範囲を広げることで、確実に、描画された部分の内容がウィンドウ上に表示されるようにするための対策です。
座標の計算結果と、実際のキャンバス描画の内容は、必ずしも一致するとは限らないので、キャンバスの更新範囲は少し大きめに見積もった方が確実です。
そして、2点がキャンバスの範囲外にあるかどうかの判定と、クリッピングの処理を行った後、2点から幅・高さを計算し、格納先にセットします。
そうして求めたキャンバス範囲を元に、update_canvas() で実際にキャンバスの更新を行います。