AndroidJava/Pathで描く、その1

パスで絵を描く、その1(2019-08-14)


何かアプリに画像やら図形やらを表示する時に、
BitmapやらPathやらを使って、画面(Canvas)に描くことができます。

そして、Canvasの指定した座標に対して線を書くのが Pathクラス。
どこかで用意された写真や画像を貼るのではなく、自分で描いたイラストを使うならば、
BitmapよりPathを使った方が何かとラクになります。


Pathの描き方

Path path = new Path(); みたいにインスタンスを作った後に、
path.moveTo() で初期位置を決めて、
path.lineTo() で今の位置から指定位置までの直線を引き、
path.cubicTo() で今の位置から指定位置までの曲線を引きます。
(詳しくはAndroidのAPIを参照、ですが、
 moveTo(), lineTo(), cubicTo() の3つで絵は描けます。あと reset() で初期化)

はじめてこのクラスを見た時には
そんな、座標計算なんてむずかしいことできないし、
cubicTo に至っては、ペジェ曲線?なにそれ、おいしいの?
と思っていましたが、


一旦、話がそれますが、
パソコンでお絵描きする時に、やり方を解説してくれている入門サイトで、
(あるいは、動画で絵を描くところをアップしてくれている時に)
「パス」を使って絵を描いていることが多いと思います。
...はい。そのPathです。

Windowsのペインター(mspaint)には無かった機能なので、
わたしはgimp2を使って初めて「パス」という単語を知りました。

gimp2はフリーの画像作成ソフトですが、
画像作成ソフト、編集ソフトだとパス機能はわりとふつうに用意されていると思います。


画像エディタで描いて、画像ファイルとして保存してから、drawBitmap()する過程で、
もしパスを使って絵を描いているなら、
そのパスを、moveToやらcubicToやらで直接Canvasへ描けば良い、ということです。
つまり、

エディタでパスを描くの図

エディタの座標をpathに見立てるの図

上の画像の例だと Path p が得られるので、
あとは Paint paint = new Paint(); でペイントを作った後に、
canvas.drawPath(p, paint); で描画するという流れですね。
(Paintの話は後述)

コツ、という程ではないですが、
何かの図形、たとえば矢印の図形とかが欲しくて、
そのPathやら座標やらを今すぐに得たい!とかであれば、イメージ画像のように、
任意の描画アプリで実際に矢印とかを描けば良いのですが、
その際に、
キャンバスのサイズを 100 x 100 ピクセルに設定して描きましょう。
その 100px = 100% にみなして(パーセント × canvas.getWidth() で計算して)
Path.lineTo() などに渡せば、イメージがしやすいと思います。

あるいは、これを画像エディタではなく方眼紙に実際に描いてみるのも良いです。
10マスx10マスにざっと描いてみて、イメージができたら、
今度はできるだけ正確に描いて、定規で測って、実際の画面幅に計算する感じです。

(gimp2から直接Path関数にする話は後編で...)

STROKE か FILL か

lineTo() などで作ったpathを使って、
canvas.drawPath(path, paint); するのですが、ここで引数としてPaintを渡します。

もう一つの引数の Paintで、図形の塗りかたを設定します。
Paint paint = new Paint(); のようにインスタンスを作った後に、
paint.setColor(Color.BLACK); で色を指定したり、
paint.setAlpha(127); のように半透明に変えたりできます。

そして、それに加えて
paint.setStyle() を STROKE にするか、FILLにするかを選びます。
(paint.setStyle(Paint.Style.STROKE); というふうに設定する)

STROKEは、線を引くイメージで Paint.setStrokeWidth(線の太さ) とセットで使います。
線の太さを決めないと、特に小さい絵を書く時に絵が線で潰れたり見えなくなったりするので。

FILLは塗りつぶしで、パスで囲んだ部分を指定した色で塗りつぶします。

Pathではないですが、Canvas.drawText()に渡す引数の paint部分を
setStyle で STROKEやFILLで変えてみても、違いが分かると思います。

では、どちらを使うのか?
用途や好み次第ですが、やってみた感想としては...

  • 何かシンプルな、線のみのイラストを描くならSTROKEが楽だと思います。
    STROKEで線画を書くならば、setStrokeWidthで線の太さの調整は必要で、
    たとえば画面横幅の 0.2%くらい(canvas.getWidth() * 0.002f)
    から様子を見て調整する、というイメージです。
  • カラフルなイラスト、特にそれをアプリ内で動的に拡大縮小させて使うなら
    STROKEで線画するよりも、FILLで書いたほうが楽な気がします。
    その場合は、元になるPathを決める時に常に閉じた線を書いておきましょう。
    (moveToの座標から始まり、moveToの座標で描き終わるように)
  • 線の太さを変える、つまりイメージとして、
    ゴシック体では無く明朝体のような文字や絵を描きたいなら、
    STROKEで引くのではなく、FILLで「その形の図形を塗りつぶす」方法になります。
    手間さえかければ、FILLを使って精緻な絵が描けます。

ボタン用の絵として使うための閉じたパスの例

STROKEにせよFILLにせよ、複数のパーツを重ねて描くイメージです。
画像ソフトで言う、レイヤーを重ねる?感覚でしょうか。
例えば、顔を描くなら

  1. まず顔の輪郭部分をFILLで白く塗りつぶす
  2. おなじ輪郭PathをSTROKEで黒の線で描く
  3. 目、鼻、口をSTROKEの黒の線で描く

という感じで、
色とスタイルを決めて、moveTo(), lineTo(), canvas.drawPath()して、
さらに別の絵をその上から重ねて描画...と繰り返すイメージです。

STROKEとFILLでそれっぽい生き物を描く例

こうやって例にすると面倒くさそうですが、
一度作ってしまえば後はどうとでも、応用が利くようにできます。
身体を追加、右足と左足を交互に表示して、歩かせる...徐々に複雑にすれば良いかと。
(コツは「徐々に」複雑に、です...徐々に足していけば、何か作っている感が得られて楽しいので)

もちろん Bitmapでもレイヤーを重ねるようなことは可能なのですが、
(背景が透明な画像を重ねる)
重ねた分だけサイズも増えるので、注意が必要です。
Pathと違って、あっという間にメモリが枯渇したり、処理が重くなったりします。
(透明な画像でもサイズは0ではなく、"透明"というデータを持っています。ARGBですね)

PathはBitmapで必要な初期化やら開放やらの処理が要らないのと、
サイズが小さく済むので、楽です。
写真等ではなく、イラストのようなシンプルな画像であれば
Pathで描画できないか検討してみると、色々と楽になるかもしれません。


これを踏まえて、
つぎはもう少し複雑な話になります。