ワコム筆圧ペンで太さが変わる線をひくアルゴリズム

アニメ作成フリーソフト「9VAeきゅうべえ」に、ワコムのペンタブレットの筆圧に応じて、太さが変化する線を搭載しました。筆圧に応じて太さが変化する線を、簡単なアルゴリズムで実現しているので紹介します。

- プロジェクトは、[https://github.com/dnjiro/wacom9va](https://github.com/dnjiro/wacom9va) から取得できます。

Ver.0.5.0<br>![0500w600.gif](https://qiita-image-store.s3.amazonaws.com/0/67734/a3381c11-6aec-01cf-5762-fa804b676559.gif)


# 太さが変わる線をひくプログラム

- Mac / Linuxで、Wacomペンタブレットの筆圧を検知して線をひく適当なサンプルプログラムが見つからず、いろいろ試行錯誤を繰り返したので、結果をシェアします。
- このアルゴリズムは以下のフリーソフト9VAeきゅうべえに搭載しています。


- Win/Mac/Linux版はリンク先右上の ![Untitled.png](https://qiita-image-store.s3.amazonaws.com/0/67734/dbea08ea-33e3-c9f2-aa04-73528cfa0f7f.png) をクリックして**ダウンロード**

|[iOS<br>iPad<br>![qvadanb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67734/e6527c3c-7ec3-e357-37c2-2765538e47e0.png)](https://apps.apple.com/jp/app/9vae-iphone/id1482450143)|[Android<br>Chromebook<br>![qvaqva.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67734/6337d377-d49c-f68e-eff5-89d3bf50487d.png)](https://play.google.com/store/apps/details?id=com.qvae)|[Win<br>![qvaqva.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67734/6337d377-d49c-f68e-eff5-89d3bf50487d.png)](https://drive.google.com/file/d/1tIuOkL0TNhPaP3UfYl2pI7Cfk49QNfn3/view?usp=drive_link)|[Mac<br>![qvaqva.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67734/6337d377-d49c-f68e-eff5-89d3bf50487d.png)<br>M1対応](https://drive.google.com/file/d/13yLPjYXv_fSH1MeSMfhyf_x7c3D02hJS/view?usp=drive_link)|[Raspberry Pi<br>![qvaqva.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67734/6337d377-d49c-f68e-eff5-89d3bf50487d.png)](https://drive.google.com/file/d/1HxKGRhI-v4F4Io3PShy8KVtLqSnNEcCy/view?usp=drive_link)|[Ubuntu<br>32bit<br>![qva32b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67734/580af743-7e1c-7a04-2966-470f4a33537c.png)](https://drive.google.com/file/d/1VebfBt-0LsnfN7J4q6Nbv_wMOd5PpBIM/view?usp=sharing)|
|:--:|:--:|:--:|:--:|:--:|:--:|
|[9VAePro<br>![qvapro64.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67734/46826bc3-250a-5133-f827-3a4d086e2f4d.png)<br>* 599<br>Page](https://apps.apple.com/jp/app/9vaepro/id1493183090)|[Amazon<br>Fire<br>![qvaqva.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67734/6337d377-d49c-f68e-eff5-89d3bf50487d.png)](https://www.amazon.co.jp/dp/B082MYHTT7/ref=sr_1_1)|上が<br>見えない<br>場合は<br>Vector<br>↓|[古いMac版<br>10.7-10.11](https://drive.google.com/file/d/1QTwckowY7DTdbxPFtVW-OA4IR0umaYjw/view?usp=sharing)||[Ubuntu<br>64bit<br>![qva64b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67734/e3edbeac-ece0-c506-e02f-cf5e0f5dd04e.png)](https://drive.google.com/file/d/1H6UkwUNdjTAX6_IvbFmkvXAHot6ASC5y/view?usp=sharing)|
||[Android<br>Chromebook<br>![qvadanb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67734/e6527c3c-7ec3-e357-37c2-2765538e47e0.png)](https://play.google.com/store/apps/details?id=com.qvaDangla)|[Win<br>(Vector)](https://www.vector.co.jp/soft/dl/winnt/art/se515201.html)<br>|[Mac<br>(Vector)](https://www.vector.co.jp/soft/dl/mac/art/se515202.html)|[RaspberryPi<br>(Vector)](https://www.vector.co.jp/soft/other/other/se515318.html)|[安全性<br>は？](https://www.vector.co.jp/info/vtest.html)|


| [インストール方法＞](https://qiita.com/danjiro/items/1debda10c37fd8a55927)|[windows](https://qiita.com/danjiro/items/1debda10c37fd8a55927#windows%E7%89%889va-win)|[Mac](https://qiita.com/danjiro/items/1debda10c37fd8a55927#mac%E7%89%889va-mac)|[Pi](https://qiita.com/danjiro/items/1debda10c37fd8a55927#raspberry-pi%E7%89%889va-pi)|[Linux](https://qiita.com/danjiro/items/1debda10c37fd8a55927#ubuntu--linux-mint--xtra-pc-%E7%89%889va-pi)|
|:--:|:--:|:--:|:--:|:--:|
|[●アニメの作り方](https://dnjiro.hatenablog.com/archive/category/%E3%82%A2%E3%83%8B%E3%83%A1%E3%81%AE%E4%BD%9C%E3%82%8A%E6%96%B9)|[ボタン説明](https://qiita.com/danjiro/items/ea71567fa8c313f6f3fc)|[メニュー<br>キー](https://9vae.blogspot.com/p/scl0mnu.html)|[9VAe質問](https://chiebukuro.yahoo.co.jp/search?p=9vae)|[だんグラ](https://dangla.jp/)|
|動画出力|[ffmpeg](https://youtu.be/Lc5-ac01n6w)|〇|→|[ffmpeg](https://youtu.be/Lc5-ac01n6w)|
|しゃべる|[SofTalk](https://youtu.be/0k9bAN4Jge4)|〇|→|[OpenJTalk](https://youtu.be/Khx31_0IWcE)|




### サンプルプログラムの仕様

- ワコムペンタブレットを使って、ウィンドウの中に毛筆のような線をひくことができます。
- マウスホイールや、２本指スクロール（Macintosh）を使って、画面を上下にスクロールさせれば、無限につづいたロールに書くことができます。

#  Linux版 プログラム

- ビルドには Gtk+3 開発用ヘッダ, 実行には X server Wacom input ドライバーが必要です。取得例<br>
 `$ sudo apt-get install libgtk-3-dev` <br> `$ sudo apt-get install xserver-xorg-input-wacom`

```c
//  main.c
//  wacom9va
//
//  Created by user on 17/02/26.
//  Copyright __EVA*Project__ 2017. All rights reserved.
//  you need libgtk-3-dev, xserver-xorg-input-wacom

#include <gtk/gtk.h>
#include <gdk/gdk.h>
#include <math.h>

#define Max(x,y)        ((x)>=(y) ? (x) : (y))
#define Abs(x)          ((x)>=0   ? (x) : -(x))


int		m_index = 0;		// Number of input strokes
int		isDown = 0;			// 1= mouse button down 0= up
int		whDelta = 0;		// Scroll Offset (Changed by mouse wheel)

// Stroke data
#define AnLINEMAX  1000  	//Maximum strokes number 
#define AnLPNTMAX  250  	//Maximum points number in one stroke

int		m_points[AnLINEMAX];				// Number of points
int		m_pointX[AnLINEMAX][AnLPNTMAX];		// x
int		m_pointY[AnLINEMAX][AnLPNTMAX];		// y
int		m_pressure[AnLINEMAX][AnLPNTMAX];	// pressure


GtkWidget *window;
cairo_surface_t *offscreen=NULL;
int offWidth = 0;
int offHeight = 0;
int	offDelta = 0;

//--------------------------------------------------------------------
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 );
}

//--------------------------------------------------------------------
void	 MakeBrushLine(cairo_t *qp, int in, int pnt)
{
	int x1,y1,w1,u1,x2,y2,w2,u2,v1,v2, dir, kk;
	double aPI = 3.1415926535897932;
	double qq, pi8 = aPI/12;
	
	kk=0;
	x2 = m_pointX[in][pnt];
	y2 = m_pointY[in][pnt];
	w2 = m_pressure[in][pnt];	
	w2 = Max(1,w2);
	v2 = (w2*1732/2000);u2=Max(1,w2/2);
	dir = 0;	
	for(;pnt>0;){
		x1 = m_pointX[in][pnt-1];
		y1 = m_pointY[in][pnt-1];
		w1 = m_pressure[in][pnt-1];
		w1 = Max(1,w1);
		v1 = (w1*1732/2000);u1=Max(1,w1/2);
		qq = atan2((double)y2-y1,(double)x2-x1);
		for(;qq<0;) qq+=2*aPI;
		dir = (int)((qq+pi8) / pi8/2);
		switch(dir){ //no break in following lines
			case 0: asSetPoint(qp, kk, x1   , y1+w1); if(++kk==7) break;
			case 1: asSetPoint(qp, kk, x1-u1, y1+v1); if(++kk==7) break;
			case 2: asSetPoint(qp, kk, x1-v1, y1+u1); if(++kk==7) break;
			case 3: asSetPoint(qp, kk, x1-w1, y1   ); if(++kk==7) break;
			case 4: asSetPoint(qp, kk, x1-v1, y1-u1); if(++kk==7) break;
			case 5: asSetPoint(qp, kk, x1-u1, y1-v1); if(++kk==7) break;
			case 6: asSetPoint(qp, kk, x1   , y1-w1); if(++kk==7) break;
			case 7: asSetPoint(qp, kk, x1+u1, y1-v1); if(++kk==7) break;
			case 8: asSetPoint(qp, kk, x1+v1, y1-u1); if(++kk==7) break;
			case 9: asSetPoint(qp, kk, x1+w1, y1   ); if(++kk==7) break;
			case 10:asSetPoint(qp, kk, x1+v1, y1+u1); if(++kk==7) break;
			default:asSetPoint(qp, kk, x1+u1, y1+v1); if(++kk==7) break;
				asSetPoint(qp, kk, x1   , y1+w1); if(++kk==7) break;
				asSetPoint(qp, kk, x1-u1, y1+v1); if(++kk==7) break;
				asSetPoint(qp, kk, x1-v1, y1+u1); if(++kk==7) break;
				asSetPoint(qp, kk, x1-w1, y1   ); if(++kk==7) break;
				asSetPoint(qp, kk, x1-v1, y1-u1); if(++kk==7) break;
				asSetPoint(qp, kk, x1-u1, y1-v1); if(++kk==7) break;
				asSetPoint(qp, kk, x1   , y1-w1); if(++kk==7) break;
		}
		break;
	}
	switch(dir){  //no break in following lines
			case 0: asSetPoint(qp, kk, x2,    y2-w2); if(++kk==14) break;
			case 1: asSetPoint(qp, kk, x2+u2, y2-v2); if(++kk==14) break;
			case 2: asSetPoint(qp, kk, x2+v2, y2-u2); if(++kk==14) break;
			case 3: asSetPoint(qp, kk, x2+w2, y2)   ; if(++kk==14) break;
			case 4: asSetPoint(qp, kk, x2+v2, y2+u2); if(++kk==14) break;
			case 5: asSetPoint(qp, kk, x2+u2, y2+v2); if(++kk==14) break;
			case 6: asSetPoint(qp, kk, x2,    y2+w2); if(++kk==14) break;
			case 7: asSetPoint(qp, kk, x2-u2, y2+v2); if(++kk==14) break;
			case 8: asSetPoint(qp, kk, x2-v2, y2+u2); if(++kk==14) break;
			case 9: asSetPoint(qp, kk, x2-w2, y2)   ; if(++kk==14) break;
			case 10:asSetPoint(qp, kk, x2-v2, y2-u2); if(++kk==14) break;
			default:asSetPoint(qp, kk, x2-u2, y2-v2); if(++kk==14) break;
				asSetPoint(qp, kk, x2,    y2-w2); if(++kk==14) break;
				asSetPoint(qp, kk, x2+u2, y2-v2); if(++kk==14) break;
				asSetPoint(qp, kk, x2+v2, y2-u2); if(++kk==14) break;
				asSetPoint(qp, kk, x2+w2, y2)   ; if(++kk==14) break;
				asSetPoint(qp, kk, x2+v2, y2+u2); if(++kk==14) break;
				asSetPoint(qp, kk, x2+u2, y2+v2); if(++kk==14) break;
				asSetPoint(qp, kk, x2,    y2+w2); if(++kk==14) break;
	}
}

//--------------------------------------------------------------------
void AddPolygonPoint(int in, int x, int y, int prs)
{    
	if( in < AnLINEMAX ){
		int pnt = m_points[in];
		if( pnt < AnLPNTMAX ){
			m_pointX[in][pnt] = x;
			m_pointY[in][pnt] = y + whDelta;
			m_pressure[in][pnt] = prs;
			m_points[in]++;
		}
	}
}


gboolean cb_expose_event(GtkWidget *widget, cairo_t *cr, gpointer data)
{
	int in, pnt;
	int ww = gdk_window_get_width(gtk_widget_get_window(widget));
	int hh = gdk_window_get_height(gtk_widget_get_window(widget));
    if(offWidth!=ww || offHeight!=hh || offDelta != whDelta){//Resize offscreen
		if(offscreen != NULL){
			cairo_surface_destroy(offscreen);
		}
		offscreen = cairo_surface_create_similar(cairo_get_target(cr), 
                         CAIRO_CONTENT_COLOR_ALPHA, ww, hh);
		offWidth  = ww;
		offHeight = hh;
		offDelta  = whDelta;
		{   
			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_set_source_surface(cr, offscreen, 0, 0);
	cairo_paint (cr);
    return FALSE;
}

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;
}


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;
}

gint cb_button_release_event( GtkWidget *widget, GdkEventMotion *event, gpointer user_data )
{
	isDown = 0;
	{ int in,pnt; //Draw line on offscreen 
			cairo_t *qp = cairo_create(offscreen);
			cairo_set_line_width(qp, 0.);
			cairo_set_source_rgb (qp, 0., 1., 0.);
			cairo_new_path (qp);
			
			in = m_index-1;
			for(pnt=0; pnt< m_points[in]; pnt++){ 
					MakeBrushLine(qp, in, pnt);
			}
			
			cairo_fill (qp);
			cairo_destroy(qp);    
			gdk_window_invalidate_rect(gtk_widget_get_window(widget),NULL,FALSE);
	}
    return TRUE;
}

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;
}

int main(int argc, char *argv[])
{
    gtk_init(&argc, &argv);
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    
    g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
    g_signal_connect(window, "motion_notify_event", G_CALLBACK( cb_motion_notify_event ), NULL );
    g_signal_connect(window, "button_press_event", G_CALLBACK( cb_button_press_event ), NULL );
    g_signal_connect(window, "button_release_event", G_CALLBACK( cb_button_release_event ), NULL );
	g_signal_connect(window, "draw", G_CALLBACK(cb_expose_event), NULL);
	g_signal_connect(window, "scroll_event",G_CALLBACK( cb_scroll_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 
      );


    gtk_widget_set_app_paintable(window, TRUE);
    gtk_widget_show(window);
    gtk_main();

	if(offscreen != NULL){
		cairo_surface_destroy(offscreen);
	}
    return 0;
}
```




### 説明
- 線のデータは、以下の配列に格納されています。AnLINEMAX本の線が入力できます。各ストロークの点数を格納した配列が m_points[AnLINEMAX]です。１本のストロークは最大AnLPNTMAX点までとしています。

```c
int     m_points[AnLINEMAX];                // Number of points
int     m_pointX[AnLINEMAX][AnLPNTMAX];     // x
int     m_pointY[AnLINEMAX][AnLPNTMAX];     // y
int     m_pressure[AnLINEMAX][AnLPNTMAX];   // pressure

```

- 太さが変わる線を描画しているのは、DrawPage() 、MakeBrushLine() 、asSetPoint()です。
- 各座標点の周囲に線の太さに対応した１２角形を描画し、それぞれを２本の線でつないで中を塗りつぶします。線の方向に応じて２本の線をどこにひくかを求めているのが、MakeBrushLine() です。asSetPoint()でパスを生成し、DrawPage()　の中の　CGContextDrawPath()　で描画しています。
- 筆圧の検出は、関数cb_motion_notify_event() の中で、gdk_event_get_axis ((GdkEvent *)event, GDK_AXIS_PRESSURE, &pressure) を実行して取得しています。    








# Macintosh版

- 簡略化のため、ウィンドウの位置やサイズの変更の処理は省いています。
- 毎回全データを書きなおしているため、線の数が増えると応答が悪くなります。
- 高速化するには、上のLinux版のようにオフスクリーンにいったん描画してから、必要な領域だけ転送すればよいでしょう。

```c

//
//  main.c
//  wacom9va
//
//  Created by user on 17/02/01.
//  Copyright __EVA*Project__ 2017. All rights reserved.
//

#include <Carbon/Carbon.h>

#define kCSkDocViewClassID	CFSTR( "com.9vae.wacomtest.view" )


static OSStatus        HandleNewWindow();
static OSStatus        WindowEventHandler( EventHandlerCallRef inCaller, EventRef inEvent, void* inRefcon );
static IBNibRef        sNibRef;

enum { 
    kDocumentViewSignature	= 'CskV',
    kDocumentBoundsParam	= 'Boun'
};


#define Max(x,y)        ((x)>=(y) ? (x) : (y))
#define Abs(x)          ((x)>=0   ? (x) : -(x))

struct CGrgba {
    CGFloat   r;
    CGFloat   g;
    CGFloat   b;
    CGFloat   a;
};
typedef struct CGrgba CGrgba;

struct CanvasData {
    HIViewRef		theView;
};
typedef struct CanvasData   CanvasData;

HIViewRef			theScrollView;


Rect 	m_bounds;			// Bounding rectangle of window
int		m_index = 0;		// Number of input strokes
int		m_press;			// Pressure
int		m_scrollY = 0;		// Scroll Offset (Changed by mouse wheel)
int		isDown = 0;			// 1= mouse button down 0= up

// Stroke data
#define AnLINEMAX  1000  	//Maximum strokes number 
#define AnLPNTMAX  250  	//Maximum points number in one stroke

int		m_points[AnLINEMAX];				// Number of points
int		m_pointX[AnLINEMAX][AnLPNTMAX];		// x
int		m_pointY[AnLINEMAX][AnLPNTMAX];		// y
int		m_pressure[AnLINEMAX][AnLPNTMAX];	// pressure

CGMutablePathRef	m_path;


//--------------------------------------------------------------------
void	 asSetPoint(int pnt, int x, int y)
{
	if(!pnt) CGPathMoveToPoint(m_path , NULL, x, y +m_scrollY);
	else     CGPathAddLineToPoint(m_path , NULL, x, y +m_scrollY);
}

//--------------------------------------------------------------------
void	 MakeBrushLine(int in, int pnt)
{
	int x1,y1,w1,u1,x2,y2,w2,u2,v1,v2, dir, kk;
	double aPI = 3.1415926535897932;
	double qq, pi8 = aPI/12;
	
	kk=0;
	x2 = m_pointX[in][pnt];
	y2 = m_pointY[in][pnt];
	w2 = m_pressure[in][pnt];	
	w2 = Max(1,w2);
	v2 = (w2*1732/2000);u2=Max(1,w2/2);
	dir = 0;	
	for(;pnt>0;){
		x1 = m_pointX[in][pnt-1];
		y1 = m_pointY[in][pnt-1]; if(Abs(y1-y2)>m_bounds.bottom/2) break;
		w1 = m_pressure[in][pnt-1];
		w1 = Max(1,w1);
		v1 = (w1*1732/2000);u1=Max(1,w1/2);
		qq = atan2((double)y2-y1,(double)x2-x1);
		for(;qq<0;) qq+=2*aPI;
		dir = (int)((qq+pi8) / pi8/2);
		switch(dir){ //no break in following lines
			case 0: asSetPoint(kk, x1   , y1+w1); if(++kk==7) break;
			case 1: asSetPoint(kk, x1-u1, y1+v1); if(++kk==7) break;
			case 2: asSetPoint(kk, x1-v1, y1+u1); if(++kk==7) break;
			case 3: asSetPoint(kk, x1-w1, y1   ); if(++kk==7) break;
			case 4: asSetPoint(kk, x1-v1, y1-u1); if(++kk==7) break;
			case 5: asSetPoint(kk, x1-u1, y1-v1); if(++kk==7) break;
			case 6: asSetPoint(kk, x1   , y1-w1); if(++kk==7) break;
			case 7: asSetPoint(kk, x1+u1, y1-v1); if(++kk==7) break;
			case 8: asSetPoint(kk, x1+v1, y1-u1); if(++kk==7) break;
			case 9: asSetPoint(kk, x1+w1, y1   ); if(++kk==7) break;
			case 10:asSetPoint(kk, x1+v1, y1+u1); if(++kk==7) break;
			default:asSetPoint(kk, x1+u1, y1+v1); if(++kk==7) break;
				asSetPoint(kk, x1   , y1+w1); if(++kk==7) break;
				asSetPoint(kk, x1-u1, y1+v1); if(++kk==7) break;
				asSetPoint(kk, x1-v1, y1+u1); if(++kk==7) break;
				asSetPoint(kk, x1-w1, y1   ); if(++kk==7) break;
				asSetPoint(kk, x1-v1, y1-u1); if(++kk==7) break;
				asSetPoint(kk, x1-u1, y1-v1); if(++kk==7) break;
				asSetPoint(kk, x1   , y1-w1); if(++kk==7) break;
		}
		break;
	}
	switch(dir){  //no break in following lines
			case 0: asSetPoint(kk, x2,    y2-w2); if(++kk==14) break;
			case 1: asSetPoint(kk, x2+u2, y2-v2); if(++kk==14) break;
			case 2: asSetPoint(kk, x2+v2, y2-u2); if(++kk==14) break;
			case 3: asSetPoint(kk, x2+w2, y2)   ; if(++kk==14) break;
			case 4: asSetPoint(kk, x2+v2, y2+u2); if(++kk==14) break;
			case 5: asSetPoint(kk, x2+u2, y2+v2); if(++kk==14) break;
			case 6: asSetPoint(kk, x2,    y2+w2); if(++kk==14) break;
			case 7: asSetPoint(kk, x2-u2, y2+v2); if(++kk==14) break;
			case 8: asSetPoint(kk, x2-v2, y2+u2); if(++kk==14) break;
			case 9: asSetPoint(kk, x2-w2, y2)   ; if(++kk==14) break;
			case 10:asSetPoint(kk, x2-v2, y2-u2); if(++kk==14) break;
			default:asSetPoint(kk, x2-u2, y2-v2); if(++kk==14) break;
				asSetPoint(kk, x2,    y2-w2); if(++kk==14) break;
				asSetPoint(kk, x2+u2, y2-v2); if(++kk==14) break;
				asSetPoint(kk, x2+v2, y2-u2); if(++kk==14) break;
				asSetPoint(kk, x2+w2, y2)   ; if(++kk==14) break;
				asSetPoint(kk, x2+v2, y2+u2); if(++kk==14) break;
				asSetPoint(kk, x2+u2, y2+v2); if(++kk==14) break;
				asSetPoint(kk, x2,    y2+w2); if(++kk==14) break;
	}
}

//--------------------------------------------------------------------
CGColorSpaceRef GetGenericRGBColorSpace(void)
{
    static CGColorSpaceRef genericRGBColorSpace = NULL;
    if (genericRGBColorSpace == NULL){
		genericRGBColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
    }
    return genericRGBColorSpace;
}


//--------------------------------------------------------------------
void DrawPage(CGContextRef ctx)
{    
   	int in,pnt;
	const CGrgba blackColor	    = { 0.0, 0.0, 0.0, 1.0 };
    CGColorSpaceRef genericColorSpace  = GetGenericRGBColorSpace();
	
	CGContextSaveGState(ctx);	// because SetContextStateForDrawObject is doing what it says it will
	CGContextSetFillColorSpace(ctx, genericColorSpace); 
	CGContextSetStrokeColor( ctx, (CGFloat*)&blackColor);		
	CGContextSetFillColor( ctx, (CGFloat*)&blackColor);		
	CGContextBeginPath(ctx);			// reset current path to empty
	for(in=0; in < m_index; in++){
		m_path = CGPathCreateMutable();
		for(pnt=0; pnt< m_points[in]; pnt++){ 
			MakeBrushLine(in, pnt);
		}
		CGContextAddPath(ctx, m_path);
	}
	CGContextDrawPath(ctx, kCGPathFillStroke);
	CGContextRestoreGState(ctx);	// undo the changes for the specific obj drawing
}

//--------------------------------------------------------------------
void AddPolygonPoint(int in, int x, int y, int prs)
{    
	if( in < AnLINEMAX ){
		int pnt = m_points[in];
		if( pnt < AnLPNTMAX ){
			m_pointX[in][pnt] = x;
			m_pointY[in][pnt] = y;
			m_pressure[in][pnt] = prs;
			m_points[in]++;
		}
	}
}

//--------------------------------------------------------------------------------------------
int main(int argc, char* argv[])
{
    OSStatus                    err;
    err = CreateNibReference( CFSTR("main"), &sNibRef );
    require_noerr( err, CantGetNibRef );
    
    // Create a new window. 
    HandleNewWindow();
    
    // Run the event loop
    RunApplicationEventLoop();

CantGetNibRef:
    return err;
}


//--------------------------------------------------------------------------------------------
DEFINE_ONE_SHOT_HANDLER_GETTER( WindowEventHandler )

//--------------------------------------------------------------------------------------------
static OSStatus
HandleNewWindow()
{
    OSStatus     err;
    WindowRef    window;
    static HIObjectClassRef sMyViewClassRef = NULL;
    EventRef	event;
    HIViewRef	theView;
    HIViewRef contentView;
    OptionBits options = kHIScrollViewOptionsVertScroll | kHIScrollViewOptionsHorizScroll | kHIScrollViewOptionsAllowGrow;
    const HIViewID viewID = { kDocumentViewSignature, 0 };
	
	
    static const EventTypeSpec    kWindowEvents[] =
    {
        { kEventClassCommand, kEventCommandProcess },

		{ kEventClassHIObject, kEventHIObjectConstruct },
		{ kEventClassHIObject, kEventHIObjectInitialize },
		{ kEventClassHIObject, kEventHIObjectDestruct },
        { kEventClassControl, kEventControlDraw },
		
		{ kEventClassMouse,		kEventMouseWheelMoved },
		{ kEventClassMouse,		kEventMouseDown },
		{ kEventClassMouse,		kEventMouseDragged },
		{ kEventClassMouse,		kEventMouseMoved },
		{ kEventClassMouse,		kEventMouseUp }							
    };
    
    // Create a window. "MainWindow" is the name of the window object. This name is set in 
    // InterfaceBuilder when the nib is created.
    err = CreateWindowFromNib( sNibRef, CFSTR("MainWindow"), &window );
    require_noerr( err, CantCreateWindow );

    // Install a command handler on the window. We don't use this handler yet, but nearly all
    // Carbon apps will need to handle commands, so this saves everyone a little typing.
    InstallWindowEventHandler( window, GetWindowEventHandlerUPP(),
                               GetEventTypeCount( kWindowEvents ), kWindowEvents,
                               window, NULL );
    // Position new windows in a staggered arrangement on the main screen
    RepositionWindow( window, NULL, kWindowCascadeOnMainScreen );
	GetWindowBounds(window,kWindowContentRgn,&m_bounds);			
	
	
	
	//Create HIView
    // Make scroll view
    err = HIScrollViewCreate(options, &theScrollView);
    require(err == noErr, CantCreateScrollView);
	
    // Bind it to the window's contentView
    HIRect bounds;
    HIViewFindByID(HIViewGetRoot(window), kHIViewWindowContentID, &contentView);
    HIViewAddSubview(contentView, theScrollView);
    HIViewGetBounds(contentView, &bounds);
    HIViewSetFrame(theScrollView, &bounds);
    HIViewSetVisible(theScrollView, true);
	
    err = HIObjectRegisterSubclass( kCSkDocViewClassID,
								   kHIViewClassID,			// base class ID
								   0,					// option bits
								   WindowEventHandler,		// construct proc
								   GetEventTypeCount( kWindowEvents ),
								   kWindowEvents,
								   NULL,				// construct data,
								   &sMyViewClassRef );

    // Make an initialization event
    err = CreateEvent( NULL, kEventClassHIObject, kEventHIObjectInitialize, GetCurrentEventTime(), 0, &event ); 
    require_noerr( err, CantCreateEvent );
	
    err = SetEventParameter(event, kDocumentBoundsParam, typeQDRectangle, sizeof(Rect), &m_bounds);
    require_noerr( err, CantSetParameter );

    err = HIObjectCreate(kCSkDocViewClassID, event, (HIObjectRef*)&theView);
    require_noerr(err, CantCreate);
    err = HIViewAddSubview(theScrollView, theView);
	
	SetControlID(theView, &viewID);
	HIViewSetVisible(theView, true);

    // The window was created hidden, so show it
    ShowWindow( window );
    
CantCreate:
CantSetParameter:
CantCreateEvent:
    ReleaseEvent( event );	
	
CantCreateScrollView:
CantCreateWindow:
    return err;
}

//--------------------------------------------------------------------------------------------
static OSStatus
WindowEventHandler( EventHandlerCallRef inCaller, EventRef inEvent, void* inUserData )
{
    OSStatus    err = eventNotHandledErr;
    unsigned long	ekind;
	SInt32 		whDelta = 0;
	EventRecord eve;
	WindowRef	wptr;
	Point   	where;
	UInt32  	modifiers;
	CGContextRef ctx;
	CanvasData	*data = (CanvasData	*)inUserData;
	
    ekind = GetEventKind(inEvent);
	
    switch ( GetEventClass( inEvent ) ){	
			
	case kEventClassHIObject:   // the boilerplate HIObject business
		switch ( ekind ){
		case kEventHIObjectConstruct:
			data = (CanvasData*)malloc(sizeof(CanvasData));	
			err = GetEventParameter( inEvent, kEventParamHIObjectInstance, typeHIObjectRef, NULL, sizeof(HIObjectRef*), NULL, &data->theView );
			require_noerr( err, ParameterMissing );
			SetEventParameter( inEvent, kEventParamHIObjectInstance, typeVoidPtr, sizeof(CanvasData), &data ); 
			break;
				
		case kEventHIObjectDestruct:
			free(inUserData);
			break;
		}
		break;	// kEventClassHIObject
			
			
	case kEventClassControl:    // draw, hit test and track
		switch ( ekind ){
		case kEventControlDraw:
			GetEventParameter(inEvent, kEventParamCGContextRef, typeCGContextRef, NULL, sizeof(CGContextRef), NULL, &ctx);
			//CallNextEventHandler(inCallRef, inEvent);	    // Erase old content
			DrawPage(ctx);
			err = noErr;
			break;
		}			
		break; //kEventClassControl
		
			
    case kEventClassMouse:
    	{ // Get Wacom Event	 
			UInt32 eventType;
			struct TabletPointRec tabletPnt; 
			struct TabletProximityRec tabletTyp;
			
			err = GetEventParameter(inEvent, 
							kEventParamTabletEventType, typeUInt32, NULL,
							sizeof(eventType), NULL, &eventType);
			if(err==noErr){ 
				switch( eventType ){
				case kEventTabletPoint:
					err = GetEventParameter(inEvent, 
							kEventParamTabletPointRec, typeTabletPointRec, NULL,
							sizeof(tabletPnt), NULL, &tabletPnt);
					if(err==noErr){
							m_press = tabletPnt.pressure;
					}
					break;
				case kEventTabletProximity:
					err = GetEventParameter(inEvent, 
							kEventParamTabletProximityRec, typeTabletProximityRec, NULL,
							sizeof(tabletTyp), NULL, &tabletTyp);
					if(err==noErr){
					}
					break;
				}
			}
		}//Wacom Event
		switch( ekind ){
		case kEventMouseWheelMoved:
			err = GetEventParameter(inEvent, kEventParamMouseWheelDelta, typeSInt32, NULL, sizeof(SInt32), NULL, &whDelta);
			m_scrollY += whDelta*4;
			/*no break*/
		case kEventMouseUp:
		case kEventMouseDown:
		case kEventMouseDragged:
		case kEventMouseMoved:
			if(ekind==kEventMouseDown){
				isDown = 1;
				m_points[m_index] = m_pointX[m_index][0] = m_pointY[m_index][0] = 0;
				m_index++;
			}
			if(ekind==kEventMouseUp){
				isDown = 0;
				m_press = 0;
			}
			ConvertEventRefToEventRecord(inEvent, &eve);

			if(FindWindow(eve.where, &wptr)!=inContent && ekind!=kEventMouseUp){
				break;
			}
			if(wptr != ActiveNonFloatingWindow()&& ekind!=kEventMouseUp){
				break;
			}
			GetEventParameter(inEvent, kEventParamMouseLocation, typeQDPoint, NULL, sizeof(Point), NULL, &where);
			GetEventParameter(inEvent, kEventParamKeyModifiers, typeUInt32, NULL, sizeof(UInt32), NULL, &modifiers);
			if(isDown){
				AddPolygonPoint(m_index-1, where.h, 
								where.v - m_scrollY -m_bounds.top, 15*m_press/0x10000);
			}
			if(whDelta || isDown){
				whDelta=0;
				HIViewSetNeedsDisplay(theScrollView, true);				
			}
			break;
		}		   
		break; //kEventClassMouse
   	default:
    	break;
    }
ParameterMissing:	
    return err;
}

```

### 説明
- 線のデータは、以下の配列に格納されています。AnLINEMAX本の線が入力できます。各ストロークの点数を格納した配列が m_points[AnLINEMAX]です。１本のストロークは最大AnLPNTMAX点までとしています。

```c
int     m_points[AnLINEMAX];                // Number of points
int     m_pointX[AnLINEMAX][AnLPNTMAX];     // x
int     m_pointY[AnLINEMAX][AnLPNTMAX];     // y
int     m_pressure[AnLINEMAX][AnLPNTMAX];   // pressure

```

- 太さが変わる線を描画しているのは、DrawPage() 、MakeBrushLine() 、asSetPoint()です。
- 各座標点の周囲に線の太さに対応した１２角形を描画し、それぞれを２本の線でつないで中を塗りつぶします。線の方向に応じて２本の線をどこにひくかを求めているのが、MakeBrushLine() です。asSetPoint()でパスを生成し、DrawPage()　の中の　CGContextDrawPath()　で描画しています。
- 筆圧の検出は、関数　WindowEventHandler() の中で、マウスイベント kEventClassMouse を受け取ったときに、GetEventParameter(inEvent, kEventParamTabletEventType, typeUInt32, NULL, sizeof(eventType), NULL, &eventType);
を実行して取得しています。    




---
# 関連記事

1. [無料ソフトでアニメを作ってみよう（9VAe きゅうべえ）](http://qiita.com/danjiro/items/253e5a33a38599098274)
3. [スクラッチ、ビスケットの次は 9VAe＝第３のプログラミング学習環境](http://qiita.com/danjiro/items/d856d393b3e138af123a)
6. [書き順アニメーションの作り方](http://qiita.com/danjiro/items/8dd0d7c7263f62189437)
7. [9VAeきゅうべえ：長いアニメを作る方法](http://qiita.com/danjiro/items/020cd885edb5eb3d9fb0)
1. [動くLINEスタンプのAPNG作成：無料ソフト9VAeきゅうべえ](http://qiita.com/danjiro/items/e4d9eb2b22f90ebd1e91)
1. [9VAeきゅうべえ」で絵を描かずに作れるGIFアニメ](http://qiita.com/danjiro/items/7fa584a4336cda9c0a05)
2. [9VAeきゅうべえで作成したSVGアニメーション](http://qiita.com/danjiro/items/1b4f6b5a520ade762116)
1.  [アニメソフト 9VAe をカスタマイズする方法](http://qiita.com/danjiro/items/c5dc07671ffdbbb8a9a9)
3. [9VAeをキッズプラザ大阪向けに改造する](http://qiita.com/danjiro/items/bbb10e07a9775a938200)
4. [9VAe / 9svg データフォーマット解説](http://qiita.com/danjiro/items/0d4ac334a7a97ddb4fe2)
5. [フリーソフト9VAeきゅうべえで簡単デジタルサイネージ](http://qiita.com/danjiro/items/e3b7ac1ef24826f9ff3d)