RaspberryPi
gtk
Geany
ペンタブレット
毛筆

Raspberry Piとペンタブレットで毛筆描画アプリを作る

ラズベリーパイに、ワコムのペンタブレットを接続して、毛筆のような線が書けるアプリの作り方を解説します。

はじめに

  • ワコムのペンタブレットの筆圧を Linuxで検知する方法を調べた結果、ラズベリーパイのOS Raspbian Jessie with PIXEL でも、筆圧をちゃんと検出できることがわかりました。
  • そこでラズベリーパイで、毛筆のような線が描けるアプリの作り方を解説します。初心者が実際に試せるように記事を書きました。Scratch の次に体験してみるとよいと思います。
  • アニメーション作成ソフト「9VAeきゅうべえ(9va-pi)」もこれと同じ方法で開発しています。

必要なもの

ここで学習できるソフト

以下の項目が体験できます。

項目 内容
C プログラム言語
Gtk3 ウィンドウやボタンなどユーザーインタフェースを作るライブラリ
cairo Gtkに含まれる2次元の図形を描くライブラリ
geany 開発環境
wacom9va 筆描画を行うサンプルプログラム。githubというサイトで公開されています
git サンプルプログラムの取得
シェル ターミナルの中で使う命令

サンプルプログラム wacom9va の取得

ターミナルの中で以下のコマンドを入力してサンプルプログラムを取得します。

項目 意味
cd xxx xxxという名前のフォルダに移動
cd ~ ホームフォルダに移動
mkdir xxx xxxという名前のフォルダを作成
ls 今のフォルダの中身を表示
ls xxx xxxというフォルダの中身を表示
git clone xxx xxx からプロジェクトを取得する
& 命令をバックグランドで実行
タブ(Tab)キー 後に続く文字を自動的に選んで補完する
↑↓キー 前に実行した命令を読み出す
Ctrl+C キー プログラムの中止
  1. メニューバーpi-terminal.pngからターミナルを起動。ホームに移動($ はターミナルでの入力を示す記号で入力する必要はありません)
    • $ cd ~
      ホームフォルダに移動します。
  2. 作業用フォルダ「test」を作成し、その中に移動します。
    • $ mkdir test
    • $ cd test
  3. 筆の描画を行うサンプルプログラムを読み込みます。
    • $ git clone https://github.com/dnjiro/wacom9va
  4. 取得したフォルダを確認
    • $ ls
      wacom9va と表示されればOKです。中を見てみると
    • $ ls wacom9va
      linux mac README.md
      中には、Mac と linux 用のフォルダがはいっています。

プログラム開発環境 geany

  • プログラム開発するツールとして geany を使います。
  • Raspbian Jessie with PIXEL には、最初から geany が入っています。もし、geany がはいっていなければ、
    $ sudo apt-get install geany geany-plugins
    で取得できます。

geany の起動

  • linux フォルダの中の makefile を geany で開きます。最後に「&」をつけると、すぐターミナルに戻ります。
    • $ geany wacom9va/linux/makefile &
      最初の数文字入力してからタブキー(Tab)を押せば自動入力されます。上の例なら geany wa タブキー li タブキー mak タブキーで入力できます。

メイク(プログラムの作成)

早速プログラムを作成して、geany の機能をみてみましょう。

  • geany メニューバー > ビルド(Build) 下図A > メイク(Make) をクリック。これでプログラムの作成が始まります。
    pi-geany02.png

  • 結果「Package gtk+-3.0 was not found」 といったエラーメッセージがずらずらと表示されるでしょう。

  • 下のエラーの中から、main.c:8:21: fatal error: gtk/gtk.h: そのようなファイルやディレクトリはありません(No such file or directory) という行(B)をクリックしてください。 main.c のエラーが発生した場所(C)が表示されます。

  • また、左側の関数名(D)をクリックすると、関数の場所が表示されます。

これで、geany の働きがわかったでしょうか。

  • makefile というファイルには、プログラムの作り方が記述されています。「ビルド>メイク」はそれに従ってプログラムを作ります。
  • main.c が c言語で書かれたプログラムです。
  • エラーが発生した#include <gtk/gtk.h> (上図C)は、「gtk フォルダの中の gtk.h」ファイルをこの場所に挿入するという意味です。Gtk の開発用ファイルがラズベリーパイの中にないためエラーになりました。
  • そこで、gtk3 を入手しましょう。

Gtk3 の取得

  • ターミナルから
    • $ sudo apt-get install libgtk-3-dev
    • 続行しますか(Do you want ti continue?) [Y/n] に対して 'y'を入力

※ apt-get でエラーが出る場合、次の記事が参考になります。rasbianでapt-get updateがうまく行かない場合の可能性について

もう一度 geany でメイク

  1. geany メニューバー ビルド「Build」(上図A)>メイク「Make」をクリック
    今度はエラーなく「コンパイル完了」と表示されたはずです。
  2. linuxフォルダの中を見てみましょう。
    • $ ls wacom9va/linux
      main.c main.o makefile wacom9va
      main.c から main.o が作られ(この作業をコンパイルといいます)、その後、wacom9va が作られました(この作業をリンクといいます)。

プログラムの実行

  1. ターミナルから
    • $ ./wacom9va/linux/wacom9va
    • 画面にウィンドウが表示されます。マウスで線が書けます。ワコムペンタブレットなら筆のような線をひくことができます。
      pi-wacom9va.png
  2. もしタブレットのペンを動かしてもカーソルが反応しない場合、次のコマンドで、xserver-xorg-input-wacom を取得してから再起動してみてください。
    $ sudo apt-get install xserver-xorg-input-wacom
  3. プログラムの終了は、右上の「×」ボタンもしくは、ターミナルから Ctrl+C キーです。

プログラムの改良

ここまでできたら、いよいよプログラムを改良してみましょう。main.c の中身を解説します。

  • サンプルプログラムの改良は初心者におすすめです。ちゃんと動作する状態から始めれば、動かなくなった時に直前の修正が間違っていたことがわかります。いろいろ試してみてください。

1.線の色を黒にする

  • 線の色が緑色であることをヒントに、main.c の中から色を指定している場所を探します。cairo_set_source_rgb (qp, 0., 1., 0.);がその箇所で、144行(cb_expose_event)、189行(cb_buton_release) にあります。rgb(red,green blue) が、0,1,0 なので、緑色(green)になります。これを、0,0,0 に変更すれば黒になります。プログラムを修正してほんとに色が黒くなるか確かめてみてください。main.c を修正してから、Ctr+S キーで保存。ビルド>メイク。実行は、ターミナルから「./wacom9va/linux/wacom9va」です。ターミナルで上矢印キーを押せば以前使った命令を実行できます。
  • 144行(cb_expose_event)、189行(cb_buton_release) の2箇所にあるのはなぜでしょうか。片方だけ黒にして、この2つがどういういう働きをしているか試してみてください。
  • 144行は、if(offWidth!=ww || offHeight!=hh || offDelta != whDelta){ という if 文(条件分岐)の中にはいっています。「!=」は、値が異なるの意味。「||」は「または」です。offWidth、offHeight、offDelta は、それぞれオフスクリーンの幅、高さ、スクロール量です。この部分はオフスクリーンという画像の状態が、ウィンドウの状態と違った場合、オフスクリーンの中身を書き直すという処理を行っています。
  • 189行(cb_buton_release) では、ペンを離したときオフスクリーンに1本の線だけ書いています。187行cairo_t *qp = cairo_create(offscreen) が書き込み先をオフスクリーン(offscreen)にするという意味です。
  • cairo は、Gtk の中に含まれる2次元の描画を行うライブラリです。cairo では、cairo_tで指定される画面に、別の画面を転送することで絵を書きます。cairo_set_source_rgb (qp, 0., 1., 0.);は、転送元を固定色にする命令です。ほかに転送元をグラデーションにしたり、画像にしたりできます。cb_expose_event 関数の中にある c cairo_set_source_surface(cr, offscreen, 0, 0); cairo_paint (cr); は、転送元をオフスクリーン(offscreen)にして、全体を塗りつぶし(cairo_paint)ています。その結果、オフスクリーンからウィンドウへ画面を転送するという意味になります。
  • オフスクリーンを使うのは高速化のためです。線を書くたびにオフスクリーンに書き込んでいき、画面を再描画するときにオフスクリーンから画面転送しています。こうすれば線の数が増えても遅くなりません。

2.起動時のウィンドウサイズを変える

  • 217行(main) window = gtk_window_new(GTK_WINDOW_TOPLEVEL); でウィンドウを作成しています。その後何も設定していません。ここにウィンドウサイズの設定を入れましょう。
  • ウィンドウ関係の関数を調べるために、gtk_window_new をネットで検索すれば、GtkWindow gtk+3 が見つかります。
  • 多くの関数がありますが、中からウィンドウサイズを設定しそうな名前を探します。gtk_window_resize () が見つかりました。gtk_window_resize (window,800,600);と入れてみて、ウィンドウサイズが変わるかどうか確かめてみましょう。
  • この関数は、214行、int main(int argc, char *argv[]) という関数の中にあります。ターミナルから起動するプログラムは、main 関数からスタートします。

3.上下矢印キーでスクロールできるようにする

  • main() 関数からプログラムがどう動くか説明しましょう。main()関数からスタートしたあと、gtk_main(); まで進んでイベント待ちになります。
  • その前にどんなイベントに対して、どんな処理をするかが設定されます。例えば、221行め g_signal_connect(window, "button_press_event", G_CALLBACK( cb_button_press_event ), NULL );は、 "button_press_event" というイベントに対し、関数 cb_button_press_event()を実行するという意味です。これは、ボタンが押されたときの処理です。
  • もうひとつ必要な設定が、225-228行
gtk_widget_set_events( window, GDK_EXPOSURE_MASK 
      | GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
      | GDK_SCROLL_MASK 
);

です。これは、このプログラムがどのようなイベントを受け付けるかの設定で、「GDK_BUTTON_PRESS_MASK」を設定しておかなければ、"button_press_event" が送られてきません。「|」はビット演算のORで、どんなイベントを受け付けるかを設定しています。
- このプログラムのイベント処理は次のようになります。

イベント 処理関数 マスク
"button_press_event"
ペンダウン
cb_button_press_event GDK_BUTTON_PRESS_MASK
"motion_notify_event"
ペンの移動
cb_motion_notify_event GDK_POINTER_MOTION_MASK
"button_release_event"
ペンアップ
cb_button_release_event GDK_BUTTON_RELEASE_MASK
"draw"
再描画
cb_expose_event GDK_EXPOSURE_MASK
"scroll_event"
スクロール
cb_scroll_event GDK_SCROLL_MASK
"destroy"
終了
gtk_main_quit
  • 上下矢印キーで画面がスクロールするようにするために、次の情報を調べましょう。

    • キーを押したときに発生するイベント> "key-press-event"
    • キーイベントを受け取るマスク> GDK_KEY_PRESS_MASK
    • 押されたキーが上下矢印であることの判定。
  • このプログラムは、マウスホイールを回転させると画面が上下にスクロールします。その処理は、cb_scroll_event()関数です。

gint cb_scroll_event( GtkWidget *widget, GdkEventScroll *event, gpointer user_data )
{
    switch ( event->direction ) { 
    case GDK_SCROLL_DOWN:   whDelta += 10; break;   
    case GDK_SCROLL_UP:     whDelta -= 10; break;   
    }
    gdk_window_invalidate_rect(gtk_widget_get_window(widget),NULL,FALSE);
    return TRUE;
}

  • スクロールの方向によって、whDelta という変数の値を増やしたり減らしたりしています。whDelta += 10; は、whDeltaに10を加えるという意味です。これと同じように、上矢印キー、下矢印キーがきたら、whDelta を増減させる関数を追加してみてください。

4.u キーで Undo、 r キーで Redoできるようにする

  • 次は、線のデータがどのように処理されているか見てみましょう。ペンダウンの処理は、cb_button_press_event() です。
gint cb_button_press_event( GtkWidget *widget, GdkEventMotion *event, gpointer user_data )
{
    isDown = 1;
    m_points[m_index] = m_pointX[m_index][0] = m_pointY[m_index][0] = 0;
    m_index++;
    return TRUE;
}
  • ここで、m_index、m_points、m_pointX、m_pointYは次の意味を持ちます。m_points、m_pointX、m_pointY、m_pressure は配列です。
変数 意味
m_index 入力した線の本数
m_points[nn] nn番目の線に含まれる点の数
m_pointX[nn][kk] nn番目の線のkk番目の点のX座標
m_pointY[nn][kk] nn番目の線のkk番目の点のY座標
m_pressure[nn][kk] nn番目の線のkk番目の点の太さ
  • そこで、m_index を一つ減らせば、最後に描いた線を書かなかったことになり(Undo)。逆に増やせば、線が復活するはずです(Redo)。例えば、次のようにしてみましょう。 最後の offWidth = 0; と gdk_window_invalidate_rect() 画面を書き直す関数です。これをいれないと、画面が変化しません。
gint cb_key_press_event( GtkWidget *widget, GdkEventKey *event,gpointer *user_data)
{
    switch(event -> keyval){
    case 'u':
        if(m_index > 0){
            m_index--;
        }
        break;
    case 'r':
        if(m_points[m_index] > 0){
            m_index++;
        }
        break;
    default:
        return 0;
    }
    offWidth = 0;
    gdk_window_invalidate_rect( gtk_widget_get_window(widget), NULL, FALSE);
    return TRUE;
}
  • gdk_window_invalidate_rect()は、画面が変化したことを伝える関数です。この関数の中で画面が描画されるわけではありません。画面の書き直しはウィンドウのサイズを変えたときなど、アプリの状態とは無関係に発生します。様々な状況を判断し、適切なタイミングで、"draw"イベントが発生し、cb_expose_event() 関数で再描画が行われます。 offWidth = 0 としておけば、オフスクリーンの書き直しが行われます。

  • キーに反応するためには、main 関数に、次のように g_signal_connect関数と GDK_KEY_PRESS_MASK の追加が必要です。

    g_signal_connect(window, "key_press_event",G_CALLBACK( cb_key_press_event ), NULL );
    gtk_widget_set_events( window, GDK_EXPOSURE_MASK 
      | GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
      | GDK_SCROLL_MASK | GDK_KEY_PRESS_MASK
      );
  • さて、上の例では、'r' を押したとき、前に書いた線まで書いてしまう問題があります。どう修正すればよいか考えてみてください。

5.マウスで描いても線の太さを変える

  • ワコムのペンタブレットがない場合でも、線の太さを変えるようにしてみましょう。筆圧を取得しているのは、ペンの移動に対応した関数 cb_motion_notify_event() です。
gint cb_motion_notify_event( GtkWidget *widget, GdkEventMotion *event, gpointer user_data )
{
    if(offscreen){
        if(isDown){
            gdouble pressure =0;
            if(!gdk_event_get_axis ((GdkEvent *)event, GDK_AXIS_PRESSURE, &pressure)){
                pressure =0;
            }
            AddPolygonPoint(m_index-1, event->x, event->y, pressure * 16);
        }
    }
    return TRUE;
}
  • gdk_event_get_axis ((GdkEvent *)event, GDK_AXIS_PRESSURE, &pressure) で筆圧を検出しています。ここで、pressure は筆圧を取得する浮動小数点(gdouble)の変数です。&pressure で変数が配置されている場所(ポインタ)を関数にわたし、結果を、pressure の中に入れてもらいます。その戻り値が0(失敗)だった場合、関数の前の「!」(Not)によって、1に変わり、if 文の中(pressure = 0)が実行されます。
  • そこで、pressure = 0; を、`pressure = (gdouble)(rand()%100) / 100;' に変更してみましょう。rand()%100 は、整数の乱数 rand() を 100 で割った余りで 0 から 99 になります。(gdouble) は整数をを小数点に変換するという意味で、これがないと整数演算になってしまい割り算の結果が0になってしまいます。
  • マウスで書くと、おもしろい線が書けたと思います。pressure の値をうまく設定すれば、筆のような線がかけるでしょう。挑戦してみてください。

6.線の太さを変えるアルゴリズム

  • オフスクリーンに対して、全部の線を書き直している部分が、cb_expose_event() 関数の中にあります。
        {   
            cairo_t *qp = cairo_create(offscreen);
            cairo_set_line_width(qp, 0);
            cairo_set_source_rgb (qp, 0., 1., 0.);
            cairo_new_path (qp);        
            for(in=0; in < m_index; in++){
                for(pnt=0; pnt< m_points[in]; pnt++){ 
                    MakeBrushLine(qp, in, pnt);
                }
            }
            cairo_fill (qp);
            cairo_destroy(qp);    
        }
  • cairo_new_path (qp);で、パス(多角形)を初期化します。
  • MakeBrushLine(qp, in, pnt); の中で、多角形の頂点を追加していきます。実際に追加しているのは、 asSetPoint() 関数です。
void     asSetPoint(cairo_t *qp, int pnt, int x, int y)
{
    if(!pnt) cairo_move_to(qp, x, y - whDelta );
    else     cairo_line_to(qp, x, y - whDelta );
}
  • 最初(pntが0)の点には移動(cairo_move_to)し、次の点からつなぎます(cairo_line_to)。
  • MakeBrushLine 関数の中では、記録した頂点から、m_pressure に記録された大きさの12角形の頂点を計算し、2つの12角形を囲む多角形を何個も作っていきます。
  • 最後に、cairo_fill (qp) で多角形を一気に塗りつぶします。
  • この12角形を作っている部分を修正すれば、縦が太く、横が細い、Gペンのような線がひけるでしょう。

7.この後の展開

これをベースに以下のような改良を考えてみてはどうでしょうか。

  • 数字キーを押して、色を変更する
  • 'h' キーを押すと半透明で書けるようにする。cairo には半透明で描画する機能があります。
  • 画像の読み込み。cairo に画像を読み込む関数があります。オフスクリーンに画像を描くようにすれば、写真の上に筆で書き込むプログラムが作れるでしょう。
  • メニューバーをつける。難しいかもしれません。ぜひ挑戦してみてください。

参考にしたサイト


関連記事

  1. 無料ソフトでアニメを作ってみよう(9VAe きゅうべえ)
  2. スクラッチ、ビスケットの次は 9VAe=第3のプログラミング学習環境
  3. 書き順アニメーションの作り方
  4. 9VAeきゅうべえ:長いアニメを作る方法
  5. 動くLINEスタンプのAPNG作成:無料ソフト9VAeきゅうべえ
  6. 9VAeきゅうべえ」で絵を描かずに作れるGIFアニメ
  7. 9VAeきゅうべえで作成したSVGアニメーション
  8. アニメソフト 9VAe をカスタマイズする方法
  9. 9VAeをキッズプラザ大阪向けに改造する
  10. 9VAe / 9svg データフォーマット解説
  11. フリーソフト9VAeきゅうべえで簡単デジタルサイネージ