CSSによる拡大表示と描画

 

概 要

今回はCANVAS上で拡大した写真に描画するテストを行ってみます。過去2回の投稿で、scale() や drawImage() を使って拡大処理しましたが、今回はdrawImage() で、CANVAS倍率を変えず描画し、CSSでCANVAS要素の幅(width),高さ(height)指定し拡大表示する方法を試みます。
外観のズームだけであれば、drawImage() を使って描画する際に、ソース画像とターゲット画像の比率によって拡大・縮小表示することが可能でしたが、例えば倍率が異なるCANVAS上に線を描く際に線の太さをそれぞれどの様に設定すれば良いのかという課題に直面しました。単純に表示倍率に比例して設定することも考えましたが、実際のCANVAS解像度とは別にCSSで表示上の解像度を設定できることを知り確認することにしました。

結 果

→サンプルプログラムへのリンク
次の写真は今回の確認の結果を示すものです。写真左は倍率1倍で動物の顔の周りに円を描画したものです。写真右は顔の周りの円を描画後、拡大し動物の鼻の周りに円を描画したものです。拡大前後でも線の太さが等しいことが判ります。
どちらの写真もCANVASの解像度(粗さ)は等しく、CCS設定により拡大表示しているので、描画時の線の太さ設定を変更することなく、同じ太さの線を描画することが出来ました。


次の写真は、CCS設定による拡大表示(写真左)とdrawImage()の元画像と描画画像のサイズ比率変更による拡大表示(写真右)の画像を比較したものです。比較すると写真右の方がシャープ感があり、左は少しぼやけて感じますが、あくまで一時的な表示画像であり、プログラムの容易性を考量すると、個人的にはCCS設定による拡大の方で十分と思っています。
ただ必要性があれば、写真と描画を行うCANVASはレイヤー構成となっていて、異なるCANVAS上に描画していますので、写真を描画するCANVASだけは、drawImage() を使って外観を改善することは可能かもしれません。

CANVAS 構成

今回のサンプルプログラムでは4つのCANVASを使用します。過去の描画のサンプルプログラムでも3つのCANVASを使っていました。今回追加したのは、全領域の描画画像の情報を記録する “④CANVAS_TMP ”です。スライダーや領域選択によるズーム処理時に拡大画面の描画内容は②CANVAS_DRWに追加表示しますが、同時にここに書かれた内容を全体として、④CANVAS_TMPにも合成保存します。更に④CANVAS_TMPは描画やズーム処理毎に、DATA URI 変換し、再び必要部分を②CANVAS_DRWに更新表示します。この一連のサイクルによってズーム画像最新化と全体画像を維持します。尚、④CANVAS_TMPは③CANVAS_PICの背面に位置し、直接見ることはありません。

①CANVAS_EDTは過去の描画の投稿でも書いていますが、マウス操作時の途中形状やズーム処理時の選択領域等を表示する為に使用します。③CANVAS_PICは写真出力に使用します。ページ読み込み時に写真はDATA URI変換しています。ズーム処理時、このDATA URIから選択領域に対応する画像を倍率1倍で出力します。なお、上図では①②③は④に対して小さく見えますが、CCS設定により外観上  拡大されて同じサイズになります。

プログラミングメモ

→サンプルプログラムへのリンク
(1)機能
倍率を1倍に戻す“初期化”ボタン,倍率を1~10倍に変更する“スライダー”,“描画/ズーム” 切替ボタン,画像を出力するCANVAS領域(4つのCANVASが上下に配置)で構成します。
“描画/ズーム” 切替ボタンでは、クリック毎に“描画”と“ズーム”のモードが入れ替わります。
“描画”モードではマウス操作でCANVAS上2点を指定し、2点を対角とする長方形に収まる円を描画します。線種・線色・太さ・透明度等は固定です。 “ズーム” モードでは、同様にマウスを使って、領域選択するとCANVAS表示領域縦横比にあわせて、領域補正しCANVASに出力します。

(2)プログラミングメモ
今回の確認ポイントは、描画時に拡大・縮小処理を行わず、CCS設定により表示拡大を自動で行うことです。
実際のプログラム内では、ページ呼び出し直後、行番64~67で4つのCANVASにCCSのwidth , height を設定しています。ここでは直前の行番56で表示する写真サイズを取得し、縦横比率が同じになる様に 求めた cvs_wdt , cvs_hgt を設定しています。
行番59~62でCANVASオブジェクト自体の width , height に同じ設定をしますので、初期表示は1倍になります。

次に“ズーム”時の処理についてメモします。切替ボタンをクリックすると、行番219~228で変数“op_mode”の値を変更します。この値に関連する処理時に条件分岐を行います。ズーム処理の領域選択中の“mouse_move”イベント発生時には行番120~124を実行し、領域外周枠を点線で表示します。領域選択完了の“mouse_up” イベント発生時には、行番147で関数(pic_zoom)を呼びます。
関数(pic_zoom)は、前の投稿のズーム処理内容と大きく違いませんが、今回、CSSを利用対応として、行番199~201でCANVASサイズを選択領域に合わせ再設定しています。また、行番203,206で選択領域画像をCANVASへ描画する際も元画像サイズと描画サイズを同じ(1倍)にして描画します。
仮にCCS設定サイズが(840,600)で描画サイズを(420,300)とすると拡大され外観は2倍になります。(面積は4倍)

最後に描画についてメモします。マウスイベントで取得する座標はCCS設定した画像サイズに対応している様ですので、行番110~113で座標をCANVAS座標に変換しています。
行番128~134では変換座標を対角とする長方形に収まる円を編集用CANVAS_DRWに仮描画(行番134)します。“mouse_up” イベントが発生し、形状が確定すると行番156で全領域画像を記録する CANVAS_TMPに元画像を消さずに円を重ね書き(※1)します。(※1:以後、CANVAS合成と呼びます。)
行番134では拡大領域内のCANVAS座標を指定しますが、行番156で、CANVAS合成する時には、全領域に対する拡大領域の位置座標を加算して指定します。この様な処理時も全領域,拡大領域とも1倍ですので、感覚的に理解しやすいというメリットを感じています。
全領域画像を記録する CANVAS_TMP更新後、行番158でデータURI変換、行番160~165で編集用CANVAS_DRWを更新します。

<!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 ; }
	#draw_info th, #draw_info td { border: solid 1px #000000; border-collapse: collapse; }
	#draw_info { border: solid 1px #000000; border-collapse: collapse; }
	input { height:30px; }
    </style>

    <script>

	var cvs_pic ; var ctx_pic ;
	var cvs_drw ; var ctx_drw ;
	var cvs_edt ; var ctx_edt ;
	var cvs_tmp ; var ctx_tmp ;

	var cvs_wdt ; var cvs_hgt ; var img_h ; var img_w ;
	var zos_w ; var zom_w ; var zos_h ; var zom_h ; 
	var x_b=0; var y_b=0; var x_c=0; var y_c=0; var p_c_x=0; var p_c_y=0;
	var f_b ; var a_effect ; var a_efc_max=10;
	var sld_chk ; var tmp_pic ; var tmp_drw ;
        var IE_Flag ;
	var op_mode = "md_zoom" ;
	var x_b_h , y_b_h , x_c_h , y_c_h ;

	function draw_begin(){
	    var ua, isIE;
	    ua = window.navigator.userAgent.toLowerCase();
	    isIE = (ua.indexOf('msie') >= 0 || ua.indexOf('trident') >= 0);
 
	    cvs_wdt = 840 ; cvs_hgt = 600 ;
	    cvs_pic = document.getElementById('CANVAS_PIC') ; 
	    ctx_pic = cvs_pic.getContext("2d") ;
	    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_tmp = document.getElementById('CANVAS_TMP') ; 
	    ctx_tmp = cvs_tmp.getContext("2d") ;
	    
	    sld_chk = document.getElementById("zoom-slider");
	    sld_chk.min = 1; sld_chk.max = a_efc_max; sld_chk.step = 'any';
	    sld_chk.value=1;

	    var image = new Image(); 
            image.src = "./sample.jpg" ;

	    image.onload = function () {
		img_h = image.height ; img_w = image.width ;

		cvs_hgt = Math.floor(img_h * cvs_wdt / img_w) ;
                cvs_pic.width = cvs_wdt ; cvs_pic.height = cvs_hgt ;
		cvs_drw.width = cvs_wdt ; cvs_drw.height = cvs_hgt ;
		cvs_edt.width = cvs_wdt ; cvs_edt.height = cvs_hgt ;
		cvs_tmp.width = cvs_wdt ; cvs_tmp.height = cvs_hgt ;

		cvs_pic.style.width = cvs_wdt + "px" ; cvs_pic.style.height = cvs_hgt + "px" ;
		cvs_drw.style.width = cvs_wdt + "px" ; cvs_drw.style.height = cvs_hgt + "px" ;
		cvs_edt.style.width = cvs_wdt + "px" ; cvs_edt.style.height = cvs_hgt + "px" ;
		cvs_tmp.style.width = cvs_wdt + "px" ; cvs_tmp.style.height = cvs_hgt + "px" ;
                ctx_pic.drawImage( image, 0 , 0 , cvs_wdt , cvs_hgt ) ;
		ctx_pic.strokeRect( 0 , 0 , cvs_wdt, cvs_hgt) ;

		//ctx_tmp.drawImage( image, 0 , 0 , cvs_wdt , cvs_hgt ) ;
		ctx_tmp.strokeRect( 0 , 0 , cvs_wdt, cvs_hgt) ;

		//始点(X,Y),幅・高さ初期化
		zos_w = 0 ; zos_h = 0 ; zom_w = cvs_wdt ; zom_h = cvs_hgt ;

		tmp_pic = new Image ; tmp_pic.src = ctx_pic.canvas.toDataURL() ;
		tmp_drw = new Image ; tmp_drw.src = ctx_tmp.canvas.toDataURL() ;

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

		if(isIE){
		    sld_chk.addEventListener( "change", pict_scale , false ) ;
		}else{
		    sld_chk.addEventListener( "input", pict_scale , false ) ;
		}
	    }
	}

	//▼▼▼▼▼▼ マウス検知 ▼▼▼▼▼▼

	// マウスダウンイベント
	function mouse_dn(event){
	    var rect = event.target.getBoundingClientRect() ;
	    x_b = event.clientX-rect.left ;
	    y_b = event.clientY-rect.top ;
	    f_b = true ;
	}

	// マウスムーブイベント
	function mouse_mv(event){
	    if(f_b){
		var rect = event.target.getBoundingClientRect() ;
		x_c = event.clientX-rect.left ;
		y_c = event.clientY-rect.top ;

		// 座標補正
		x_b_h = zom_w * x_b / cvs_wdt ;						// 拡大考慮 座標補正(X)
		y_b_h = zom_h * y_b / cvs_hgt ;						// 拡大考慮 座標補正(Y)
		x_c_h = zom_w * x_c / cvs_wdt ;						// 拡大考慮 座標補正(X)
		y_c_h = zom_h * y_c / cvs_hgt ;						// 拡大考慮 座標補正(Y)

		// 選択エリア確認用表示
		ctx_edt.clearRect( 0, 0, cvs_wdt , cvs_hgt ) ;
		ctx_edt.beginPath();

		if(op_mode == "md_zoom"){
		    // 拡大範囲(長方形)を点線表示
		    ctx_edt.globalAlpha = 1 ; ctx_edt.strokeStyle = "#000000" ;
		    ctx_edt.lineWidth = 1 ; ctx_edt.setLineDash([2,2]);

		    ctx_edt.strokeRect( x_b_h , y_b_h, x_c_h-x_b_h , y_c_h-y_b_h );

		}else{
		    // 対角に収まる円を描画
		    ctx_edt.globalAlpha = 0.3 ; ctx_edt.strokeStyle = "#FF0000" ;
		    ctx_edt.lineWidth = 8 ; ctx_edt.setLineDash([]);

		    var x_l_h = Math.abs(x_b_h-x_c_h) ; var y_l_h = Math.abs(y_b_h-y_c_h);
		    var arc_r_h = x_l_h; if(x_l_h>y_l_h){ var arc_r_h = y_l_h; }

		    ctx_edt.arc( (x_b_h + x_c_h)/2 , (y_b_h + y_c_h)/2 , arc_r_h / 2 , 0 , 2 * Math.PI , false) ;

		}

		ctx_edt.stroke();
	    }
	}

	// マウスアップイベント
	function mouse_up(event){ 
	    f_b=false;

	    if(op_mode == "md_zoom"){
		pic_zoom("1") ;
		ctx_edt.clearRect( 0, 0, cvs_wdt , cvs_hgt ) ;
	    }else{
		ctx_tmp.globalAlpha = 0.3 ; ctx_tmp.strokeStyle = "#FF0000" ;
		ctx_tmp.lineWidth = 8 ; ctx_tmp.setLineDash([]);
		
		var x_l_h = Math.abs(x_b_h-x_c_h) ; var y_l_h = Math.abs(y_b_h-y_c_h);
		var arc_r_h = x_l_h; if(x_l_h>y_l_h){ var arc_r_h = y_l_h; }
		ctx_tmp.beginPath();
		ctx_tmp.arc( zos_w + (x_b_h + x_c_h)/2 ,  zos_h + (y_b_h + y_c_h)/2 , arc_r_h / 2 , 0 , 2 * Math.PI , false) ;
		ctx_tmp.stroke();
		tmp_drw.src = ctx_tmp.canvas.toDataURL() ;

		tmp_drw.onload = function () {
		    ctx_edt.clearRect( 0, 0, cvs_wdt , cvs_hgt ) ;
		    ctx_drw.clearRect( 0, 0, cvs_wdt , cvs_hgt ) ;
		    ctx_drw.drawImage(tmp_drw , zos_w, zos_h , zom_w , zom_h , 0 , 0 , zom_w , zom_h ) ;
	    	    ctx_drw.strokeRect( 0 , 0 , cvs_pic.width , cvs_pic.height ) ;
		}
	    }
	}

	//▲▲▲▲▲▲ マウス検知 ▲▲▲▲▲▲


	// ズーム表示メイン
	function pic_zoom(ptn){
	    if(ptn!="1"){ p_c_x = cvs_wdt/2 ; p_c_y = cvs_hgt/2 ; }			// 対象:範囲選択以外(初期化+スライダー)
	    else{
	    	var tmp_cnt_x = (x_b + x_c) / 2 ; var tmp_cnt_y = (y_b + y_c) / 2 ;	// 選択領域の中心座標
		p_c_x = zos_w + zom_w * tmp_cnt_x / cvs_wdt ;				// 拡大考慮 座標補正(X)
		p_c_y = zos_h + zom_h * tmp_cnt_y / cvs_hgt ;				// 拡大考慮 座標補正(Y)

		var tmp_wdt = zom_w * Math.abs(x_b - x_c) / cvs_wdt ;			// 拡大考慮 幅補正
		var tmp_hgt = zom_h * Math.abs(y_b - y_c) / cvs_hgt ;			// 拡大考慮 高さ補正

	    	var efc_x = Math.floor(100 * cvs_wdt / tmp_wdt ) / 100 ; 		// 幅方向倍率計算
	    	var efc_y = Math.floor(100 * cvs_hgt / tmp_hgt ) / 100 ; 		// 高さ方向倍率計算
	    	if(efc_x>efc_y){ a_effect=efc_y ; }else{ a_effect=efc_x ; }		// 倍率の小さい方を確定倍率とする
	    	if(a_effect > a_efc_max){ a_effect = a_efc_max ; }			// 倍率最大値補正
	    }

	    ctx_pic.globalAlpha=1; ctx_pic.lineWidth=1; ctx_pic.strokeStyle ="#000000"; // 描画関連設定

	    zom_w = cvs_wdt/a_effect ; zom_h = cvs_hgt/a_effect ; 			// 拡大幅,拡大高さ
	    zos_w = p_c_x-zom_w/2 ; zos_h = p_c_y-zom_h/2;				// 始点X,始点Y

	    if(0>zos_w){zos_w=0;}							// 開始位置補正(X-)
	    if(0>zos_h){zos_h=0;}							// 開始位置補正(Y-)
	    if(zos_w+zom_w > cvs_wdt){zos_w=cvs_wdt-zom_w;}				// 開始位置補正(X+)
	    if(zos_h+zom_h > cvs_hgt){zos_h=cvs_hgt-zom_h;}				// 開始位置補正(Y+)

	    cvs_pic.width = zom_w ; cvs_pic.height = zom_h ;				// CANVASサイズ再設定
	    cvs_drw.width = zom_w ; cvs_drw.height = zom_h ;				// CANVASサイズ再設定
	    cvs_edt.width = zom_w ; cvs_edt.height = zom_h ;				// CANVASサイズ再設定

	    ctx_pic.drawImage(tmp_pic,zos_w,zos_h,zom_w,zom_h,0,0,zom_w,zom_h) ;	// 写真拡大描画
	    ctx_pic.strokeRect( 0 , 0 , cvs_pic.width , cvs_pic.height ) ;

	    ctx_drw.drawImage(tmp_drw,zos_w,zos_h,zom_w,zom_h,0,0,zom_w,zom_h ) ;	// 書き込み拡大再描画
	    ctx_drw.strokeRect( 0 , 0 , cvs_pic.width , cvs_pic.height ) ;

	    if(ptn!="0"){document.getElementById("zoom-slider").value=a_effect ; }	// 対象:初期化ボタンと範囲選択

	}

	// ズームスライダー
	function pict_scale(event){ a_effect = event.target.value; pic_zoom("0") ; }

	// 初期化ボタン
	function screen_init(){ a_effect = 1; pic_zoom("2") ; }

	// ズーム・描画モード切り換え
	function mode_chng(){
	    if(op_mode != "md_zoom"){
		op_mode = "md_zoom";
		document.getElementById('chng_mode').value="ズーム";
	    }else{
		op_mode = "md_draw";
		document.getElementById('chng_mode').value="描画";
	    }
	}

    </script>
</head>

<body onLoad="draw_begin()">

    <TABLE><TR>
    <TD><input type="button" value="初期化" onclick="screen_init()"></TD><TD style="width:50px;"></TD>
    <TD>縮小</TD><TD><input id="zoom-slider" type="range"></TD><TD>拡大</TD>
    <TD style="width:60px;"></TD>
    <TD><input id="chng_mode" type="button" value="ズーム" onclick="mode_chng()"></TD>
    </TR></TABLE>

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

</body> 

</html>

 

まとめ

描画機能の拡充は必要ですが、基本機能としてはだいたいイメージしていた感じになってきたように思います。
今後の予定は現時点で未定ですが、スマホのタッチ操作対応として、Hammer.js なるものを勉強してみようかなとも思っています。また、VPS(仮想レンタルサーバー)借りて、Linuxサーバー立ち上げの勉強してみようかなとも思っています。
同時にいろいろ出来ないので、少し考えてみます。

コメントを残す

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