Edited at

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

More than 1 year has passed since last update.

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

Ver.0.5.0


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


  • Mac / Linuxで、Wacomペンタブレットの筆圧を検知して線をひく適当なサンプルプログラムが見つからず、いろいろ試行錯誤を繰り返したので、結果をシェアします。

  • このアルゴリズムは以下のフリーソフト9VAeきゅうべえに搭載しています。

ダウンロードはリンク先の

をクリック。
ベクターからダウンロード

9va-win (Windows版)
9va-win (ベクター)

9va-mac (Macintosh版)
9va-mac (ベクター)

9va-pi (ラズベリーパイ版)

9va-pi (Ubuntu/ Linux X86 32bit版)

くわしいインストール方法はこちら

キッズプラザ版(Windows 子供用)はこちら


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


  • ワコムペンタブレットを使って、ウィンドウの中に毛筆のような線をひくことができます。

  • マウスホイールや、2本指スクロール(Macintosh)を使って、画面を上下にスクロールさせれば、無限につづいたロールに書くことができます。


Linux版 プログラム


  • ビルドには Gtk+3 開発用ヘッダ, 実行には X server Wacom input ドライバーが必要です。取得例

    $ sudo apt-get install libgtk-3-dev
    $ sudo apt-get install xserver-xorg-input-wacom

//  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]です。1本のストロークは最大AnLPNTMAX点までとしています。

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()です。

  • 各座標点の周囲に線の太さに対応した12角形を描画し、それぞれを2本の線でつないで中を塗りつぶします。線の方向に応じて2本の線をどこにひくかを求めているのが、MakeBrushLine() です。asSetPoint()でパスを生成し、DrawPage() の中の CGContextDrawPath() で描画しています。

  • 筆圧の検出は、関数cb_motion_notify_event() の中で、gdk_event_get_axis ((GdkEvent *)event, GDK_AXIS_PRESSURE, &pressure) を実行して取得しています。



Macintosh版


  • 簡略化のため、ウィンドウの位置やサイズの変更の処理は省いています。

  • 毎回全データを書きなおしているため、線の数が増えると応答が悪くなります。

  • 高速化するには、上のLinux版のようにオフスクリーンにいったん描画してから、必要な領域だけ転送すればよいでしょう。


//
// 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]です。1本のストロークは最大AnLPNTMAX点までとしています。

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()です。

  • 各座標点の周囲に線の太さに対応した12角形を描画し、それぞれを2本の線でつないで中を塗りつぶします。線の方向に応じて2本の線をどこにひくかを求めているのが、MakeBrushLine() です。asSetPoint()でパスを生成し、DrawPage() の中の CGContextDrawPath() で描画しています。

  • 筆圧の検出は、関数 WindowEventHandler() の中で、マウスイベント kEventClassMouse を受け取ったときに、GetEventParameter(inEvent, kEventParamTabletEventType, typeUInt32, NULL, sizeof(eventType), NULL, &eventType);
    を実行して取得しています。




関連記事


  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きゅうべえで簡単デジタルサイネージ