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    
無料ブログはココログ

« 「双方向リンク」シールを update しました。 | トップページ | 手書き検索シールと、とりあえずな汎用の(?) stroke 入力 GUI »

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 の本は買ってきました。

« 「双方向リンク」シールを update しました。 | トップページ | 手書き検索シールと、とりあえずな汎用の(?) stroke 入力 GUI »

enchantMOON」カテゴリの記事

コメント

コメントを書く

(ウェブ上には掲載しません)

トラックバック

この記事のトラックバックURL:
http://app.f.cocolog-nifty.com/t/trackback/1527616/54187182

この記事へのトラックバック一覧です: 手書き版コピペシールのソースコード:

« 「双方向リンク」シールを update しました。 | トップページ | 手書き検索シールと、とりあえずな汎用の(?) stroke 入力 GUI »