お絵描き③(定形形状描画)

マウスによる定形形状描画

今回はマウスを使って、始点と終点を指定することで、簡単に描ける定形形状について検討してみました。下の図の通り、黒太線で囲まれたボタンをクリックして描画形状を選択後、CANVAS上でマウスを使って、始点選択(マウスダウン)→移動(マウスムーブ)→終点選択(マウスアップ)の一連の操作で矢印,円,長方形,楕円等の形状を描くことが出来ます。
(→テストプログラムにリンク

テストプログラム

楕円を書く関数が無いのは予想外でした。scaleを指定して円を変形させる方法がある様ですが、scale指定時に何故かエラーが発生してしまうので、断念しました。矢印と長丸は少し時間が掛かりましたが、図の通りなんとかそれらしいものが出来ました。プログラム中にも各形状部にコメントしているので、細かい説明は省略します。
(→テストプログラムにリンク

<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>DRAWING TEST</title>

    <style>
	#cvs-layer { position:relative ; }
	#cvs-layer canvas { position:absolute ; top:0 ; left:0 ; }

	input { height:30px; }
	#select-color input { width:26px; height:36px; }
	#select-shape input { padding:0; font-size:16px; font-weight:900; width:36px; height:36px; border-style:solid; border-width:4px; }
    </style>

    <script>

	var cvs_drw ; var ctx_drw ;
	var cvs_edt ; var ctx_edt ;
	var cvs_wdt ; var cvs_hgt ;
	var x_b , y_b , x_c , y_c ;
	var f_b ;
	var selected_shape ;

	function draw_begin(){
	    cvs_wdt = 840 ; cvs_hgt = 600 ;
	    cvs_drw = document.getElementById("CANVAS_DRW") ; 
	    ctx_drw = cvs_drw.getContext("2d") ;
	    cvs_edt = document.getElementById("CANVAS_EDT") ; 
	    ctx_edt = cvs_edt.getContext("2d") ;

	    cvs_drw.width = cvs_wdt ; cvs_drw.height = cvs_hgt ;
	    cvs_edt.width = cvs_wdt ; cvs_edt.height = cvs_hgt ;
	 
	    cvs_edt_init() ; chng_col("#000000") ; chng_shape(3);
	    ctx_drw.strokeRect( 0 , 0 , cvs_wdt , cvs_hgt ) ;

	    cvs_edt.addEventListener( "mousedown" , mouse_dn , false ) ;
	    cvs_edt.addEventListener( "mousemove" , mouse_mv , false ) ;
	    cvs_edt.addEventListener( "mouseup", mouse_up , false ) ;
	}

	//▼▼▼▼▼▼ マウス検知 ▼▼▼▼▼▼
	function mouse_dn(event){
	    var l_thkn = 1 ; var t_rate = 1 ; var l_colr = "#333333" ;

	    var rect = event.target.getBoundingClientRect() ;
	    x_b = event.clientX-rect.left ;
	    y_b = event.clientY-rect.top ;
	    f_b = true ;
	    ctx_edt.lineJoin = "round" ; ctx_edt.lineCap = "round" ;

	    t_rate = document.getElementById("s_transparency").value/100 ;
	    l_colr = document.getElementById("select-color").style.backgroundColor ;
	    l_thkn = document.getElementById("l_thickness").value ;

	    if(selected_shape==0){ctx_edt.lineCap = "butt";}
	    if(l_thkn>6 && 3>selected_shape){ l_thkn=6 ; document.getElementById("l_thickness").value=l_thkn ; }

	    ctx_edt.globalAlpha = t_rate ; ctx_edt.lineWidth = l_thkn ;
	    ctx_edt.strokeStyle = l_colr ; ctx_edt.fillStyle = l_colr ;
	}


	function mouse_mv(event){
	    if(f_b){
		var rect = event.target.getBoundingClientRect() ;
		x_c = event.clientX-rect.left ;
		y_c = event.clientY-rect.top ;

		if(selected_shape==3){
		    draw_line() ; x_b = x_c ; y_b = y_c ;	// 自由線
		}else{
		    draw_shape() ;				// 定型形状
		}
	    }
	}

	function mouse_up(event){ 
	    f_b=false;
	    CVS_Transfer() ;
	}
	//▲▲▲▲▲▲ マウス検知 ▲▲▲▲▲▲


	// 自由線
	function draw_line(){
	    ctx_edt.beginPath() ;
	    ctx_edt.moveTo(x_b,y_b) ;
	    ctx_edt.lineTo(x_c,y_c) ;
	    ctx_edt.stroke() ;
	}

	// 定型形状
	function draw_shape(){
	    var rct_edt = cvs_edt.getBoundingClientRect() ;
	    ctx_edt.clearRect( 0, 0, rct_edt.width , rct_edt.height ) ;		// CANVASクリア
	    var x_t = x_c - x_b ; var y_t = y_c - y_b ;

	    // ◆直線・矢印・〇付矢印
	    if(selected_shape==0 || selected_shape==1 || selected_shape==2){
		if(selected_shape==0 || selected_shape==1){ line_p_p(x_b,y_b,x_c,y_c); }

		if(selected_shape==1 || selected_shape==2){
		    var arw_w=10; var arw_l=20;
		    var v_l = Math.sqrt(x_t*x_t+y_t*y_t); var x_u= x_t/v_l; var y_u= y_t/v_l;
		    var p_x1 = x_c - y_u * arw_w - x_u * arw_l; var p_y1 = y_c + x_u * arw_w - y_u * arw_l;
		    var p_x2 = x_c + y_u * arw_w - x_u * arw_l; var p_y2 = y_c - x_u * arw_w - y_u * arw_l;
		    line_p_p(x_c,y_c,p_x1,p_y1); line_p_p(x_c,y_c,p_x2,p_y2);

		    if(selected_shape==2){
			ctx_edt.fillStyle = "#ffffff" ; var arw_r=20;
			var p_x0 = x_b + x_u * arw_r ; var p_y0 = y_b + y_u * arw_r ;
			line_p_p(p_x0,p_y0,x_c,y_c);
			//ctx_edt.beginPath() ; ctx_edt.moveTo(p_x0,p_y0) ; ctx_edt.lineTo(x_c,y_c) ; ctx_edt.stroke() ;
			ctx_edt.beginPath(); ctx_edt.arc( x_b , y_b , arw_r , 0 , 2 * Math.PI , false) ; ctx_edt.fill();
			ctx_edt.beginPath(); ctx_edt.arc( x_b , y_b , arw_r , 0 , 2 * Math.PI , false) ; ctx_edt.stroke();
		    }
		}

	    // ◆真円+両端円弧(中心+頂点1ヶ所)
	    }else if(selected_shape==4 || selected_shape==5){
		var x_t_abs = Math.abs(x_t) ; var y_t_abs = Math.abs(y_t) ;
		var arc_p = [] ; var arc_r = 1;
		var arc_c_x1 = x_b; var arc_c_y1 = y_b; var arc_c_x2 = x_b; var arc_c_y2 = y_b;

		if( x_t_abs > y_t_abs ){
		    // 横軸(X軸)長い
		    arc_r = Math.ceil( y_t_abs / 2 );
		    if(y_c>y_b){ arc_c_y1 = y_b + arc_r; }else{ arc_c_y1 = y_b - arc_r; }
		    arc_c_y2 = arc_c_y1 ; arc_p[0]=0.5; arc_p[1]=1.5; arc_p[2]=-0.5; arc_p[3]=0.5;

		    if(x_c>x_b){ arc_c_x1 = x_b+arc_r; arc_c_x2 = x_c - arc_r; }
		    else{ arc_c_x1 = x_c+arc_r; arc_c_x2 = x_b - arc_r; }
		    
		}else{
		    // 縦軸(Y軸)長い
		    arc_r = Math.ceil( x_t_abs / 2 );		
		    if(x_c>x_b){ arc_c_x1 = x_b + arc_r; }else{ arc_c_x1 = x_b - arc_r; }
		    arc_c_x2 = arc_c_x1 ;
		    arc_p[0]=1; arc_p[1]=2; arc_p[2]=0; arc_p[3]=1;

		    if(y_c>y_b){ arc_c_y1 = y_b+arc_r; arc_c_y2 = y_c-arc_r; }
		    else{ arc_c_y1 = y_c+arc_r; arc_c_y2 = y_b-arc_r; }
		}
		
		ctx_edt.beginPath();
		ctx_edt.arc( arc_c_x1 , arc_c_y1 , arc_r , arc_p[0] * Math.PI , arc_p[1] * Math.PI , false) ;
		ctx_edt.arc( arc_c_x2 , arc_c_y2 , arc_r , arc_p[2] * Math.PI , arc_p[3] * Math.PI , false) ;
		ctx_edt.closePath();
		if(selected_shape==5){ ctx_edt.fill(); }else if(selected_shape==4){ ctx_edt.stroke(); }


	    // ◆真円+両端円弧(中心+頂点1ヶ所)
	    }else if(selected_shape==14 || selected_shape==15){
		var x_t_abs = Math.abs(x_t) ; var y_t_abs = Math.abs(y_t) ;
		var arc_p = [] ; var arc_r = 1;
		var arc_c_x1 = x_b; var arc_c_y1 = y_b; var arc_c_x2 = x_b; var arc_c_y2 = y_b;

		if( x_t_abs > y_t_abs ){ 
		    // 横軸(X軸)長い
		    arc_r = Math.ceil( y_t_abs );
		    
		    if(x_b>x_c){
			arc_p[0]=0.5; arc_p[1]=1.5; arc_p[2]=-0.5; arc_p[3]=0.5;
			arc_c_x1 = x_c+arc_r; arc_c_x2 = x_b - x_t - arc_r;
		    }else{
			arc_p[0]=-0.5; arc_p[1]=0.5; arc_p[2]=0.5; arc_p[3]=1.5;
			arc_c_x1 = x_c-arc_r; arc_c_x2 = x_b - x_t + arc_r;
		    }
		    
		}else{
		    // 縦軸(Y軸)長い
		    arc_r = Math.ceil( x_t_abs );

		    if(y_b>y_c){
			arc_p[0]=1; arc_p[1]=2; arc_p[2]=0; arc_p[3]=1;
			arc_c_y1 = y_c+arc_r; arc_c_y2 = y_b - y_t - arc_r;
		    }else{
			arc_p[0]=0; arc_p[1]=1; arc_p[2]=1; arc_p[3]=2;
		        arc_c_y1 = y_c-arc_r; arc_c_y2 = y_b - y_t + arc_r;
		    }
		}
		
		ctx_edt.beginPath();
		ctx_edt.arc( arc_c_x1 , arc_c_y1 , arc_r , arc_p[0] * Math.PI , arc_p[1] * Math.PI , false) ;
		ctx_edt.arc( arc_c_x2 , arc_c_y2 , arc_r , arc_p[2] * Math.PI , arc_p[3] * Math.PI , false) ;
		ctx_edt.closePath();
		if(selected_shape==5){ ctx_edt.fill(); }else if(selected_shape==4){ ctx_edt.stroke(); }

	    // ◆長方形輪郭(対角2ヶ所)
	    }else if(selected_shape==6){
		ctx_edt.beginPath() ; ctx_edt.strokeRect( x_b , y_b, x_t , y_t );

	    // ◆長方形塗り潰し(対角2ヶ所)
	    }else if(selected_shape==7){
		ctx_edt.fillRect( x_b , y_b, x_t , y_t );

	    // ◆楕円(対角2ヶ所)
	    }else if(selected_shape==8 || selected_shape==9 ){
		draw_ellipse( x_b , y_b, x_t , y_t , selected_shape - 8 );

	    }
	}


	// ◆楕円(対角2ヶ所)
	function draw_ellipse( ps_x , ps_y , w , h , fff ){
	    var c_x = ps_x + w / 2 , c_y = ps_y + h / 2 ;
	    var m_x = c_x - w / 2 , s_x = c_x + w / 2 , u_y = c_y - h / 2 , e_y = c_y + h / 2 ;
	    var elp_eff = 0.551784; var x_elp = elp_eff * w / 2; var y_elp = elp_eff * h / 2;
	
	    ctx_edt.beginPath();
	    ctx_edt.moveTo( c_x , u_y );
	    ctx_edt.bezierCurveTo( c_x + x_elp , u_y , s_x , c_y - y_elp , s_x , c_y );
	    ctx_edt.bezierCurveTo( s_x , c_y + y_elp , c_x + x_elp , e_y , c_x , e_y );
	    ctx_edt.bezierCurveTo( c_x - x_elp , e_y , m_x , c_y + y_elp , m_x , c_y );
	    ctx_edt.bezierCurveTo( m_x , c_y - y_elp , c_x - x_elp , u_y , c_x , u_y );
	    ctx_edt.closePath();
	    if( fff==0 ){ ctx_edt.stroke(); } else { ctx_edt.fill(); }
	}

	// 2点間直線
	function line_p_p(ps_x,ps_y,pe_x,pe_y){
	    ctx_edt.beginPath() ; ctx_edt.moveTo(ps_x,ps_y) ; ctx_edt.lineTo(pe_x,pe_y) ; ctx_edt.stroke() ;
	}

	// CVS_edt 初期化
	function cvs_edt_init(){
	    ctx_edt.globalAlpha = 1 ; 
	    ctx_edt.lineWidth = 1 ;
	    ctx_edt.strokeStyle = "#000000" ;
	    ctx_edt.clearRect( 0 , 0 , cvs_wdt , cvs_hgt ) ;
	    ctx_edt.strokeRect( 0 , 0 , cvs_wdt , cvs_hgt ) ;
	}

	// CVS_drw 初期化
	function cvs_drw_init(){
	    ctx_drw.globalAlpha = 1 ; 
	    ctx_drw.lineWidth = 1 ;

	    ctx_drw.clearRect( 0 , 0 , cvs_wdt , cvs_hgt ) ;
	    ctx_drw.strokeRect( 0 , 0 , cvs_wdt , cvs_hgt ) ;
	}

	// 描画色変更
	function chng_col(gColor){ 
	    ctx_edt.strokeStyle = gColor ;
	    document.getElementById("select-color").style.backgroundColor = gColor ;
	}

	// 描画形状変更
	function chng_shape(shp_num){ 
	    var elm_tmp = document.getElementById("select-shape") ;
	    var elm_inp = elm_tmp.getElementsByTagName("input") ;
	    for(var i=0 ; elm_inp.length > i ; ++i){
		if(i==shp_num){
		    //elm_inp[i].style.backgroundColor = "#ffd700" ;
		    elm_inp[i].style.borderColor = "#ff0000" ;
		} else {
		    //elm_inp[i].style.backgroundColor = "#d3d3d3" ;
		    elm_inp[i].style.borderColor = "#000000" ;
		}
	    }
	    selected_shape = shp_num ;
	}

	// CANVAS 転送
	function CVS_Transfer(){
	    var img_edt = new Image ;
	    img_edt.src = ctx_edt.canvas.toDataURL() ;
	    img_edt.onload = function(){
		ctx_drw.drawImage(img_edt,0,0) ;
	    	cvs_edt_init();
	    }
	}

    </script>
</head>

<body onLoad="draw_begin()">
    <TABLE><TR>
    <TD><input type="button" value="消去" id="ClearCVS" onclick="cvs_drw_init()"></TD>
    <TD style="width:10px;"></TD>
    <TD id="select-shape">
	<input type="button" value="/" onclick="chng_shape(0)">
	<input type="button" value="→" onclick="chng_shape(1)">
	<input type="button" value="♂" onclick="chng_shape(2)">
	<input type="button" value=" ~ " onclick="chng_shape(3)">

	<input type="button" value="○" onclick="chng_shape(4)">
	<input type="button" value="●" onclick="chng_shape(5)">
	<input type="button" value="□" onclick="chng_shape(6)">
	<input type="button" value="■" onclick="chng_shape(7)">
	<input type="button" value="楕" onclick="chng_shape(8)" style="background-color:#f0f0f0;">
	<input type="button" value="楕" onclick="chng_shape(9)" style="background-color:#707070;">
    </TD>
    <TD style="width:5px;"></TD>
    <TD>
	<input type="button" style="background-color:#000000;" onclick="chng_col('#000000')">
	<input type="button" style="background-color:#008000;" onclick="chng_col('#008000')">
	<input type="button" style="background-color:#00ff00;" onclick="chng_col('#00ff00')">
	<input type="button" style="background-color:#0000ff;" onclick="chng_col('#0000ff')">
	<input type="button" style="background-color:#00ffff;" onclick="chng_col('#00ffff')">
	<input type="button" style="background-color:#fffacf;" onclick="chng_col('#fffacf')">
	<input type="button" style="background-color:#ffff00;" onclick="chng_col('#ffff00')">
	<input type="button" style="background-color:#ffa500;" onclick="chng_col('#ffa500')">
	<input type="button" style="background-color:#ff00ff;" onclick="chng_col('#ff00ff')">
	<input type="button" style="background-color:#ff0000;" onclick="chng_col('#ff0000')">
    </TD>
    <TD style="width:5px;"></TD>
    <TD id="select-color">
	<select id="s_transparency" style="height:28px; text-align:center; font-size:16px; margin:0 3px;">
	<option value="100">100</option><option value="80">80</option><option value="60">60</option>
	<option value="40">40</option><option value="20">20</option></select>

	<input type="number" id="l_thickness" min="1" max="500" value="3" style="height:23px; width:50px; text-align:center; font-size:16px; margin:0 3px;">
    </TD>
    
    </TR></TABLE>

    <div id="cvs-layer">
	<canvas id="CANVAS_DRW"></canvas>
	<canvas id="CANVAS_EDT"></canvas>
    </div>

</body> 

</html>
転送・重ね書きの目的

先回、2つのCANVASを用意し、1つのCANVASには途中の状態を表示し、形状確定時にもう1つのCANVASにデータを転送・重ね書きする確認を行いました。その目的について少し整理しておきます。矢印を描画する際に始点と終点を指定しますが、下の図はマウスムーブイベントが発生した際の途中の形状を消さずに残したものです。テストプログラムでは、ムーブイベント発生時に都度CANVASをクリアし書き直しているので、最新の途中の状態だけを確認していますが、実際には下図の通り1つの定形形状を描く過程で何回も描画を行っています。
今回のプログラムでは行番98が、CANVASをクリアする命令ですので、この行を削除もしくはコメントアウトすれば下記の様な描画結果になります。
(→クリアしないテストプログラムにリンク
一方、CANVAS全体をクリアすると、これまで書いた他の画像も消してしまうことになりますので、今回は2つのCANVASを用意し形状確定時に転送・重ね書きすることとしています。
形状確定時にもう一つのCANVASに改めて描画する方法もあると思いますが、転送方式で今のところ問題なさそうなので確認していません。(デバイスコンテキストを関数の引数にして、描画先を変えれば、同じ関数で対応できる様にも思いましたが、少し面倒なのでとりあえず保留にしています。)

まとめ

次回は描画内容を消す方法やCANVASへの文字出力についても調べてみたいと思います。また、近いうちにもう一つCANVASを重ねて、一番下のCANVASに写真を描画し、あたかも写真に描画している様な機能を追加してみようと思っています。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です