2015年4月
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30    
無料ブログはココログ

« 2013年11月 | トップページ | 2014年3月 »

2013年12月の6件の記事

2013年12月15日 (日)

手書き検索シールと、とりあえずな汎用の(?) stroke 入力 GUI

先週、 「手書きコピペ」シール を作りましたが、stroke 入力を使えると楽しいです。コピー対象領域を 8 の字型みたいにくり抜けるのは、矩形領域指定ではできない芸当です。

また、シールを起動してから GUI で動作を選択できるのも、ぐっと幅がひろがります。これまで、シールをタップすると問答無用でコードが走り出し、結果を出力して終了というシールしか書けなかったので、処理ごとに別々のシールを書くしかなかったのですが、シールを立ち上げてから動作を選択できれば、似たような動作のシールを一つにまとめることができます。stroke 関係も、これまではシールを立ち上げてから、ユーザーからの入力を受け取るチャンスがなかったため、「最後に書いた storke の範囲を消す」とか「最後に書いた stroke に沿って絵を動かす」とかするしかありませんでしたが、シール起動後に stroke を受け取ることができれば、ずっと自然なシールアプリを作成できるわけです。

しかし、enchant.js がある分かなりマシなはずなのですが、やはり GUI を書くのは面倒です。先週のコピペシールでも、結局一番行数をかけているのは GUI 部分です。ここに可能性があると思うのですが、敷居が高いと手を出す意欲が薄れます。

こう考えて、先週作った GUI ライブラリ部分を、少しでも汎用性が上がるように手を加えてみました。stroke 周り、および単純な動作選択 GUI には使えると思います。

このライブラリをそのまま使って下さるもよし、とりあえずのアイディア確認にプロトタイプとして使い、本番ではオリジナルの GUI を作るもよし、或いは改造して流用してもらうもよし、とりあえず使えるものがあるのは大事だと思い、ライブラリの使用法を解説したいと思います。

また、大きく設計を変えるようなものはともかく、多少なら機能追加もできると思います。何か要望があれば、ダメ元でご意見ください。

以下に使用例を挙げますので、使いたければ、シールをダウンロードして解凍し、lib/getstorke.js (と lib/accessjson.js) を、自分のシールの lib にコピーし、importJS() の引数に、"lib/getstroke.js", "lib/accessjson.js" を追加して使ってみてください。

使い方

getstroke.js を import したうえで、

GUI_GetStroke.base( buttons, caption, options,
    function( selected, minX, minY, maxX, maxY, strokes ){
        // 入力された strokes と、選択されたボタンに応じた処理
}

という形で使用します。options = {} にいろいろなプロパティを設定することで動作を修飾します。

buttons, caption は、以下のような形式で指定します。

var buttons = [
    ['ボタン1', true],
    ['ボタン2', false],
    ['Retry',   true],
    ['ボタン3', true]
];

var caption = "動作選択を促す; 説明文";

buttons の true/false で、ボタンの有効/無効を指定します。false が指定されていると、ボタンはグレーアウトされて、tap しても反応しません。ボタンの名前が 'Retry' になっているボタンを tap すると、callback 関数に処理を移すことなく、ペンの入力をキャンセルして stroke を消去します。

caption に指定した文字列は、動作選択ボタン UI の横幅に合わせて自動的に改行されますが、明示的に改行したい場合は ';' を挟むと、その位置で強制改行されます。

callback 関数の形式は以下のようになります。

function( selected, minX, minY, maxX, maxY, strokes ){}

callback 関数に渡される引数は、

selected :

  • tap されたボタンの番号、buttons に設定した配列の添え字が返ります。上記の例であれば、'ボタン1' が tap されれば 0 が、'ボタン3' が tap されれば 3 が返ります。'Retry' ボタンが tap されても callback されないので、2 を受け取ることはありません。また、ペンによる stroke 入力が全くない状態でボタンが tap された場合は、入力無効を示すために、 -1 が返ります。この場合は strokes に渡されるデータを処理せずに、キャンセル処理など適切な処理をすべきです。

strokes :

  • singleStroke モードでは stroke 1 本分のデータ {width:xx, type:"pen", color:xx, data:[...]} が返ります。multipleStrokes モードでは、stroke の配列[{width:, type:, color:, data:},{},{}...] が返ります。

minX, minY, maxX, maxY :

  • stroke 或いは strokes に含まれる、全ての点を含む矩形領域の左上、右下座標を返します。画面をくり抜いたり、stroke 全体を囲む境界線を描いたりするのに使えますし、コピペなどで stroke を移動する場合は、(minX,minY) を基準とした相対座標に加工する際に必要になります。

options に指定できる内容

options に何も設定せずに呼び出すと、デフォルトとして全画面の singleStroke モードで動作します。コピペなどの領域指定に便利なモードです。

指定できるプロパティは

bgMinX, bgMinY, bgMaxX, bgMaxY :

  • ペン stroke 入力可能領域の左上・右下座標を指定する。4 つ全てを指定しないと無効。デフォルトでは画面全体を入力可能領域とする。

bgColor :

  • ペン stroke 入力可能領域の背景色を指定する。canvas.fillStyle に渡せる値を指定する。デフォルトでは透明で、ページの stroke がそのまま見える。

bgRimColor :

  • ペン stroke 入力可能領域の縁取りの色を指定する。デフォルトでは黒。bgColor が指定されていないと無効。

buttonsCenterX, buttonsCenterY :

  • 動作選択ボタン UI の画面上の初期位置を、その中心座標で指定する。デフォルトは画面中心。

penColorR, penColorG, penColorB :

  • 画面にベン入力される描線の色、および、その結果作成される stroke(s) data の color プロパティを設定する情報。描線色は canvas.strokeStyle = "rgb(penColorR, penColorG, penColorB)" stroke(s) data は color: MOON.rgba2int(penColorR, penColorG, penColorB, 255) に設定される。デフォルトは 255, 0, 0.

penWidth :

  • 画面にベン入力される描線の太さ。デフォルトでは、2 だが、MOONPhase ver 2.8.0 現在、画面描画に反映されない。

multipleStrokes :

  • singleStroke モードと multipleStrokes モードを切り替える。true で multiple, false で single. デフォルトは false. singleStroke モードは範囲指定のための囲み線を想定、データを多少間引きつつ、stroke 一本分のデータを返す。multipleStrokes モードは、文字列あるいは図画の入力を想定。筆圧は反映しないが複数 storke の data を stroke の配列として返す。

attachBgToButtons :

  • デフォルトでは false. true を指定すると、ペン stroke 入力可能領域を、bgMin/Max/X/Y で指定したサイズで、動作選択ボタン UI の背側に張り付ける。動作選択ボタン UI を drag すると、ペン stroke 入力可能領域も drag できる。

頻用する設定のショートカットとして、

GUI_GetStroke.singleStroke( buttons, caption, function(){} )

    GUI_GetStroke.base( buttons, caption, {}, function(){} )

および、

GUI_GetStroke.multipleStrokes( buttons, caption, function(){} )

    var options = { multipleStrokes: true, bgColor: "white" };
    GUI_GetStroke.base( buttons, caption, options, function(){} )

も使えます。

使用例

コピペシール (hack.js の抜粋) [ダウンロード] 

前回のコピペシールを今回のライブラリを使う形に書き換えると、以下のようになります。なお、領域内外判定ルーチンは canvasutil.js を使って作成しています。

    var sticker = Sticker.create();

    sticker.onattach = function(event) {
        var myStickerID = InfoUtil.getStickerID();
        var nullStrokeJSON = MOON.getPaperJSON( myStickerID );
        nullStrokeJSON.strokes = [];
        MOON.setPaperJSON( myStickerID, nullStrokeJSON );

        var buttons = [
            [ 'Cut', true ],
            [ 'Copy', true ],
            [ 'Retry', true ],
            [ 'Cancel', true ]
        ];
        var caption = 'Draw a closed stroke,  then ;select an action.'

        GUI_GetStroke.singleStroke( buttons, caption,
          function( selected, minX, minY, maxX, maxY, stroke ){
            var action = ( selected < 0 ) ? 'Cancel' : buttons[selected][0];
            var isInsideStroke = CanvasUtil.getFuncIsInsideOfStrokeApplied( stroke );
            switch ( action ) {
            case 'Cut':
                CopyPaste.cut( minX, minY, maxX, maxY, isInsideStroke );
                clearSaveInfo();
                saveDataActivity();
                saveCutBasePosition( minX, minY );
                break;
            case 'Copy':
                CopyPaste.copy( minX, minY, maxX, maxY, isInsideStroke );
                clearSaveInfo();
                saveDataActivity();
                break;
            case 'Cancel':
                MOON.peel();
                break;
            }
            MOON.finish();
        });
    };

画面に字を書いて、文字認識させてみる「だけ」のシール (hack.js) [ダウンロード] 

「自分の手書き文字が汚くて認識されないかもしれない」のを確認するシールです。

GUI_GetStroke.multipleStrokes() を使用しているので、背景を白で塗りつぶすため、元のページの描線は見えない状態で、字を書くことができます。

/*
*  recogDrawTest
*   by @H_Kuruno - http://kuruno.cocolog-nifty.com/blog/
*                                   last modified on 2013/12/15
*/
importJS(["lib/MOON.js", "lib/enchant.js", "lib/stylus.enchant.js", "lib/accessjson.js", "lib/getstroke.js"], function() {
    enchant();

    var sticker = Sticker.create();

    sticker.onattach = function(event) {
        MOON.finish();
    };

    sticker.ontap = function(event) {
        var buttons = [
            [ '認識', true ],
            [ 'Retry', true ],
            [ 'Cancel', true ]
        ];
        var caption = 'Draw a closed stroke,  then select an action.';

        GUI_GetStroke.multipleStrokes( buttons, caption,
          function( selected, minX, minY, maxX, maxY, strokes ){
            var action = ( selected < 0 ) ? 'Cancel' : buttons[selected][0];
            switch ( action ) {
            case '認識':
                MOON.alert( '認識結果 : ' + MOON.recognizeStrokes( strokes ) );
                break;
            case 'Cancel':
                MOON.finish();
                break;
            }
        });
    };

    sticker.ondetach = function(event) {
        MOON.finish();
    };

    sticker.register();
});

手書き検索シール (hack.js) [ダウンロード]

Issue Tracker の #98 を、シールで無理やり解決するものです。シールを張り付けると、黄色の背景色で、ペン stroke 入力可能領域が設定されます。options で attachBgToButtons が true に設定されているので、ボタンごと drag することができますから、適当な位置に drag して、もとのページの情報を参照しながら入力することができます。文字を書いたら [認識テスト]ボタンで、正しく認識されるか確認します。誤認識されるようなら [Retry] ボタンでもう一度手書き入力をやり直します。認識結果が良ければ、[検索] ボタンを tap すると、シールを剥がしてから、enchant コマンドで検索したときと同じような処理を実行します。

十分使えるんですが、enchantMOON ここまで来ると動作の遅いのが重ね重ね残念ですね。

/*
*  searchDrawStr
*   by @H_Kuruno - http://kuruno.cocolog-nifty.com/blog/
*                                   last modified on 2013/12/15
*/
importJS(["lib/MOON.js", "lib/enchant.js", "lib/stylus.enchant.js", "lib/accessjson.js", "lib/getstroke.js"], function() {
    enchant();

    var THROW_PEEL_SIGNAL = 'THROW_PEEL_SIGNAL';

    function peelThenThrowToDetach( signal ){
        var sig = signal || true;
        var myStickerID = InfoUtil.getStickerID();
        StorageUtil.set( THROW_PEEL_SIGNAL + myStickerID, sig );
        MOON.peel();
    };

    function catchFromPeel( callback ){
        var myStickerID = InfoUtil.getStickerID();
        var signal = StorageUtil.get( THROW_PEEL_SIGNAL + myStickerID, false );
        if ( signal ) {
            StorageUtil.set( THROW_PEEL_SIGNAL + myStickerID, null );
            callback( signal );
        }
    };

    var sticker = Sticker.create();

    sticker.onattach = function(event) {
        var myStickerID = InfoUtil.getStickerID();
        var nullStrokeJSON = MOON.getPaperJSON( myStickerID );
        nullStrokeJSON.strokes = [];
        MOON.setPaperJSON( myStickerID, nullStrokeJSON );

        var buttons = [
            [ '認識;テスト', true ],
            [ '検索', true ],
            [ 'Retry', true ],
            [ 'Cancel', true ]
        ];
        var caption = 'Draw a closed stroke,  then ;select an action.';
        var options = {
             bgMinX: 0
            ,bgMinY: 0
            ,bgMaxX: 330
            ,bgMaxY: 300
            ,bgColor: "rgb(249,255,211)"
            ,attachBgToButtons: true
            ,buttonsCenterX: 768/2
            ,buttonsCenterY: 1024 *3/5
            ,multipleStrokes: true
        };

        GUI_GetStroke.base( buttons, caption, options,
          function( selected, minX, minY, maxX, maxY, strokes ){
            var action = ( selected < 0 ) ? 'Cancel' : buttons[selected][0];
            switch ( action ) {
            case '認識;テスト':
                MOON.alert( '認識結果 : ' + MOON.recognizeStrokes( strokes ) );
                break;
            case '検索':
                peelThenThrowToDetach( MOON.recognizeStrokes( strokes ) );
                break;
            case 'Cancel':
                MOON.peel();
                break;
            }
        });
    };

    sticker.ontap = function(event) {
        MOON.finish();
    };

    sticker.ondetach = function(event) {
        catchFromPeel( function( signal ){
            MOON.searchPage( signal );
        });
        MOON.finish();
    };

    sticker.register();
});

こういうのは、本格的にやるのなら

こういうのは、本格的にやるのなら、GitHub でも使って、大勢で開発するのがいいんでしょうが、私がまだ git の勉強中なのと、GitHub にしちゃうと、祭りに参加できる人の敷居が上がりそうなので、とりあえずシールをダウンロードして zip を解凍して使いまわしてもらう方向で。

できれば、こんな感じで、他にも使いまわせるライブラリを共同開発していけるといいんですけどね。

2013年12月 8日 (日)

手書き版コピペシールのソースコード

MOON のシールを作るようになって、改めて気になったのですが、いわゆる script 系言語とか、LL のコードでもソースコードという言葉を使うのでしょうか。compiler や asembler ならソースで問題ないと思うんですが。BASIC の頃は「プログラムリスト」で、ソースとかいう言葉はなかったような気もしますし。正しい用語の使い方をご存じな方は是非教えてください。

さて、先日作った「手書きコピペ」シールのコードを供覧します。これをベースに、似たようなシールをいくつか作ろうと思っているのですが、拡張しようとすると関数名や変数名などに少し気になるところがあったので、少しだけ修正してあります。同時に複数枚のシールを貼った場合の配慮も不足していたので、これも対応しています。

Download : 修正版「手書きコピペ」シール / [使い方]

hack.js

onattach では、GUI を呼び出して、callback 関数内で stroke と動作指定コードを取得、ページ側の stroke とシール側の stroke を切り貼りするルーチンを呼び出します。この際、与えられた stroke 内部と外部の座標を判定する関数を渡しています。単純に stroke を渡して、切り貼りルーチンの中で CanvasUtil.isPointInsideStrokeData() を呼び出すように書けばいいのですが、このように回りくどくしておくことで、切り貼りルーチン側を全く変えずに、領域判定ルーチンだけを差し替えることができますので、ファイルのメンテナンスを考えてこうしてますが、正直何度かタイプミスしてしまい、面倒でした。

ontap では、動作指定コードを返す GUI を呼び出して、以下同様です。その他は、localStorage に内部状態を読み書きしているだけです。

/*
*  squareCopyPaste
*   by @H_Kuruno - http://kuruno.cocolog-nifty.com/blog/
*                                   last modified on 2013/12/08
*/
importJS(["lib/MOON.js", "lib/enchant.js", "lib/stylus.enchant.js", "lib/surface.enchant.js","lib/accessjson.js", "lib/canvasutil.js","lib/cutstrokes.js", "lib/getstroke.js"], function() {
    enchant();

    var BASE_POSITION_TAG = 'CutCopyPasteBasePosition';
    var DATA_ACTIVITY = 'CutCopyPaste_DataActive';

    function saveCutBasePosition( baseX, baseY ) {
        var myStickerID = InfoUtil.getStickerID();
        var positionTag = StorageUtil.get( BASE_POSITION_TAG, {} );
        InfoUtil.setObjToJSON( positionTag, myStickerID, {x:baseX, y:baseY} );
        StorageUtil.set( BASE_POSITION_TAG, positionTag );
    }

    function getCutBasePosition() {
        var myStickerID = InfoUtil.getStickerID();
        var positionTag = StorageUtil.get( BASE_POSITION_TAG, {} );
        return InfoUtil.getObjFromJSON( positionTag, myStickerID, null );
    }

    function saveDataActivity() {
        var myStickerID = InfoUtil.getStickerID();
        var dataTag = StorageUtil.get( DATA_ACTIVITY, {} );
        InfoUtil.setObjToJSON( dataTag, myStickerID, true );
        StorageUtil.set( DATA_ACTIVITY, dataTag );
    }

    function getDataActivity() {
        var myStickerID = InfoUtil.getStickerID();
        var dataTag = StorageUtil.get( DATA_ACTIVITY, {} );
        return InfoUtil.getObjFromJSON( dataTag, myStickerID, false );
    }

    function clearSaveInfo() {
        var myStickerID = InfoUtil.getStickerID();
        var positionTag = StorageUtil.get( BASE_POSITION_TAG, {} );
        var dataTag = StorageUtil.get( DATA_ACTIVITY, {} );
        InfoUtil.setObjToJSON( positionTag, myStickerID, null );
        InfoUtil.setObjToJSON( dataTag, myStickerID, null );
        StorageUtil.set( BASE_POSITION_TAG, positionTag );
        StorageUtil.set( DATA_ACTIVITY, dataTag );
    }

    var sticker = Sticker.create();

    sticker.onattach = function(event) {
        var myStickerID = InfoUtil.getStickerID();
        var nullStrokeJSON = MOON.getPaperJSON( myStickerID );
        nullStrokeJSON.strokes = [];
        MOON.setPaperJSON( myStickerID, nullStrokeJSON );

        var buttons = [
            [ 'Cut', true ],
            [ 'Copy', true ],
            [ 'Retry', true ],
            [ 'Cancel', true ]
        ];
        var caption = 'Draw a closed stroke,  then ;select an action.'

        GUI_GetStroke( buttons, caption,
          function( selected, minX, minY, maxX, maxY, stroke ){
            switch ( buttons[selected][0] ) {
            case 'Cut':
                CopyPaste.cut( minX, minY, maxX, maxY, function( x, y ){
                    return CanvasUtil.isPointInsideStrokeData( x, y, stroke.data );
                });
                clearSaveInfo();
                saveDataActivity();
                saveCutBasePosition( minX, minY );
                break;
            case 'Copy':
                CopyPaste.copy( minX, minY, maxX, maxY, function( x, y ){
                    return CanvasUtil.isPointInsideStrokeData( x, y, stroke.data );
                });
                clearSaveInfo();
                saveDataActivity();
                break;
            case 'Cancel':
                MOON.peel();
                break;
            }
            MOON.finish();
        });
    };

    sticker.ontap = function(event) {
        var myStickerID = InfoUtil.getStickerID();
        var stickerJSON = MOON.getPaperJSON( myStickerID );
        var centerX = stickerJSON.x + stickerJSON.width/2;
        var centerY = stickerJSON.y + stickerJSON.height/2;
        var cutBasePosition = getCutBasePosition();
        var isCutResettable = ( cutBasePosition != null );
        var isDataActive = getDataActivity();

        var buttons = [
            [ 'Paste', isDataActive ],
            [ 'reset;Cut', isCutResettable ],
            [ 'Cancel', true ]
        ];
        var caption = 'Select an action.'

        GUI_SelectAction( buttons, caption, centerX, centerY,
          function( selected ){
            switch ( buttons[selected][0] ) {
            case 'Paste':
                CopyPaste.paste();
                MOON.finish();
                break;
            case 'reset;Cut':
                CopyPaste.resetCut( cutBasePosition.x, cutBasePosition.y );
                clearSaveInfo();
                MOON.peel();
                break;
            case 'Cancel':
                MOON.finish();
                break;
            }
        });
    };

    sticker.ondetach = function(event) {
        clearSaveInfo();
        MOON.finish();
    };

    sticker.register();
});

.

accessjson.js

2.8.0 対応版、12/5 の記事で解説済みです。

/*
*  InfoUtil, StorageUtil
*   by @H_Kuruno - http://kuruno.cocolog-nifty.com/blog/
*       MOONPhase v 2.8.0 での window.location に対応
*                                   last modified on 2013/12/05
*/
(function(global) {
    function relDirectories( i ){
        var relURL = location.href;
        relURL = relURL.slice( relURL.search( /\/data\//i ) +1 );
        return relURL.split( "/" )[i];
    }

    var InfoUtil = {
         PROPER_TAG: 'stickerProper'

        ,getStickerID: function(){ return relDirectories( 3 ); }
        ,getPageID: function(){ return relDirectories( 2 ); }
        ,getNoteID: function() { return relDirectories( 1 ); }

        // JSON から、tag で指定した値を読み込む。
        // tag が存在しなかった場合の戻り値を指定できる。
        //
        ,getObjFromJSON: function( json, tag, nullDefault ) {
            var obj = json[ tag ];
            if ( nullDefault == null ) {
                return obj;
            } else {
                return ( obj == null ) ? nullDefault : obj;
            }
        }
        // JSON に、値 obj を 名 tag で登録する。
        // 但し、値が null, {}, [] の場合は削除する。
        //
        ,setObjToJSON: function( json, tag, obj ) {
            if ( (obj == null)
              || ( (typeof obj).match( /object/i ) && Object.keys( obj ).length <= 0 )
            ) {
                if ( json[ tag ] ) delete json[ tag ];
            } else {
                    json[ tag ] = obj;
            }
            return json;
        }
    };


    var StorageUtil = {
        // localStorage から、tag で指定した値を読み込む。
        // tag が存在しなかった場合の戻り値を指定できる。
        //
         get: function( tag, nullDefault ) {
            var obj = localStorage[ tag ];
            if ( nullDefault == null ) {
                return obj;
            } else {
                return ( obj == null ) ? nullDefault : obj;
            }
        }
        // localStorage に、値 obj を 名 tag で登録する。
        // 但し、値が null, {}, [] の場合は削除する。
        //
        ,set: function( tag, obj ) {
            if ( (obj == null)
              || ( (typeof obj).match( /object/i ) && Object.keys( obj ).length <= 0 )
            ) {
                if ( localStorage[ tag ] ) localStorage.removeItem( tag );
            } else {
                localStorage[ tag ] = obj;
            }
        }
    };

    global.InfoUtil = InfoUtil;
    global.StorageUtil = StorageUtil;
})(this);

.

canvasutil.js

canvas.isPointInPath() がサポートされていないので、その変わりに使うライブラリです。stroke.data を渡さないといけないので、次のシールを作る時には、stroke を直接渡す interface も追加するつもりです。

(function(global){
    var Util = {};
/*
*  CanvasUtil.isPointInsideStrokes( x, y, stroke )
*       by @H_Kuruno - http://kuruno.cocolog-nifty.com/blog/
*                                       last modified on 2013/12/08
*
*  このサイトのコードを、ほぼそのまま javascript で書き直しただけです。
*   http://www.hiramine.com/programming/graphics/2d_ispointinpolygon.html
*  ----------------------------------------------------------------------
*
*       座標 ( x, y ) が stroke[] で示される閉曲線領域内に含まれるかどうか
*       判定する関数。含まれれば true, 含まれなければ false が返る。
*
*   stroke[] : 一筆書き分の閉曲線( C 字型に最後が一部閉じていなくても
*       始点と終点が一致する形で閉じたものとして扱う)
*/
    Util.isPointInsideStrokeData = function( x, y, stroke ) {
        var crossingCount = 0;
        var leFromX, leFromY, leToX, leToY;
        var prevX, prevY;

        var sLen = stroke.length;
        if ( stroke[0] == stroke[sLen-3] && stroke[1] == stroke[sLen-2] ) sLen--;

        prevX = stroke[sLen-3];    prevY = stroke[sLen-2];
        leFromX = ( x <= prevX );
        leFromY = ( y <= prevY );
        for ( var i = 0; i < sLen; i += 3 ) {
            leToX = ( x <= stroke[i] );
            leToY = ( y <= stroke[i+1] );
            if ( leFromY != leToY ) {
            // 線分の始点、終点、いずれかが (x,y) より上で、他方が下
                if ( leFromX && leToX ) { // 線分の始点、終点とも (x,y) より右
                    // 上から下にレイを横切るときには、交差回数を1引く、下から上は1足す。
                    crossingCount += ( leFromY ? -1 : 1);
                } else if ( leFromX != leToX ) {
                // 線分の始点、終点、いずれかが (x,y) より右で、他方が左
                    var crossPointX = prevX + (stroke[i] - prevX) * (y - prevY) / (stroke[i+1] - prevY);
                    if ( x <= crossPointX ) {
                        // 上から下にレイを横切るときには、交差回数を1引く、下から上は1足す。
                        crossingCount += ( leFromY ? -1 : 1);
                    }
                }
            }
            prevX = stroke[i]; prevY = stroke[i+1]; // 今回の終点を、次回の始点に
            leFromX = leToX;    leFromY = leToY;
        }
        return ( crossingCount != 0 ); // 0 なら stroke[] 領域外部
    };

    global.CanvasUtil = Util;
})(this);

.

cutstrokes.js

与えられた領域判定ルーチンを使って、ページの strokes を、領域内 stroke と領域外 stroke に別け、これをシールに貼ったり、ページに書き戻したりするルーチン。および、シールの strokes を指定した座標を基準としてページに書き戻すルーチン、などです。コピペのメイン処理部分ですね。

/*
*  CopyPaste
*   by @H_Kuruno - http://kuruno.cocolog-nifty.com/blog/
*                                   last modified on 2013/11/30
*/
(function(global) {
    var CopyPaste = {};

    var rimStrokes = [
      {width:4,color:0,type:"pen",data:[]}
    ];

    function adjustRimStrokes( width, height ) {
        var COLOR = MOON.rgba2int(166,252,222,255);
        var P = 1;  // 筆圧値
        rimStrokes[0].color = COLOR;
        rimStrokes[0].data = [0,0,P, width - 1,0,P, width - 1,height - 1,P, 0,height - 1,P, 0,0,P];
        return rimStrokes;
    }


    function showParticesOnAreaBorder( minX, minY, maxX, maxY ) {
        var pList = [];
        var pLife = 60;
        var deltaX = (maxX - minX)/10;
        var deltaY = (maxY - minY)/10;
        for ( var x = minX; x < maxX; x += deltaX ) {
            pList.push( x, minY, (Math.random()-0.5)/10.0, (Math.random()-0.5)/10.0, pLife );
            pList.push( x, maxY, (Math.random()-0.5)/10.0, (Math.random()-0.5)/10.0, pLife );
        }
        for ( var y = minY; y < maxY; y += deltaY ) {
            pList.push( minX, y, (Math.random()-0.5)/10.0, (Math.random()-0.5)/10.0, pLife );
            pList.push( maxX, y, (Math.random()-0.5)/10.0, (Math.random()-0.5)/10.0, pLife );
        }
        MOON.showParticles( pList );
    }

    function pickToSticker( minX, minY, maxX, maxY, strokes ) {
        var width = maxX - minX + 1;
        var height = maxY - minY + 1;
        var myStickerID = InfoUtil.getStickerID();
        var stickerJSON = MOON.getPaperJSON( myStickerID );
        /*
        *  MOONPhase v 2.7.0 現在、シールの表示座標を変更しても
        *  画面表示に反映されず、システム内部情報と画面表示が
        *  乖離するので、修正されるまでは x, y の変更処理は skip.
        *
        stickerJSON.x = minX;
        stickerJSON.y = miyY;
        */
        stickerJSON.width = width;
        stickerJSON.height = height;
        stickerJSON.scale = 1.0;
        stickerJSON.strokes = adjustRimStrokes( width, height ).concat( strokes );
        stickerJSON.clip.data = [
          0, 0, 0.01, width -1, 0, 0.01,
          width -1, height -1, 0.01, 0, height -1, 0.01, 0, 0, 0.01
        ];
        MOON.setPaperJSON( myStickerID, stickerJSON );
    }

    function cut_sub( baseX, baseY, isInside ) {
        var cutStrokes = [], restStrokes = [];
        var page = MOON.getCurrentPage();
        var paperJSON = MOON.getPaperJSON( page.backing );
        var strokes = paperJSON.strokes;

        for ( var i = 0, sLen = strokes.length; i < sLen; i++ ) {
            var d = strokes[i].data;
            var dLen = d.length;
            if ( isInside( d[0], d[1] ) && isInside( d[dLen-3], d[dLen-2] ) ) {
                for ( var j = 0; j < dLen; j += 3 ) {
                    d[j] -= baseX;
                    d[j + 1] -= baseY;
                }
                strokes[i].data = d;
                cutStrokes.push( strokes[i] );
            } else {
                restStrokes.push( strokes[i] );
            }
        }
        return { cut: cutStrokes, rest: restStrokes };
    }

    CopyPaste.cut = function( minX, minY, maxX, maxY, isInside ) {
        var cookedStrokes = cut_sub( minX, minY, isInside );

        showParticesOnAreaBorder( minX, minY, maxX, maxY );
        pickToSticker( minX, minY, maxX, maxY, cookedStrokes.cut );

        var page = MOON.getCurrentPage();
        var paperJSON = MOON.getPaperJSON( page.backing );
        paperJSON.strokes = cookedStrokes.rest;
        MOON.setPaperJSON( page.backing, paperJSON );
    };


    CopyPaste.copy = function( minX, minY, maxX, maxY, isInside ) {
        var cookedStrokes = cut_sub( minX, minY, isInside );

        showParticesOnAreaBorder( minX, minY, maxX, maxY );
        pickToSticker( minX, minY, maxX, maxY, cookedStrokes.cut );
    };

    function paste_sub( baseX, baseY ) {
        var myStickerID = InfoUtil.getStickerID();
        var stickerJSON = MOON.getPaperJSON( myStickerID );
        var maxX = baseX + stickerJSON.width - 1;
        var maxY = baseY + stickerJSON.height - 1;
        // 周囲を囲む stroke を捨てる
        var strokes = stickerJSON.strokes.slice( rimStrokes.length );

        for ( var i = 0, sLen = strokes.length; i < sLen; i++ ) {
            var d = strokes[ i ].data;
            for ( var j = 0, dLen = d.length; j < dLen; j += 3 ) {
                d[ j ] += baseX;
                d[ j + 1 ] += baseY;
            }
            strokes[ i ].data = d;
        }

        var page = MOON.getCurrentPage();
        var paperJSON = MOON.getPaperJSON( page.backing );
        showParticesOnAreaBorder( baseX, baseY, maxX, maxY );
        paperJSON.strokes = paperJSON.strokes.concat( strokes );
        MOON.setPaperJSON( page.backing, paperJSON );
    }

    CopyPaste.paste = function() {
        var myStickerID = InfoUtil.getStickerID();
        var stickerJSON = MOON.getPaperJSON( myStickerID );
        var stickerX = stickerJSON.x;
        var stickerY = stickerJSON.y;
        paste_sub( stickerX, stickerY );
    };


    CopyPaste.resetCut = function( orgX, orgY ) {
        paste_sub( orgX, orgY );
    };

    global.CopyPaste = CopyPaste;
})(this);

.

getstroke.js

行数としてはこれがメインになる、GUI 部分です。ボタン表示用の ButtonFace クラスと、ボタンを 3-4 個並べて caption も表示できる AskActionUI クラスを定義して、これを使って、stroke を取得する GUI と、ボタン選択だけ提供する GUI を作成しています。ButtonFace と AskActionUI を読まなければ、あとは比較的簡単に読めると思います。

blog にコードを張り付けていると、あとから自分で参照するときにも便利だったのですが、そろそろファイル数も行数も増えてきたので、全部直接貼りつけるのは今回を最後にしようと思います。今後は zip を展開して参照くださいということで。

enchant();
(function(global) {
/*
*  ButtonFace
*       by @H_Kuruno - http://kuruno.cocolog-nifty.com/blog/
*                                       last modified on 2013/12/02
*  ----------------------------------------------------------------------
*
*  new ButtonFace( x, y, width, height, text [, textRadian ] )
*
*  x,y : ButtonFace を addChild する親の座標系での button の左上座標
*  widht, height : Button のサイズ
*  text : ボタンに表示されるラベル文字列。文字列中に ';' が含まれていると
*         そこで改行される。但し、複数行表示になった場合に Button の高さ
*         方向内に文字列表示が収まることは保証されないので、文字列に応じて
*         必要な Button のサイズを明示的に指定する必要がある。
*  textRadian : optional : ラジアン単位で、ラベル文字列を回転して表示する。
*               この時も、文字列表示が横幅・高さ方向に収まることは保証されない。
*
*  buttonFace.changeColor( btnColor, fontColor )
*       ボタンの背景色と、ラベル文字列色を変更する。ボタンの反転表示などに用いる。
*
*  buttonFace.offColor()
*       ボタンの表示色を無効表示色に変更する。
*
*  buttonFace.touchingColor()
*       ボタンの表示色を touch 中を示す色に変更する。
*
*  buttonFace.resetColor()
*       ボタンの表示色を標準に戻す。
*/
    var CONST = {
         normalColor: { btn: "rgb(48,64,96)", font: "white" }
        ,offColor: { btn: "rgb(200,200,200)", font: "rgb(100,100,100)" }
        ,touchingColor: { btn: "rgb(196,250,253)", font: "black" }
        ,rimColor: "white"
        ,margin:    5
        ,rim: { box: 3, btn: 1 }
        ,padding: { x:5, y:5 }
        ,btnFont: 'bold 24px'
        ,btnFontHeight: 22
    };

    var ButtonFace = Class.create( Sprite, {
         initialize: function( x, y, width, height, text, textRadian ) {
            Sprite.call( this, width, height );  // 親クラスの初期化
            this.image = new Surface( width, height );
            this.text = text;   this.textRadian = textRadian;
            this.initialDraw();
            this.x = x;    this.y = y;
        }
        ,initialDraw: function() {
            var ctx = this.image.context;
            if ( this.textRadian ) ctx.rotate( Math.PI * this.textRadian );
            this.resetColor();
        }
        ,resetColor: function() {
            this.changeColor( CONST.normalColor.btn, CONST.normalColor.font );
        }
        ,offColor: function() {
            this.changeColor( CONST.offColor.btn, CONST.offColor.font );
        }
        ,touchingColor: function() {
            this.changeColor( CONST.touchingColor.btn, CONST.touchingColor.font );
        }
        ,changeColor: function( btnColor, fontColor ) {
            var ctx = this.image.context;

            if ( this.textRadian ) ctx.rotate( Math.PI * (-this.textRadian) );

            ctx.fillStyle = CONST.rimColor;
            ctx.fillRect( 0, 0, this.width, this.height );
            ctx.fillStyle = btnColor;
            ctx.fillRect( CONST.rim.btn, CONST.rim.btn, this.width - CONST.rim.btn * 2, this.height - CONST.rim.btn * 2 );

            ctx.fillStyle = fontColor;
            ctx.font = CONST.btnFont;
            ctx.textBaseline = 'middle';

            var maxWidth = this.width - ( CONST.rim.btn + CONST.padding.x ) * 2;
            var textLines = this.text.split( ';' );
            var lines = textLines.length;

            for ( var i = 0; i < lines; i++ ) {
                var text = textLines[ i ];
                var textWidth = ctx.measureText( text ).width;
                var orgX = this.width/2;
                var orgY = this.height/2;
                var deltaY = ( i - (lines -1)/2 ) * CONST.btnFontHeight;

                if ( this.textRadian ) {
                    var s = Math.sin( Math.PI * (-this.textRadian) );
                    var c = Math.cos( Math.PI * (-this.textRadian) );

                    ctx.rotate( Math.PI * this.textRadian );
                    var xxx = c * orgX - s * orgY;
                    var yyy = s * orgX + c * orgY;
                    ctx.fillText( text, xxx - textWidth/2, yyy + deltaY );
                } else {
                    if ( textWidth > maxWidth ) {
                        ctx.fillText( text, orgX - maxWidth/2, orgY + deltaY, maxWidth );
                    } else {
                        ctx.fillText( text, orgX - textWidth/2, orgY + deltaY );
                    }
                }
            }
        }
    });


    global.ButtonFace = ButtonFace;
})(this);


(function(global) {
/*
*  AskActionUI
*       writen by @H_Kuruno - http://kuruno.cocolog-nifty.com/blog/
*
*  -----------------------------------------------------------------------
*  enchant.Group を継承したクラス
*
*  new AskActionUI( buttons, areaMinX, areaMinY, areaMaxX, areaMaxY, caption, callback )
*
*  buttons : 表示ボタンに関する配列。
*       [ [ 表示文字列0, activeFlag0 ], [ 表示文字列1, activeFlag1 ], ... ]
*       という 2 次元配列でボタン情報を指定する。ボタン数に制限はないが、
*       現実的には 3-4 個を想定しているので、0 個や多数個の指定は避けたい。
*
*  areaMinX/MinY/MaxX/MaxY : GUI を表示する area を指定。指定された area 外に
*       GUI を移動することは禁止しないが、少なくとも初期表示で、指定 area 内に
*       GUI ができるだけ収まるように初期表示する。enchantMOON 初代機では、画面
*       全体を指定しておけばよい。将来的に壁面全体 MOON ができた時や、初代機でも
*       画面下半分に初期表示したい、などという場合に、適当な値を指定する。
*
*  caption : 説明文を指定する。適当な長さで自動的に改行されるが、文字列中に
*       ';' を含めると、ここで強制改行される。したがって、説明文として ';' を
*       表示することはできない。
*
*  callback : function( selected )
*       buttons[] で指定した動作選択ボタンのいずれかがタップされたら、何番目の
*       ボタンがタップされたかを示す整数値を与えて callback する。
*/
    var CONST = {
         btnWidth: 70
        ,btnHeight: 70
        ,spacerWidth: 10
        ,dragBtnWidth: 50
        ,dragBtnHeight: 50
        ,margin: 5
        ,padding: 5
        ,rim: 2
        ,captionFont: '18px'
        ,captionFontHeight: 22
        ,captionColor: "black"
        ,bgColor: "white"
        ,rimColor: "black"
    }

    /*
    *  makeCaptionLines( caption, width, shortWidth, shortHeight )
    *   与えられた文字列を、width ピクセル分の横幅に収まる長さごとに
    *   分割する。
    *   但し、文字列中に ';' が含まれていると、そこで強制分割する。
    *   (したがって、';' を文字列として表示することはできない)
    *   CONST.captionFont で指定された字体で横幅をチェックする。
    *
    *   dragButton を回避して表示するので、上から shortHeight ピクセル
    *   分は、sortWidth ピクセルの横幅に収める。
    *
    *   返り値 : [ [shortWidth 幅の配列], [width 幅の配列] ]
    */
    function makeCaptionLines( caption, width, shortWidth, shortHeight ) {
        var w = width;
        var shortLines = Math.ceil( shortHeight/CONST.captionFontHeight );

        var shortCaptions = []; longCaptions = [];

        var surface = new Surface( width, CONST.captionFontHeight );
        var ctx = surface.context;
        ctx.font = CONST.captionFont;

        function cutPiece( str, width ){
            for ( i = 0, sLen = str.length; i < sLen; i++ ) {
                if ( ctx.measureText( str.slice(0,i) ).width >= width ) {
                    return { piece: str.slice(0,i-1), rest: str.slice(i-1) };
                } else if ( str.charAt(i) == ';' ) {
                    return { piece: str.slice(0,i), rest: str.slice(i+1) };
                }
            }
            return { piece: str, rest: null };
        }

        for( ; shortLines > 0; shortLines -- ) {
            var result = cutPiece( caption, shortWidth );
            shortCaptions.push( result.piece );
            caption = result.rest;
            if ( !caption ) break;
        }
        if ( caption ) {
            while ( caption ) {
                var result = cutPiece( caption, width );
                longCaptions.push( result.piece );
                caption = result.rest;
            }
        }
        return {short: shortCaptions, long:longCaptions};
    }

    var AskActionUI = Class.create( Group, {
         initialize: function( buttons, areaMinX, areaMinY, areaMaxX, areaMaxY, caption, callback ) {
            Group.call( this );  // 親クラスの初期化
            this._callback = callback;

            var contentWidth = (CONST.btnWidth + CONST.spacerWidth) * Math.max(buttons.length, 3) - CONST.spacerWidth;

            var captionSL = makeCaptionLines( caption,
              contentWidth,
              contentWidth - (CONST.dragBtnWidth - CONST.margin - CONST.rim) -CONST.padding,
              CONST.dragBtnHeight - CONST.margin - CONST.rim - CONST.padding
            );
            var captionLines = captionSL.short.length + captionSL.long.length;

            var margin = ( CONST.margin + CONST.rim + CONST.padding );
            this.width = contentWidth + margin *2;
            var h = margin *2 + CONST.captionFontHeight * captionLines + CONST.padding + CONST.btnHeight;
            h = Math.max( h, CONST.dragBtnHeight + CONST.padding + CONST.btnHeight + margin );
            this.height = h;

            var bg = new Sprite( this.width, this.height -1 ); //この -1 がないと画面にゴミが出る
            this.addChild( bg );
            bg.image = new Surface( this.width, this.height );
            this.bgCtx = bg.image.context;

            this.buttons = [];
            for ( var i = 0, bLen = buttons.length; i < bLen; i++ ) {
                var btn = new ButtonFace( 0,0, CONST.btnWidth, CONST.btnHeight, buttons[i][0] );
                if ( !buttons[i][1] ) btn.offColor();
                this.buttons[i] = btn;
                this.addChild( btn );
                this.asignActBtnEvent( btn, i );
            }

            var dragButton = new ButtonFace( 0, 0, CONST.dragBtnWidth, CONST.dragBtnHeight, 'drag' );
            this.addChild( dragButton );
            this.isDragging = false;
            this.asignDragBtnEvent( dragButton );

            this.drawItems( dragButton, buttons, captionSL.short, captionSL.long );
            var x, y;
            x = Math.min( areaMaxX - this.width, (areaMinX + areaMaxX - this.width)/2 );
            x = Math.max( x, areaMinX );
            y = Math.min( areaMaxY - this.height, (areaMinY + areaMaxY - this.height)/2 );
            y = Math.max( y, areaMinY );
            this.x = x; this.y = y;
        }
        ,drawItems: function( dragBtn, buttons, shortCaptions, longCaptions ){
            var tmpX, tmpY;
            this.bgCtx.fillStyle = CONST.bgColor;
            this.bgCtx.fillRect( 0, 0, this.width, this.height );

            this.bgCtx.fillStyle = CONST.rimColor;
            tmpX = tmpY = CONST.margin;
            this.bgCtx.fillRect( tmpX, tmpY, this.width - tmpX *2, this.height - tmpY *2 );

            this.bgCtx.fillStyle = CONST.bgColor;
            tmpX = tmpY = (CONST.margin + CONST.rim);
            this.bgCtx.fillRect( tmpX, tmpY, this.width - tmpX *2, this.height - tmpY *2 );

            this.bgCtx.font = CONST.captionFont;
            this.bgCtx.fillStyle = CONST.captionColor;
            this.bgCtx.textBaseline = 'top'

            tmpX = CONST.dragBtnWidth + CONST.padding *2;
            tmpY = CONST.margin + CONST.rim + CONST.padding;
            for ( var i = 0, cLen = shortCaptions.length; i < cLen; i++ ) {
                this.bgCtx.fillText( shortCaptions[i], tmpX, tmpY + CONST.captionFontHeight * i );
            }
            tmpX = CONST.margin + CONST.rim + CONST.padding;
            tmpY += CONST.captionFontHeight * cLen;
            for ( var i = 0, cLen = longCaptions.length; i < cLen; i++ ) {
                this.bgCtx.fillText( longCaptions[i], tmpX, tmpY + CONST.captionFontHeight * i );
            }

            tmpX = CONST.margin + CONST.rim + CONST.padding;
            tmpY += CONST.captionFontHeight * cLen + CONST.padding;
            tmpY = Math.max( tmpY, CONST.dragBtnHeight + CONST.padding );
            for ( var i = 0, bLen = this.buttons.length; i < bLen; i++ ) {
                this.buttons[i].x = tmpX + (CONST.btnWidth + CONST.spacerWidth) * i;
                this.buttons[i].y = tmpY;
            }
            dragBtn.x = 0; dragBtn.y = 0;
        }
        ,asignActBtnEvent: function(btn, idx){
            var me = this;
            btn.addEventListener( 'touchstart', function(e){ me.touchstartActBtn( btn, e ); });
            btn.addEventListener( 'touchend', function(e){
                me.touchendActBtn( btn, idx, me._callback,e );
            });
        }
        ,touchstartActBtn: function( btn, e ){
            btn.touchingColor();
        }
        ,touchendActBtn: function( btn, idx, e, callback ){
            btn.resetColor();
            this._callback( idx );
        }
        ,asignDragBtnEvent: function(btn){
            var me = this;
            btn.addEventListener( 'touchstart', function(e){ me.touchstartDragBtn( me, btn, e ); });
            btn.addEventListener( 'touchmove', function(e){ me.touchmoveDragBtn( me, btn, e ); });
            btn.addEventListener( 'touchend', function(e){ me.touchendDragBtn( me, btn,e ); });
        }
        ,touchstartDragBtn: function( that, btn, e ){
            btn.touchingColor();
            that.isDragging = true
            that.mousePosPrev = { x:e.x, y:e.y };
            that.mousePosNow = { x:e.x, y:e.y };
        }
        ,touchmoveDragBtn: function( that, btn, e ){
            that.mousePosNow = { x:e.x, y:e.y };
        }
        ,touchendDragBtn: function( that, btn, e ){
            btn.resetColor();
            that.isDragging = false;
        }
        /*
        *  全体の drag 処理
        *  このオブジェクトは Group として、呼び出し元の scene に addChild
        *  されることを想定しているので、
        *  scene.addEventListener( 'enterframe', function(e){ obj.updatePosition(); });
        *  で drag できる。
        */
        ,updatePosition: function(){
            if ( this.isDragging ) {
                this.x += this.mousePosNow.x - this.mousePosPrev.x;
                this.y += this.mousePosNow.y - this.mousePosPrev.y;
                this.mousePosPrev.x = this.mousePosNow.x;
                this.mousePosPrev.y = this.mousePosNow.y;
            }
        }
    });

    global.AskActionUI = AskActionUI;
})(this);


(function(global) {
/*
*  GUI_GetStroke
*       by @H_Kuruno - http://kuruno.cocolog-nifty.com/blog/
*                                       last modified on 2013/12/08
*  ----------------------------------------------------------------------
*
*  GUI_GetStroke( buttons, caption, callback )
*
*  buttons : 表示ボタンに関する配列。
*       [ [ 表示文字列0, activeFlag0 ], [ 表示文字列1, activeFlag1 ], ... ]
*       という 2 次元配列でボタン情報を指定する。ボタン数に制限はないが、
*       現実的には 3-4 個を想定しているので、0 個や多数個の指定は避けたい。
*       なお、表示文字列が 'Retry' のボタンは、このルーチンの中で stroke
*       入力の retry 処理を行い、呼び出し元には戻らない。
*
*  caption : 説明文を指定する。適当な長さで自動的に改行されるが、文字列中に
*       ';' を含めると、ここで強制改行される。したがって、説明文として ';' を
*       表示することはできない。
*
*  callback : function( selected, minX, minY, maxX, maxY, stroke )
*       動作選択ボタンがタップされたら、そのボタンを示す整数値と
*       copy or cut の対象となる領域を指定する stroke[] 配列を与えて
*       callback する。
*
*   selected : タップされた動作選択ボタンを示す整数値。
*
*   minX, minY, maxX, maxY : stroke で示す領域に外接する矩形領域の左上と
*       右下の座標情報。
*
*   stroke[] : 一筆書き分の閉曲線( C 字型に最後が一部閉じていなくても
*       cut/copy/paste 処理の際には処理側で閉じたものとして扱う)
*/
    var CONST = {
         screenX: 768, screenY: 1024
        ,fps: 30
        ,strokeStyle: "rgb(255,200,200)"
        ,strokeWidth: 2
        ,skipDistance: 1
    }

    var GUI = function( buttons, caption, callback ) {
        var _callback = callback;
        var gui = new Game( CONST.screenX, CONST.screenY );
        gui.fps = CONST.fps;
        gui.scale = 1;

        function data2stroke( strokeData ){
            var newStroke = {
                width:CONST.strokeWidth, color:MOON.rgba2int(255,0,0,255), type:"pen",
                data:strokeData
            };
            return newStroke;
        }

    gui.onload = function() {
            var scene = gui.rootScene;
            var bg = new Sprite( CONST.screenX, CONST.screenY );
            bg.image = new Surface( CONST.screenX, CONST.screenY );
            var ctx = bg.image.context;
            scene.addChild( bg );

            var strokes = [];
            var strokeData = [];
            var lastX, lastY;
            function addLinePoint( x, y ) {
                if ( Math.abs( x - lastX ) > CONST.skipDistance ||
                  Math.abs( y - lastY ) > CONST.skipDistance ) {
                    ctx.lineTo( x, y );
                    ctx.stroke();
                    strokeData.push( x, y, 0.1 );
                    lastX = x;    lastY = y;
                }
            }

            bg.addEventListener( Event.PEN_DOWN, function(e){
                if ( strokeData.length == 0 ) {
                    ctx.beginPath();
                    ctx.strokeStyle = CONST.strokeStyle;
                    ctx.lineWidth = CONST.strokeWidth;
                    ctx.moveTo( e.x, e.y );
                    strokeData.push( e.x, e.y, 0.1 );
                    lastX = e.x;    lastY = e.y;
                } else {
                    addLinePoint( e.x, e.y );
                }
            });

            bg.addEventListener( Event.PEN_MOVE, function(e){
                addLinePoint( e.x, e.y );
            });

            var actions = new AskActionUI( buttons, 0,0,CONST.screenX -1, CONST.screenY -1, caption,
              function( selected ){
                /*
                *  action button のいずれかが tap されると、この関数が call される。
                *  ここから GUI の呼び出し元に、callback して GUI を終了する。
                */
                if ( (buttons[selected][0]).match( /retry/i ) ) { // retry
                    strokeData = [];
                    ctx.clearRect( 0, 0, CONST.screenX, CONST.screenY );
                } else {
                    ctx.closePath();

                    var minX, minY, maxX, maxY;
                    minX = maxX = strokeData[0];
                    minY = maxY = strokeData[1];
                    for ( var i = 3, sLen = strokeData.length; i < sLen; i += 3 ) {
                        minX = Math.min( minX, strokeData[i] );
                        maxX = Math.max( maxX, strokeData[i] );
                        minY = Math.min( minY, strokeData[i+1] );
                        maxY = Math.max( maxY, strokeData[i+1] );
                    }
                    gui.stop();
                    _callback( selected, minX, minY, maxX, maxY, data2stroke( strokeData ) );
                }
            });
            scene.addChild( actions );
            scene.addEventListener( 'enterframe', function(e){ actions.updatePosition(); });
            actions.x = ( CONST.screenX - actions.width )/2;
            actions.y = ( CONST.screenY - actions.height )/2;
        }
        gui.start();
    }

    global.GUI_GetStroke = GUI;
})(this);


(function(global) {
/*
*  GUI_SelectAction
*       by @H_Kuruno - http://kuruno.cocolog-nifty.com/blog/
*                                       last modified on 2013/12/03
*  ----------------------------------------------------------------------
*
*  GUI_SelectAction( buttons, caption, centerX, centerY, callback )
*
*  buttons : 表示ボタンに関する配列。
*       [ [ 表示文字列0, activeFlag0 ], [ 表示文字列1, activeFlag1 ], ... ]
*       という 2 次元配列でボタン情報を指定する。ボタン数に制限はないが、
*       現実的には 3-4 個を想定しているので、0 個や多数個の指定は避けたい。
*
*  caption : 説明文を指定する。適当な長さで自動的に改行されるが、文字列中に
*       ';' を含めると、ここで強制改行される。したがって、説明文として ';' を
*       表示することはできない。
*
*  centerX, centerY : 動作選択ボタンを、この座標を中心として表示する。
*       シールの中心座標を渡されることを想定している。但し、画面全体から
*       はみ出しそうなら補正する。(CONST.screenX, screenY を参照する)
*
*  callback : function( selected )
*       動作選択ボタンがタップされたら、そのボタンを示す整数値を与えて
*       callback する。
*
*   selected : タップされた動作選択ボタンを示す整数値。
*/
    var CONST = {
         screenX: 768, screenY: 1024
        ,fps: 60
    }

    var GUI = function( buttons, caption, centerX, centerY, callback ) {
        var _callback = callback;
        var gui = new Game( CONST.screenX, CONST.screenY );
        gui.fps = CONST.fps;
        gui.scale = 1;

    gui.onload = function() {
            var scene = gui.rootScene;

            var actions = new AskActionUI( buttons, 0,0,CONST.screenX -1, CONST.screenY -1, caption,
              function( selected ){
                /*
                *  action button のいずれかが tap されると、この関数が call される。
                *  ここから GUI の呼び出し元に、callback して GUI を終了する。
                */
                gui.stop();
                _callback( selected );
            });
            scene.addChild( actions );
            scene.addEventListener( 'enterframe', function(e){ actions.updatePosition(); });
            var x, y;
            x = Math.max( 0, centerX - actions.width/2 );
            x = Math.min( x, CONST.screenX -1 - actions.width );
            y = Math.max( 0, centerY - actions.height/2 );
            y = Math.min( y, CONST.screenY -1 - actions.height );
            actions.x = x; actions.y = y;
        }
        gui.start();
    }

    global.GUI_SelectAction = GUI;
})(this);

前々から学習しようと思っていた Git をとうとう使い始めることができましたので、自分の過去コードの参照もしやすくなりました。あとは、開発するたびにかなり書き損ないをするので、やはりテスト駆動を覚えたい処で、これもまたずっと手を付けずにいた RSpec( ruby のです ) と同時に Jasmine を覚えられないかと夢想しています。とりあえず、RSpec の本は買ってきました。

2013年12月 5日 (木)

「双方向リンク」シールを update しました。

以前「双方向リンク」シールを作りましたが、これも update しました。単に accessjson.js の修正のみではありません。

Download : 「双方向リンク登録」シール / 「双方向リンク検索」シール

実は Issue Tracker に回答が返っていました

このような tweet もありましたが、私も暫く前まで気付いていませんでした。

以前 Issue Tracker : issue #138 に 「シールを剥がしにくくて困る」 と投稿したのですが、暫く前に Issue Tracker を見直してみると、#138 が一覧から消えていました。Search 欄を "Open issues" から "All issues" に変更して再検索してみるとリストに表示され、中を見ると返事があったのです。G-mail は使わない人なので、改めて G-mail に login してみると、メールもちゃんとありました。曰く、

以下のようなコードで同様な事が実現可能かと存じます。

importJS(["lib/MOON.js"], function() { 
    var sticker = Sticker.create(); 
    sticker.ontap = function() { 
        localStorage["tap"] = true;
        MOON.peel();
        MOON.finish(); 
    }; 
    sticker.onattach = function() { 
        MOON.finish(); 
    }; 
    sticker.ondetach = function() { 
        if (localStorage["tap"] == true) {
            localStorage["tap"] = false;
            MOON.openPage("8ET7iS381356966172426"); // PageID
        }
        MOON.finish(); 
    }; 
    sticker.register(); 
});


概略は次の通りです。

① タップの際(或いは貼付けの際)にタップ操作である事のフラグを保存しつつシールを剥がす処理を実行する
② ①により ondetach の処理に移行するので、その中でページの移動処理を行う。この際、ユーザが剥がしたのか
  コードにより剥がされたのかで処理を分けたい場合、①にて保存したフラグにより確認する。

気付いたのは暫く前だったのですが、コピペシールの開発に取り掛かっていたので、途中で手を出す余裕がなく、今回の修正時に動作確認してみました。「双方向リンク検索」シールの hack.js を以下のように修正すると、無事にページ移動とシール剥がしを両立するシールが作成できました。似たようなものが必要になった時に使えそうなテクニックです。

hack.js : 修正前

importJS(["lib/MOON.js", "main.js"], function() {
    var sticker = Sticker.create();

    function shrinkStickerClip() {
        var myID = getStickerID();
        var paperJSON = MOON.getPaperJSON( myID );
        paperJSON.clip.data = [0.0,0.0,0.05,104.0,0.0,0.05,
          104.0,85.0,0.05,0.0,85.0,0.05,0.0,0.0,0.05
        ];
        paperJSON.width = 105;
        paperJSON.height = 86;
        paperJSON.image = "peel.png";
        MOON.setPaperJSON( myID, paperJSON );
    }

    sticker.ontap = function(event) {
//        MOON.finish();

        MOON.peel();
    };

    sticker.onattach = function(event) {
        var query = queryForLinkedSearch();
        shrinkStickerClip();
        MOON.searchStorage( query );
//        MOON.peel();

        MOON.finish();
    };

    sticker.ondetach = function(event) {
        MOON.finish();
    };

    sticker.register();
});

hackjs. : 修正後

importJS(["lib/MOON.js", "lib/accessjson.js", "main.js"], function() {

    var AUTO_PEEL_FLAG = 'BILINK_PEEL';

    var sticker = Sticker.create();

    sticker.ontap = function(event) {
        MOON.finish();
    };

    sticker.onattach = function(event) {
        StorageUtil.set( AUTO_PEEL_FLAG, true );
        MOON.peel();
        MOON.finish();
    };

    sticker.ondetach = function(event) {
        if ( StorageUtil.get( AUTO_PEEL_FLAG, false ) ) {
            StorageUtil.set( AUTO_PEEL_FLAG, null );
            MOON.searchStorage( queryForLinkedSearch() );
        }
        MOON.finish();
    };

    sticker.register();
});

旧 「双方向リンク検索」シール(1), 「双方向リンク検索」シール(2) は廃盤にします。

前に作った時には「剥がして移動する」ことができなかったので、残ったシールの剥がし方違いで 2 パターン作ったのですが、無事目的の動作ができるようになったので、(1) (2) は廃盤として、「双方向リンク検索」シールに一本化します。

まだ、ちょっと納得いかない動作をする時もあるのですが、MOONPhase ver 2.8.0 になり、ページ移動も速くなったので、もうちょっとすれば、そこそこ実用的なシールになりそうです。

過去シールの ver 2.8.0 対応 update

昨日の記事に書いたように、過去のシールで修正の必要なものができましたので、以下を update しました。

Download :

accessjson.js の中身は、最終的に、以下のように修正しました。

修正前

function getStickerID() {
    var relURL = window.location.getAbsoluteURL("").split("/Data/")[1];
    return relURL.split( "/" )[2];
}

function getPageID() {
    var relURL = window.location.getAbsoluteURL("").split("/Data/")[1];
    return relURL.split( "/" )[1];
}

修正後

(function(global) {
    function relDirectories( i ){
        var relURL = location.href;
        relURL = relURL.slice( relURL.search( /\/data\//i ) +1 );
        return relURL.split( "/" )[i];
    }

    var InfoUtil = {
        ,getStickerID: function(){ return relDirectories( 3 ); }
        ,getPageID: function(){ return relDirectories( 2 ); }
        ,getNoteID: function() { return relDirectories( 1 ); }

    global.InofUtil = InfoUtil;
})(this);

将来、複数のノートを持てるようになった場合、ノート名も InfoUtil.getNoteID() で取得できます。

2013年12月 4日 (水)

MOONPhase version up で、MyStickerID() の修正が必要に・・・

MOONPhase v 2.8.0 に update しました。

基本的によくなってるのですが、参ったのがひとつ。

私の作ってるシールで、シールの ID (シールの directory 名) を参照するのに、accessjson.js というライブラリを使ってるのがあります。

(「双方向リンク」シール、「定着」シール、「可変タグ」シール)

どれも内部で以下のコードを使ってるのですが、

accessjson.js の一部

function getStickerID() {
    var relURL = window.location.getAbsoluteURL("").split("/Data/")[1];
    return relURL.split( "/" )[2];
}

function getPageID() {
    var relURL = window.location.getAbsoluteURL("").split("/Data/")[1];
    return relURL.split( "/" )[1];
}

ところが、MOONPhase v 2.8.0 で window.location.getAbsoluteURL("")  の仕様が少し変わりました。

これまでは、 "???/Data/MyNotebook1/xxxx/yyyy" の形で値が戻っていたのが、v 2.8.0 では  "moon://data/MyNotebook1/xxxx/yyyy" の形になりました。

上記のコードを読めばわかりますが、大文字の /Data/ で処理しているので、小文字の /data/ だと動作しません。

急ぎ手直しして、「手書きコピペ」シールをリリースしましたが、これまでに公開したシールも今週中にバージョンアップします。

ちなみに直したコードは以下です。

(function(global) {
    var InfoUtil = {
        ,getStickerID: function() {
            var relURL = window.location.getAbsoluteURL("");
            relURL = relURL.slice( relURL.search( /\/data\//i ) +1 );
            return relURL.split( "/" )[3];
        }
        ,getPageID: function() {
            var relURL = window.location.getAbsoluteURL("");
            relURL = relURL.slice( relURL.search( /\/data\//i ) +1 );
            return relURL.split( "/" )[2];
        }
        ,getNoteID: function() {
            var relURL = window.location.getAbsoluteURL("");
            relURL = relURL.slice( relURL.search( /\/data\//i ) +1 );
            return relURL.split( "/" )[1];
        }
    };
    global.InfoUtil = InfoUtil;
})(this);

これでどうだ。コピペシール、その(2)

前回暫定公開した「矩形コピペ」シールですが、開発を始める段階から、GUI 部分を切り離しておいて、「いつか」ペンで範囲指定できるようにしたいと考えていました。

ところが、開発中にあれこれ調べていると、Canvas に isPointInPath() というメソッド があることや、enchantMOON Developers のページ に、ペンイベントの使い方 が載っているのをみつけ、これなら何とかなるだろう、と平行して開発に手を付けました。(実は後述のような罠があったのですが)とにかく、ペン周りは後に回して、 cut / copy / paste 周りのロジックを優先し、前回のシールで動くようになったので、週末からひたすらコーディングしてはデバッグしてました。

動いても処理が重くて使い物にならないことも想定していたのですが、意外と許せる程度には快適に動きます。前回の「矩形コピペ」とそんなにパフォーマンスも変わらず、使い勝手は明らかにこちらが上なので、「矩形コピペ」シールは公開しないことにしました。資料として残しておくだけにします。

で、公開 : Download : 「手書きコピペ」シール

使い方:

  • シール台帳から画面に貼りつけます。
    • 動作選択ボタンが表示されます。
  • Cut/Copy の対象としたい領域をペンで囲みます。領域を囲む線(ストローク)は、きっちりと閉じてしまう必要はありません。C 字型のように、最後が開いていても、内部で自動的に閉じて扱います。むしろ最後がクロスしてしまう(∝字型のように)の方が、思わぬ動作をしてしまうかもしれません。
    • 囲み線を描き損ねてしまったら、"Retry" ボタンをタップしてください。ストロークが消去されて、一から書き直せる状態に戻ります。
    • 囲み線は、一気に書き上げてしまわなくても、点線のように飛び飛びで書けば、自動的に直線補完してつなぎます。一筆書きしかできません。(その方が処理が簡単なので)
  • 対象領域が決まれば、"Cut", "Copy" ボタンにタップします。
    • "Cancel" ボタンをタップすると、何もせずに戻り、シールも自分で剥がしてきれいに終了します。
  • "Cut", "Copy" で、対象領域のストロークをシールに拾い上げます。始点と終点の両方が領域内に含まれるストロークだけを操作対象としますので、囲み線と交差しているストロークは残ってしまう場合があります。
    • 操作対象のストロークを、元の位置のままシールに張り付けられればよいのですが、MOONPhase v.2.7.0 現在、script からシールの位置変更ができません。(Issue Tracker #117)
    • そのため、取り込んだストロークが元のシールの位置に移動しますが、ご容赦ください。
      シールを、張り付けたい場所に移動させます。
  • タップすると、"Paste", reset Cut", "Cancel" が選択できるようになります。"Paste", "Cancel" は、そのままです。"reset Cut" は、"Copy" でなく、"Cut" でストロークを取り出していた場合、そのストロークをページのストロークに戻します。
  • "reset Cut" するとシールは剥がれますが、"Paste" ではシールは剥がれません。したがって、複数回 "Paste" できます。用が済めば、明示的に剥がしてください。

できなかったこと

シール貼り付けの際、3 本指キャンセルすると、画面上に「透明の」シールが残ってしまいます。3 本指キャンセルは、問答無用でシールが終了しますので後始末もできず、現状では回避困難です。残ったシールをタップしても何もできないので実害はありませんが、キャンセルしたければ "Cancel" ボタンをお勧めします。

この件については、Issue Tracker #154 に投稿しておきました。

囲み線が細くて大変見にくいです。これは、MOONPhase ver 2.8.0 現在、Canvas の lineWidth がサポートされておらず、1 pixel 幅のラインしか引けないからです。

あと、何故だかしりませんが、ボタン表示にゴミがでます。ほとんど、Canvas.fillRect() で描いてるんですが、座標計算まちがってるのかなぁ?

妥協したこと

上述の、囲み線が細いという件、本気になれば回避方法はあるのでしょう。@ina_ani さんの「ペイントアプリ」 では、筆圧対応などもできているそうですから、太い線を引くこともできるはずなのですが、今回は手を出しませんでした。

また、MOON で普通に線を手書きすると、info.json には サブピクセル単位の分解能でストローク情報が格納されますが、あまり細かくなると、今回の目的にはオーバースペックになるばかりか、動作が遅くなりそうです。ですので、囲み線を書く際に、1 ピクセルより小さい移動は無視するようにコーディングしてあります。(必要なら、3 ピクセル飛ばしとか、5 ピクセル飛ばしとかできるように作ってあります)

結果として、「領域を指示するなら十分」という stroke data を取得する GUI になっていますので、「手書きの味のある線」は書けません。もっとも、今回はそんな機能はない方がよいと思いますが。

後はデザイン関係ですね。線の色、ボタンの色…。もっといい色があったら、rgbコードで教えて下さい。線の色と、ボタンの色は、内部で定数としてまとめて定義しているので、気に入らなかったら変えてください。

それと、ボタンセットを画面上でドラッグするための "drag" ボタン。今回書いたコードでは、ボタンの表示は「文字」しか使えないので、今のような表示になっていますが、本当なら、

Drag こんな感じ

にしたかったのですが。まあ、いつか手が回ったら。

いろいろ

まず、冒頭で書いた「罠」ですが。

Canvas に isPointInPath() というメソッド .があるのに気付いたので開発を始めたのですが、ある程度動くようになってからテストしても期待した動作をしない。よくよく調べてみると、なんと Canvas の isPointInPath() もサポートされていませんでした。

一旦諦めかけたのですが、閉曲線内外判定ルーチンくらいなら、頑張れば書けなくもない。しかし、今から数学の教科書を開かなくても、こういうのは既に定番があって、車輪の再発明をしなくても済むだろう、と検索しました。

最終的に参考にした(というより、そのままパクった)のは、このページ です。数学的には同じ内容をあちこちで見かけましたが、これは大変こなれたコードで、処理のほとんどが大小比較と boolean の一致比較です。掛け算・割り算の出てくる部分が最小限で、非常に計算強度の楽なコードです。

おかげで、部分適用みたいな回りくどい使い方をしているのに、そこそこのパフォーマンスが出ています。自力で考えていたら、まだできていないし、もっとパフォーマンスは落ちていたと思います。

そもそも、最初に、isPointInPath() がサポートされていないことに気づいていたら、今回のシールは作っていなかったでしょう。ほぼ出来上がりかけてから気付いたので、そのまま最後まで作り上げられました。怪我の功名でした。

なお、今回のシール、「コピペシール」なのですが、'reset Cut' 機能があるので、そのままで「UNDO のある、ストローク消去シール」としても使えます。UNDO 不要なら、ストロークデータをページから剥がしてそのまま MOON.peel() すれば即消去シールに改造できます。

長くなったので、コードと注釈・言い訳は、記事を別にして書きます。

« 2013年11月 | トップページ | 2014年3月 »