CanvasDOM画像処理

【JavaScript】 Canvas上でクリックした線の色変更と移動

更新日:2020/05/30

 

デモ

線上をクリックすると色が変わるデモ

線の上をクリックすると、色が変わります。

線上をクリックして位置を移動できるデモ

線をクリックしたままマウスを移動すると、図形の位置を変更できます。
ダブルクリックすると、最上面に移動します。

 

JavaScriptコード

共通コード

引数として受け取ったコンテキストに、図形のパスを登録する関数の配列です。

JavaScript

            // 図形パスの定義関数を格納した配列
            const paths = [
                context => {
                    context.lineWidth = 10;
                    context.moveTo( 250 , 15 );
                    context.lineTo( 40 , 130  );
                    context.arcTo( 0 , 300 ,  300 , 300 , 100);
                    context.closePath();
                },
                context => {
                    context.lineWidth = 13;
                    context.rect( 10,10,200,100 );
                },
                context => {
                    context.lineWidth = 13;
                    context.moveTo( 150 , 150 );
                    context.quadraticCurveTo(220 , 110 , 220  , 70 );
                    context.bezierCurveTo(220 , 30 , 170  , 30 , 150 , 70);
                    context.bezierCurveTo(130 , 30 , 80  , 30 , 80 , 70);
                    context.quadraticCurveTo(80 , 110 , 150  , 150 );
                },
                context => {
                    context.lineWidth = 12;
                    context.arc( 250,120,70,0,2 * Math.PI );
                },
                context => {
                    context.lineWidth = 12;
                    context.moveTo( 50 , 5 );
                    context.lineTo( 50 , 250 );
                    context.lineTo( 150 , 5 );
                    context.closePath();
                }
            ];

線上をクリックすると色が変わるデモのコード

JavaScript

(()=>{

           /*****************************
             ここに共通コードが入ります
          *****************************/

                // 線色
            const pathObjStrokeColor = ["red","green","blue","black","pink","orange","purple","lightblue"];

            /*
            * 図形処理用オブジェクト
            */
            const pathObj = function ( callBack ) {

                this.callBack = callBack;   // パスを作成する関数
                            // 現在の色 ランダムで初期値を計算
                this.current = Math.floor(Math.random() * pathObjStrokeColor.length);

            };
            pathObj.prototype={
                    // 図形の描画
                draw( context ){
                    const ctx = this.drawData( context );

                    ctx.strokeStyle = pathObjStrokeColor[this.current];

                    ctx.stroke();
                    ctx.restore();
                },
                    // 線色を次の色に変更
                nextColor(){
                    this.current = ( this.current >= pathObjStrokeColor.length -1 ) ?
                        0 : this.current + 1;
                },

                    // 指定した座標が線上にあるか確認
                isIn( context , x , y ){
                    return  this.drawData( context ).isPointInStroke( x , y );
                },
                    // コンテキスト取得
                drawData( context ){

                    context.beginPath();
                    context.save();
                        // パスをセット
                    this.callBack( context );
                    return context;
                }
            };
                /*
                * 図形処理用オブジェクトのコンテナ
                */
            const pathList = function ( canvas ) {
                this.canvas = canvas;
                this.context = this.canvas.getContext("2d");
                this.path = [];
            };
            pathList.prototype={
                    // 図形処理用オブジェクト追加
                add( callBack ){
                    this.path.push( new pathObj( callBack) );
                },
                    // 全図形の描画
                draw(){

                    this.context.clearRect(0,0,this.canvas.width,this.canvas.height);
                    this.path.forEach( e => e.draw( this.context ));
                },
                    // 指定した座標を線上に持つ図形の検索
                inStroke( x , y ){
                        // 配列の最後=上階層の図形
                    for( let i = this.path.length -1 ; i >= 0 ; i -- ){
                        if( this.path[i].isIn( this.context , x , y ) ){
                            this.path[i].nextColor();
                            this.draw();
                            return;
                        }
                    }
                },
                    // キャンバス上でのクリックイベント処理
                clickEvent(){
                    this.canvas.addEventListener('click', e => {
                        const rect = e.target.getBoundingClientRect();
                        const [w,h] = [this.canvas.width / this.canvas.clientWidth , this.canvas.height / this.canvas.clientHeight];
                        const [x,y] = [ (e.clientX - rect.left) * w,(e.clientY - rect.top)*h];

                        this.inStroke(  x , y );
                    }, false);
                }
            };

                // DOMが構築されるのを待つ
            window.addEventListener( "DOMContentLoaded" , ()=> {
                const cvs = document.getElementById( "canvas" );

                const pathListObj = new pathList( cvs );

                paths.forEach( e=> pathListObj.add(e) );

                pathListObj.draw();

                pathListObj.clickEvent();

            });
        })();

線上をクリックして位置を移動できるデモのコード

HTML

<canvas id="canvas" height="500" width="600"></canvas>

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{ position: relative;border: 1px solid #ccc;width:600px;max-width: 100%;}
#canvas_warp:before{
            content:"";
            display: block;
            padding-top: 58%;
}
#canvas2,#canvas3{
            position: absolute;
            left:0;
            top:0;
            border: 0;
            max-width:100%;box-sizing: content-box;padding: 0;margin: 0;
}
#canvas2{ z-index: 0}
#canvas3{ z-index: 1}

JavaScript

(()=>{

           /*****************************
             ここに共通コードが入ります
          *****************************/

                // 線色
            const pathObjStrokeColor = "rgba( 0, 0, 255, .5)";

            /*
            * 図形処理用オブジェクト
            */
            const pathObj = function (  callBack ) {

                this.callBack = callBack;   // パスを作成する関数
                this.moveX = 0;
                this.moveY = 0;
            };
            pathObj.prototype={
                    // 図形の描画
                draw( context , effect = false , eX = 0 , eY = 0){
                    const ctx = this.drawData( context , eX , eY );

                    if( effect ) ctx.strokeStyle = pathObjStrokeColor;

                    context.stroke();
                    context.restore();
                },
                setPos( eX , eY ){
                    this.moveX += eX;this.moveY += eY;
                },
                isIn( context , x , y ){
                    const ctx = this.drawData( context , 0 , 0 );
                    const result = ctx.isPointInStroke( x - this.moveX , y - this.moveY);
                    ctx.restore();
                    return  result;
                },
                    // コンテキスト取得
                drawData( context ,  eX , eY){

                    context.save();
                    context.translate( this.moveX + eX , this.moveY + eY );

                    context.beginPath();

                        // パスをセット
                    this.callBack( context );
                    return context;
                }
            };
            /*
            * 図形処理用オブジェクトのコンテナ
            */
            const pathList = function ( canvas , effectCanvas ) {
                this.canvas = canvas;
                this.effectCanvas = effectCanvas;
                this.context = this.canvas.getContext("2d");
                this.effectContext = this.effectCanvas.getContext("2d");
                this.path = [];
                this.currentPath = null;
                this.moveMode = false;
                this.currentX = 0;
                this.currentY = 0;
            };
            pathList.prototype={
                    // 図形処理用オブジェクト追加
                add( callBack ){
                    this.path.push( new pathObj( callBack) );
                },
                    // 全図形の描画
                draw(){
                    this.context.clearRect(0,0,this.canvas.width,this.canvas.height);
                    this.path.forEach( e => e.draw( this.context ));
                },
                clearEffect(){
                    this.effectContext.clearRect(0,0,this.canvas.width,this.canvas.height);
                },
                drawEffect( x , y ){
                    this.clearEffect();
                    this.currentPath.draw(this.effectContext , true , x - this.currentX , y - this.currentY );
                },
                inStroke( x , y ){
                        // 配列の最後=上階層の図形
                    for( let i = this.path.length -1 ; i >= 0 ; i -- ){
                        if( this.path[i].isIn( this.context , x , y ) ) return i;
                    }
                    return -1;
                },
                getPoint( e ){
                    const rect = e.target.getBoundingClientRect();
                    const [w,h] = [this.canvas.width / this.canvas.clientWidth , this.canvas.height / this.canvas.clientHeight];
                    return [ (e.clientX - rect.left) * w,(e.clientY - rect.top)*h];
                },
                    // キャンバス上でのクリックイベント処理
                clickEvent(){
                        // マウスを押したときのイベント
                    this.effectCanvas.addEventListener('mousedown', e => {

                        const [x,y] = this.getPoint(e);

                        let index = this.inStroke( x , y );
                        console.log( index );
                        if( index  > -1 ){
                            this.currentPath = this.path[index];
                            this.currentX = x;
                            this.currentY = y;
                            this.moveMode = true;
                        }
                    }, false);
                        // マウスカーソルを移動したときのイベント
                    this.effectCanvas.addEventListener('mousemove', e => {

                        if( !this.moveMode ) return;

                        const [x,y] = this.getPoint(e);

                        this.drawEffect( x , y );
                    }, false);
                        // マウスボタンを離したときのイベント
                    this.effectCanvas.addEventListener('mouseup', e => {
                        if( !this.moveMode ) return;
                        this.moveMode=false;

                        const [x,y] = this.getPoint(e);
                        this.clearEffect();
                        this.currentPath.setPos( x - this.currentX,y - this.currentY);
                        this.currentPath=null;
                        this.draw();
                    }, false);
                        // マウスボタンをダブルクリックときのイベント
                    this.effectCanvas.addEventListener('dblclick', e => {

                        const [x,y] = this.getPoint(e);
                        let index = this.inStroke( x , y );
                        const current = this.path[index];

                        this.path = this.path.filter( (e,i) => i !== index);
                        this.path.push( current );

                        this.draw();
                    }, false);
                }
            };

                // DOMが構築されるのを待つ
            window.addEventListener( "DOMContentLoaded" , ()=> {
                const cvs = document.getElementById( "canvas2" );
                const cvs2 = document.getElementById( "canvas3" );

                const pathListObj = new pathList( cvs , cvs2 );

                paths.forEach( e=> pathListObj.add(e) );

                pathListObj.draw();

                pathListObj.clickEvent();

            });
})();

 

解説

クリックした座標が図形の線上にあるか確認

与えらえた座標が図形の図形の線上かどうかを確認するには、コンテキストが持っているisPointInStroke()メソッドを使用します。

コンテキストには、チェックをおこないたいパスがセットされている必要があります。
そのため、図形毎にオブジェクト(pathObj)を作成して、パスの作成とチェックをおこなっています。

また今回はクリックしやすいように太めの線で描画しています。
実際には1ピクセルの線もあるので、ここではやっていませんが、チェックするパスの線幅を一時的に太くして補正を効かせる必要があります。

パスの作成

パスはコールバック関数を呼び出して作成しています。

企画段階では、Path2Dというパスを保存しておけるオブジェクトを使用する予定でしたが、線種などの設定を保存できませんでした。

そのため、内側チェックのたびに、コールバック関数を呼び出しています。

もしPath2Dを使用するなら、図形の定義を、線種等のセットとパスのセットに分るとうまくいきそうです。
しかし現在のところPath2Dが実験的な機能のため、今回は見合わせています。

クリック座標の取得

クリック座標の取得は、キャンバスに対して"click"イベントを登録します。

this.canvas.addEventListener('click', e => {} , false );

次にコールバック関数でgetBoundingClientRect()メソッドを使い、キャンバスの位置情報を取得します。

const rect = e.target.getBoundingClientRect();

次にcssでキャンバスのwidthをパーセント表示していると座標がズレます。
そこで、キャンバスの描画エリアサイズとブラウザ上のサイズの比率を求めておきます。

const [w,h] = [this.canvas.width / this.canvas.clientWidth , this.canvas.height / this.canvas.clientHeight];

次にブラウザ上の座標を、キャンバスの位置で補正して、キャンバス上でのクリック座標を計算します。

const [x,y] = [ (e.clientX - rect.left) * w,(e.clientY - rect.top)*h];

移動時の演出について

線上をクリックして位置を移動できるデモでは、マウスカーソルの動きに合わせて移動させる演出をおこなっています。

キャンバスを2枚重ねて、下側に図形の描画を、
上側に演出の描画をおこなっています。

イベントは最上位のキャンバスでのみ受け取ることができます。
そのためイベントは、上側のキャンバスで受け取っています。

参考記事:【JavaScript】 Canvasでレイヤーを表現する

更新日:2020/05/30

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。