【JavaScript】 マウス操作でCanvasを拡大/縮小する
更新日:2023/01/30
今回はJavaScriptを使用して、Canvasのある領域を、現在の表示エリアに拡大して表示する方法をお伝えします。
完成形のデモ
マウスをクリックしながら移動し、離したときの範囲を拡大表示します。
ダブルクリックするとリセットされます。
キャンバスを拡大表示する方法
JavaScriptのCanvasAPIには、scale()という座標を拡大/縮小するメソッドがあります。
これを使用することで画面の拡大表示ができますが、save()メソッドを使用するとリセットできなくなります。
そのため、その他の座標変換系メソッドや線種などのセット系メソッドの利用が非常に難しくなります。
そこでCanvasAPIではなく、スタイルシートの設定で拡大/縮小をおこなうのが一番簡単です。
基本的には、次のコードで動作します。
HTML
<div id="canvas_warp" >
<canvas id="canvas" height="350" width="600"></canvas>
</div>
CSS
#canvas_warp,#canvas{
max-width:100%;
box-sizing: content-box;
padding: 0;margin: 0;
}
#canvas_warp{
position: relative;
width:600px;
max-width: 100%;
overflow: scroll;
}
#canvas_warp:before{
content:"";
display: block;
padding-top: 58%;
}
#canvas{
position: absolute;
left:0;
top:0;
border: 0;
transform-origin: 0 0;
}
cssやhtmlに関しては、レイヤーを考慮したものになっています。
詳しくは【JavaScript】 Canvasでレイヤーを表現するを見てください。
拡大/縮小に関して重要なのが、次のcssです。
#canvas_warp{
・・・省略
overflow: scroll;
}
#canvas{
・・・省略
transform-origin: 0 0;
}
overflow: scrollは、拡大した結果はみ出た部分を、スクロールで表示するように指定しています。
スクロールが必要ないときは、hidden(隠す)でも問題ありません。
transform-origin: 0 0は、拡大の原点をキャンバスの左上にセットしています。
これがないと、うまく動きません。
JavaScript
// 文字列組み立て用オブジェクト
const transformRaw = { raw : ["scale(",")"] };
/*
* キャンバス拡大/縮小関数
* sx , sy : 拡大/縮小エリアの左上
* zWidth , zWidth : 拡大エリアの幅・高さ
* div : 外側のdiv要素
* canvas : 拡大/縮小させるキャンバス
*/
const zoom = ( sx , sy , zWidth , zHeight , div , canvas ) => {
// 描画バッファサイズと拡大/縮小エリアの比率を求める
const [sw,sh] = [ canvas.width / zWidth , canvas.height / zHeight ];
// 縦横の比率で大きい方を拡大率として採用
const scale = Math.max( sw,sh );
// スタイルtransform にscaleをセット
canvas.style.transform = String.raw( transformRaw ,scale.toString());
// div要素のスクロール位置をセット
div.scrollTop = sy * scale;
div.scrollLeft = sx * scale ;
};
上のコードは、キャンバスのスタイル属性のtransformに、拡大率をセットしています。
次に、div要素のスクロール位置を変更しています。
デモコード
最初に紹介したデモのコードです。
HTML
<div id="canvas_warp" >
<canvas id="canvas2" height="350" width="600"></canvas>
<canvas id="canvas3" height="350" width="600"></canvas>
</div>
CSS
#canvas_warp,#canvas2,#canvas3{
max-width:100%;
box-sizing: content-box;
padding: 0;margin: 0;
}
#canvas_warp{
position: relative;
width:600px;
max-width: 100%;
overflow: hidden;
}
#canvas_warp:before{
content:"";
display: block;
padding-top: 58%;
}
#canvas2,#canvas3{
position: absolute;
left:0;
top:0;
border: 0;
transform-origin: 0 0;
}
#canvas2{ z-index: 0}
#canvas3{ z-index: 1}
JavaScript
(()=>{
// 拡大領域指定線色
const pathObjStrokeColor = "rgba( 0, 0, 255, .5)";
/****************************
* キャンバスの外側のDiv要素
****************************/
const wrapDiv = id => {
const div = document.getElementById( id );
const canvasList = [];
const transformRaw = { raw : ["scale(",")"] };
const funcs = {
// 拡大/縮小メソッド
zoom : ( sx , sy , zWidth , zHeight , canvas ) => {
const [sw,sh] = [ canvas.width / zWidth , canvas.height / zHeight ];
const scale = Math.max( sw,sh );
const transText = String.raw( transformRaw ,scale.toString());
canvasList.forEach(
e => {
e.style.transform = transText;
}
);
div.scrollTop = sy * scale;
div.scrollLeft = sx * scale ;
console.log( div.scrollHeight , div.scrollTop);
},
// 拡大/縮小をリセットするメソッド
reset : () =>{
canvasList.forEach(
e => {
e.style.transform = "scale(1)" ;
});
div.scrollTop = 0;
div.scrollLeft = 0;
},
// キャンバス上でクリックされた座標を、描画バッファ上の座標に変換
getPoint : ( e , canvas ) => {
const rect = e.target.getBoundingClientRect();
const [w,h] = [canvas.width / div.scrollWidth , canvas.height / div.scrollHeight];
return [ (e.clientX - rect.left) * w ,(e.clientY - rect.top)*h];
}
};
return {
set backgroundCanvas( id ){
canvasList.push( backgroundCanvas( id ) );
},
set eventCanvas( id ){
canvasList.push( eventCanvas( id , funcs) );
}
}
};
/****************************
* 背景画像を表示するキャンバスに対する処理
****************************/
const backgroundCanvas = id => {
const cvs = document.getElementById( id );
const context = cvs.getContext("2d");
onloadImage( ["https://xxxx.com/xxxx.jpg"] ,
(index,img) => {
context.drawImage( img , 0 , 0 , img.naturalWidth , img.naturalHeight ,
0 , 0 , cvs.width , cvs.height);
debugGrid(cvs);
} );
return cvs;
};
/****************************
* イベントを受け取るキャンバスの処理
****************************/
const eventCanvas = ( id , funcs ) => {
const canvas = document.getElementById( id );
const ctx = canvas.getContext("2d");
ctx.strokeStyle = pathObjStrokeColor;
ctx.lineWidth = 3;
const clearRect = () =>
ctx.clearRect(0,0,canvas.width,canvas.height);
// 2点(x,y)-(current.x,current.y)から左上点と幅・高さを求める
const getRect = ( x , y ) =>{
const [sx,w] = x < current.x ? [ x , current.x - x] : [ current.x , x - current.x];
const [sy,h] = y < current.y ? [ y , current.y - y] : [ current.y , y - current.y];
return [ sx , sy , w , h ];
};
const boxDraw = ( x , y ) =>{
clearRect();
ctx.strokeRect( ...getRect( x , y ) );
};
const current = { x: 0 , y :0 , moveMode:false};
const events = {
mousedown : e => {
const [x,y] = funcs.getPoint(e,canvas);
console.log( "click",x ,y );
current.x = x; current.y = y; current.moveMode = true;
},
mousemove : e => {
if( !current.moveMode ) return;
boxDraw( ...funcs.getPoint(e,canvas));
},
mouseup : e => {
if( !current.moveMode ) return;
current.moveMode=false;
const [x,y] = funcs.getPoint(e,canvas);
if( Math.abs(x - current.x) < 5 && Math.abs(y - current.y) < 5 ) return;
clearRect();
funcs.zoom( ...getRect(x,y) , canvas );
},
dblclick : e => {
funcs.reset();
}
};
Object.keys(events).forEach(
e=>canvas.addEventListener( e , events[e] , false)
);
return canvas;
};
/****************************
* 画像の読み込み
* DOMが構築されるのを待つ
*****************************/
window.addEventListener( "DOMContentLoaded" , ()=> {
const wDiv = wrapDiv("canvas_warp");
wDiv.backgroundCanvas = "canvas2";
wDiv.eventCanvas = "canvas3";
});
/****************************
* 画像の読み込み
* 【JavaScript】 画像を動的に読み込んでhtml(DOM)やCanvasで使用するより
*****************************/
const onloadImage = ( imageUrls , callBack = ()=>null , errorCallBack = ()=>null ) => {
const stat = { wait:"wait", ok : "ok" , error : "error" };
const im = imageUrls.map( e => ( { image : new Image() , stat : stat.wait } ) );
im.forEach( ( e , i ) => {
e.image.onload = () => {
e.image.onload = e.image.onerror = null;
if( e.stat === stat.wait) { e.stat = stat.ok; callBack( i , e.image );}
};
e.image.onerror = function(){ e.stat = stat.error;errorCallBack( i );};
e.image.src = imageUrls[i]
} );
return im;
};
// 機能確認用グリッド
const debugGrid = (canvas) =>{
const context = canvas.getContext( "2d" );
const lineDraw = ( x1 , y1 , x2 , y2 , count ) =>{
context.beginPath();
context.strokeStyle = count % 5 ===0 ? "aqua" : "white";
context.moveTo( x1 , y1 );
context.lineTo( x2 , y2 );
context.stroke();
};
for( let i = 0,j = 0 ; i < canvas.width ; i += 25 , j ++ ) lineDraw( i , 0 , i , 400 ,j );
for( let i = 0,j = 0 ; i < canvas.height ; i += 25 , j ++ ) lineDraw( 0 , i , 600 , i,j);
};
})();
デモコードの解説
上のコードは、キャンバスを2枚重ねて上段のキャンバスでイベントを受け取っています。
さらに上段のキャンバスに、拡大領域を示す青い枠を描画しています。
長いコードですが、それほど難しいことはやっていません。
更新日:2023/01/30
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。