[HTML5] Canvas + EaselJSを使ってSDNもどきのUIを作ってみる #1

クラウド環境の利用がスタンダードになりつつある昨今、Web I/Fを通して対話的にクラウド環境構築できることは当然ですが、もっとビジュアルにクラウド環境が構築できればいいなと思い、SDN(Software Defined Networking)もどきのUIをHTML5のCanvasを利用してデモプログラムを作成してみることにしました。このシリーズは3回に分けて公開します。

利用した環境

  • EaselJS0.7.0
  • IIS7.5(Web Serverならなんでも可)
  • ブラウザはChrome

作成する流れ

  1. 固定の文字と画像をStage上に配置する
  2. 配置した画像に対してEventListenerを定義する
  3. 固定画像をmousedownイベントでコピーを作成し、clickイベント(マウスボタンアップ)時に有効範囲であれば図形を追加し、範囲外であればドラッグ中の図形を削除する
  4. 緑のエリア内の図形をダブルクリックすることで、内部的に選択状態にし、もう一方の図形をダブルクリックで2画像間に連結線を描画する
  5. 緑のエリア内の図形をDragする際に連結線が描画されている場合はマウスドラッグに合わせて連結線も再描画させる

CANVASを作成し、固定の文字と画像をStage上に配置する

No0

1つのStageを2ペインに内部的に分割して、緑のエリアを機器構成定義エリア、右のエリアにはオリジナルを配置するエリアとして見立てます。(幅730pxのところで内部的に分割)

	<div class="canvasHolder">
		<canvas id="DnDCanvas" width="960" height="400" style="float:left;" tabindex="1"></canvas>
	</div>
//SDNエリアに色を付ける
var _rect = new createjs.Graphics();
_rect.beginFill("#CEF4B5");
_rect.drawRect(0,0,730,400);
var sdn = new createjs.Shape(_rect);
stage.addChild(sdn);

var _line = new createjs.Shape();

//仕切り縦の描画
_line.graphics.beginStroke("black");
_line.graphics.moveTo(730, 0);
_line.graphics.lineTo(730, 400);
stage.addChild(_line);

次に固定の文字と画像を右側のエリアに配置してみます。

//とりあえずハードコーディングでx,yを指定
//テキストの描画(LB)
_label = new createjs.Text("Router", "bold 10px Arial", "#000");
_label.textAlign = "center";
_label.x = 800;
_label.y = 10;
stage.addChild(_label);

//テキストの描画(FW)
_label = new createjs.Text("Firewall", "bold 10px Arial", "#000");
_label.textAlign = "center";
_label.x = 900;
_label.y = 10;
stage.addChild(_label);

省略
・
・
・

//timer ms
var ms = 200;
// load image:
var image = new Image();
image.src = "assets/LB.png";
image.onload = ImageLoad;
Sleep(ms);

var image_1 = new Image();
image_1.src = "assets/FW.png";
image_1.onload = ImageLoad;
Sleep(ms);

var image_2 = new Image();
image_2.src = "assets/Router.png";
image_2.onload = ImageLoad;
Sleep(ms);

省略
・
・
・

画像に対してEventListenerを定義する

function createBitmap(image, i, x, y) {

  bitmap = new createjs.Bitmap(image);
  bitmap.x = x;
  bitmap.y = y;
  bitmap.rotation = 0;

  bitmap.regX = bitmap.image.width/2|0;
  bitmap.regY = bitmap.image.height/2|0;
  bitmap.scaleX = bitmap.scaleY = bitmap.scale = 1;
  bitmap.cursor = "pointer";

  bitmap.on("mousedown", Dragstart);

  bitmap.on("dblclick",doubleClick);

  bitmap.on("click", Dragend);

  bitmap.on("mouseover", function(evt) {
      var o = evt.target;
      o.scaleX = o.scaleY = o.scale;
      update = true;
  });
        
  bitmap.on("mouseout", function(evt) {
      var o = evt.target;
      o.scaleX = o.scaleY = o.scale;
      update = true;
  });
  return bitmap;
}

EaselJS0.6.0では
bitmap.addEventListener("mousedown", Dragstart)
としていましたが、EaselJS0.7.0では
bitmap.on("mousedown", Dragstart)と定義するようです。

mousedownイベントでオリジナル画像の複製を作る

Dragstart関数の中で、オリジナル画像を選択した場合は、click開始と同時にcreateBitmap関数を呼び出して複製を作成し、mouseの動きに合わせて画像をDragさせる。

	function Dragstart(evt){
            var o = evt.target;
            if (o.name == null){
	            o.name = "canvas_"+ sdn_container.getNumChildren();
	            o.index = sdn_container.getNumChildren();
	            o.scaleX = o.scaleY = o.scale = 1;
            }

            sdn_container.addChild(o);
            var offset = {x:o.x-evt.stageX, y:o.y-evt.stageY};

            //ここでドロップした座標がエリア内かを判断してaddChildするかを決めるロジック
            if ( o.x > 730 ){
	            var newBitmap = createBitmap(o.image, sdn_container.getNumChildren(), o.x, o.y);
	            newBitmap.x = o.x;
	            newBitmap.y = o.y;
	            sdn_container.addChildAt(newBitmap,sdn_container.getNumChildren()-1);
	            update = true;
            }
            o.on("pressmove", function(ev) {
       	    var o = ev.target;
            o.x = ev.stageX+offset.x;
            o.y = ev.stageY+offset.y;
            // indicate that the stage should be updated on the next tick:
	       update = true;
	    });
	}

ここまででこんな感じ

No1

2画像間に連結線を描画する

ダブルクリックイベントで取得した1つ目の画像オブジェクトの名前をlocalStorageへ保存しておき、2つ目の画像オブジェクトの名前と比較して同一でなければ1つ目の画像の中心座標から2つ目の画像の中心座標へlineToを使って線を描画します。

	function doubleClick(evt){
        var o = evt.target;
        if (localStorage.getItem("obj_name") == null){
        	//1番目のオブジェクトダブルクリック
	        localStorage.setItem("obj_name",o.name);
        }else{
        	//2番目のオブジェクトダブルクリック
        	var prev_obj_name = localStorage.getItem("obj_name");
        	if( prev_obj_name != o.name){
                        //1番目にクリックした情報を取得する
                        tmp = sdn_container.getChildByName(prev_obj_name);
                        var relation_line = new createjs.Shape();
                        //連結線の描画
                        relation_line.graphics.beginStroke("black");
                        relation_line.graphics.moveTo(tmp.x, tmp.y);
                        relation_line.graphics.lineTo(o.x, o.y);

                        var check = prev_obj_name.indexOf("_");
                        var line_num = prev_obj_name.substring(check+1,prev_obj_name.length);

                        //counterを取得する
                        var m = move_cnt();
                        relation_line.name = "dgline_" + m;

                        sdn_container.addChildAt(relation_line,0);
                        localStorage.removeItem("obj_name");

                        //親子間の連結線状態をlocalstorageに保存しておく。
                        //from 親 to 子
                        if ( localStorage.getItem(o.name) == null){
                            tmptext = "E:" + relation_line.name;
                        }else{
                            tmptext = localStorage.getItem(o.name) + ",E:" + relation_line.name;
                        }

                        //線の名前をKeyにする
                        localStorage.setItem(o.name, tmptext);
                        if( localStorage.getItem(prev_obj_name) == null){
                            tmptext = "S:" + relation_line.name;
                        }else{
                            tmptext = localStorage.getItem(prev_obj_name) + ",S:" + relation_line.name;
                        }
                        localStorage.setItem(prev_obj_name,tmptext);
        	}
        }
        update = true;
	}

ここまででこんな感じ。ちょっとそれっぽく見えてきた感がありますね。

No3

マウスドラッグに合わせて画像と連結線を再描画させる

createBitmapで複製された画像にはEventListenerが定義してあるので、今のままだと画像はドラッグ&ドロップはできますが、連結線は当然何も処理を入れていないので置き去りになります。(下記画像を参照)

No4

なので、画像ドラッグ時のpressmoveイベントの中に連結線の再描画処理を加えます。(素人なので、強引にlocalStorageをフル活用して対応しています。)

        o.on("pressmove", function(ev) {
        var o = ev.target;
        o.x = ev.stageX+offset.x;
        o.y = ev.stageY+offset.y;

        //ここから連結線の処理を追加する。
        var obj_string = localStorage.getItem(o.name);
        if (obj_string != null){

            //カンマで分割して配列に格納
            var objArray = obj_string.split(",");
                for( var i=0 ; i<objArray.length ; i++ ) {
	        				
                    var flag = objArray[i].substring(0,objArray[i].indexOf(":"));
                    var line_num = objArray[i].substring(objArray[i].indexOf(":")+1,objArray[i].length);
                    var line_obj = container.getChildByName(line_num);

                    //lineオブジェクトの座標再設定
                    if( flag == "S"){
                        line_obj.graphics._activeInstructions[0].params[0]=o.x;
                        line_obj.graphics._activeInstructions[0].params[1]=o.y;
                    }else{
                        line_obj.graphics._activeInstructions[1].params[0]=o.x;
                        line_obj.graphics._activeInstructions[1].params[1]=o.y;
                    }
                }
            //ここまで連結線の処理を追加する。
            update = true;
	 });

これで画像のドラッグに合わせて連結線も再描画され自由に移動させることができました。

No5

動作できる機能は

  • 右エリア画像のドラッグ&ドロップ
  • 1つ目画像のダブルクリック、2つ目画像のダブルクリックで連結線の描画
  • 配置後の画像オブジェクトのドラッグ&ドロップ(連結線も連動)

デモはこちら(完成度は低いです)

シビアなイベントハンドリングをしていないので、やさしくゆっくりと動作させてくださいw

まとめ

次回はもう少し処理を追加したいと思います。

  1. メニューボタンの作成
  2. マウスオーバーでオブジェクトの情報を表示