CanvasDOM画像処理

【JavaScript】 Canvas上でクリックした図形を塗りつぶす

更新日:2020/06/03

JavaScriptのCanvasAPI にisPointInPath()というメソッドがあります。

パス内に指定した座標が含まれているかどうか確認するメソッドですが、どんなケースで使用するのか不明でした。

そこで自分なりに使用目的を考えてみたところ、ブラウザ用で図形の内側がクリックされたかどうかの確認できると思いいたりました。

そこでデモとして、クリックした図形を塗りつぶすJavaScriptコードを作成しました。

 

デモ

図形の内側をクリックすると塗りつぶします。

 

JavaScriptコード

HTML

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

JavaScript

(()=>{
            // 塗りつぶし色
            const pathObjFillColor = "rgba( 0 , 0 , 255 , .5)";

            /************************************
             * 図形処理用オブジェクト
             *************************************/
            const pathObj = function ( callBack ) {
                this.callBack = callBack;   // パスを作成する関数
                this.filled = false;    // 塗りつぶしフラグ
            };
            pathObj.prototype={
                // 図形の描画
                draw( context ){
                    const cts = this.drawData( context );

                    if( this.filled ) {
                        cts.fillStyle=pathObjFillColor;
                        cts.fill();
                    }
                    cts.stroke();
                    cts.restore();
                },
                isFilled(){ return this.filled; }, // 塗りつぶし状態の取得
                fill(){ this.filled = true; }, // 塗りつぶしフラグをセット
                resetFill(){ this.filled = false; }, // 塗りつぶしフラグをリセット

                // 指定した座標が図形内にあるかチェック
                // ※塗りつぶし状態のときは、false
                isIn( context , x , y ){
                    return this.filled ? false : this.drawData( context ).isPointInPath( 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(){
                    const context = this.canvas.getContext("2d");
                    context.clearRect(0,0,this.canvas.width,this.canvas.height);
                    this.path.forEach( e => e.draw( this.context ));
                },
                // クリックされた座標が図形内かチェックして塗りつぶし
                inFill( x , y ){
                    // 配列の最後=上階層の図形
                    for( let i = this.path.length -1 ; i >= 0 ; i -- ){
                        if( this.path[i].isIn( this.context , x , y ) ){
                            this.path[i].fill();
                            this.draw();
                            return;
                        }
                    }
                    // 全て塗りつぶされたかチェック
                    if( this.path.every( e=> e.isFilled()) ) {
                        // 塗りつぶされていたら、リセット
                        this.path.forEach( e=>e.resetFill());
                        this.draw();
                    }
                },
                // キャンバス上でのクリックイベント処理
                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.inFill( x , y );
                    }, false);
                }
            };

            /************************************
            * 図形パスの定義関数を格納した配列
            *************************************/
            const paths = [
                context => {
                    context.lineWidth = 1;
                    context.strokeStyle = "#000";
                    context.moveTo( 250 , 15 );
                    context.lineTo( 40 , 130  );
                    context.arcTo( 0 , 300 ,  300 , 300 , 100);
                    context.closePath();
                },
                context => {
                    context.lineWidth = 3;
                    context.strokeStyle = "red";
                    context.rect( 10,10,200,100 );
                },
                context => {
                    context.lineWidth = 3;
                    context.strokeStyle = "pink";
                    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 = 2;
                    context.strokeStyle = "green";
                    context.arc( 250,120,70,0,2 * Math.PI );
                },
                context => {
                    context.lineWidth = 2;
                    context.strokeStyle = "#ccc";
                    context.moveTo( 50 , 5 );
                    context.lineTo( 50 , 250 );
                    context.lineTo( 150 , 5 );
                    context.closePath();
                }
            ];

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

                const pathListObj = new pathList( cvs );

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

                pathListObj.draw();

                pathListObj.clickEvent();

            });
})();

 

解説

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

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

コンテキストには、チェックをおこないたいパスがセットされている必要があります。

そのため、図形毎にオブジェクト(pathObj)を作成して、パスの作成とチェックをおこなっています。

パスの作成

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

企画段階では、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];

 

キャンバスを重ねた場合

キャンバスを重ねてレイヤを表現した場合、クリックイベントは最上位のキャンバスのみで発生します。

下層のキャンバスの図形をクリックしたい場合は、最上位のキャンバスでクリックイベントを取得して、座標を下層の図形に適用させます。

考え方は難しくないと思うので、挑戦してみてください。

更新日:2020/06/03

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

スポンサーリンク

記事の内容について

null

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

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

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

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

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

 

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