CanvasDOM画像処理

【JavaScript】 Canvasでレイヤーを表現する

更新日:2023/01/30

Canvasでアニメーションをおこなう場合、全体をクリアしてから再描画する必要があります。

しかし描画内容が複雑だと処理が重くなり、滑らかな表現が難しくなります。

改善策として、動かない図形とアニメーションをさせる図形のレイヤーを分けることで、処理を軽くする方法をお伝えします。

 

Canvasを重ねることでレイヤーを表現する

Canvasにはレイヤーという機能がありません。

しかしCanvasを重ねることで、疑似的にレイヤーを表現できます。

Canvasを重ねるには、スタイルの設定が重要となります。

スタイルの定義

次のようなHTMLがあるとします。

HTML

<div class="canvas-wrap">
    <canvas class="canvas" width="600" height="300"></canvas>
    <canvas class="canvas" width="600" height="300"></canvas>
</div>

このとき、次のようにスタイルを設定することで、ぴったりとキャンバスを重ね合わせることができます。

CSS

.canvas-wrap{
            width: 600px;
            max-width: 100%;
            position: relative;
            padding: 0;
            box-sizing: content-box;
}
.canvas_warp:before{
            content:"";
            display: block;
            padding-top: 50%;
}
.canvas{
            position: absolute;
            left:0;
            top:0;
            border: 0;
            max-width:100%;
            box-sizing: content-box;
            padding: 0;
            margin: 0;
}

親要素divには、positionにrelativeを。
子要素のcanvasには、positionにabsolute、topとleftに0をセットします。

こうすることで、親要素divの座標( 0 , 0 )に子要素のcanvas全てがセットされます。
このとき親要素divの高さを指定しないと高さがゼロになり、次に続く要素がcanvasと重なってしまいます。

canvas 高さ 次要素

heightを指定すれば問題ないのですがレスポンシブに対応させる場合、縦横の比率を保つことができません。

そこで疑似要素beforeに、padding-topを設定します。
padding-topにパーセント指定すると、親要素の幅に対する比率がセットされます。

padding-top 疑似要素

これで、幅と高さの比率を維持できます。

キャンバスの幅と高さ

上のスタイル定義では、キャンバスのwidthとheightを設定していません。

タグ上サイズが描画サイズ。
スタイル上のサイズ指定が表示サイズというように、特別な意味があるためです。

詳しくは次のページをご覧ください。
参考記事:【JavaScript】 Canvasのサイズが難解で困る

キャンバスの階層

キャンバスの階層(重なり順)は、html上で後にくるものが上になります。

ただしスタイルシート上で、z-indexを設定することで、順番を変更できます。

レイヤーデモ

JavaScript

(()=>{
/****************************
* 画像の読み込み
* 【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 drawBackground = ( cvs , image ) =>{
            const [ w , h ] = [ image.naturalWidth , image.naturalHeight ];
            cvs.getContext("2d").drawImage( image , 0 , 0 , w , h , 0 , 0 , cvs.width , cvs.height);
        };
        const drawBackgroundError = ( cvs  ) =>{
            const ctx = cvs.getContext("2d");
            ctx.fillStyle = "orange";
            ctx.fillText("背景画像を読み込めませんでした",10,130);
            ctx.strokeRect( 0 , 0 , cvs.width , cvs.height);
        };
/****************************
* アニメキャンバスの描画
*****************************/
        const anime = ball =>{
            const ctx = ball.ctx;
            ctx.clearRect( 0 , 0 , ball.width , ball.height );
            ctx.beginPath();
            ctx.fillStyle = ball.color;
            ctx.arc( ball.x , ball.y , ball.r , 0 , 2 * Math.PI);
            ctx.fill();

            ball.x += ( ball.step * ball.xDir );
            if( ball.x < 0 || ball.x > ball.width ) {
                ball.xDir *= -1; ball.x += ( ball.step * ball.xDir*2 );
            }
            ball.y += ( ball.step * ball.yDir );
            if( ball.y < 0 || ball.y > ball.height ) {
                ball.yDir *= -1; ball.y += ( ball.step * ball.yDir*2 );
            }
        };
/****************************
* DOMが構築されるまで待つ
*****************************/
        window.addEventListener( "DOMContentLoaded" , ()=> {
            const cvs = document.getElementsByClassName("canvas");
            const backgroundCanvas = cvs[0];
            const animeCanvas = cvs[1];
            const ball = { x : 100 , y : 100 , xDir : 0.3 , yDir : 1 ,
                r : 10 , step: 10 , color : "orange",
                ctx : animeCanvas.getContext("2d") ,
                width : animeCanvas.width , height : animeCanvas.height};

            onloadImage( ["https://xxxx.com/xxxx.jpg"] ,
                (index,img)  =>  drawBackground( backgroundCanvas , img ) ,
                index => drawBackgroundError(backgroundCanvas) );

            setInterval(
                ()=>anime(ball)
            ,50);
        });
})();

 

動的にレイヤーキャンバスを追加

次は、キャンバスを動的に追加してみます。

最低限必要な処理

次のコードは、最低限必要な処理です。

const wrap = document.getElementById( "canvas-wrap" );
const cvs = document.createElement("canvas");
cvs.classList.add("canvas");
cvs.setAttribute("width",width);
cvs.setAttribute("height",width);
cvs.style.zIndex = 1;
wrap.appendChild( cvs );
  1. 親要素を取得する
  2. キャンバス要素を作成する
  3. キャンバス要素にクラスをセット(任意)
  4. キャンバス要素に描画サイズをセットする
  5. キャンバス要素にz-indexをセットする(任意)
  6. 親要素にキャンバス要素を追加する

キャンバスのサイズは作成時に幅300px、高さ150pxに設定されます。
そのため、コード上で正しい値をセットする必要があります。

動的追加デモ

追加ボタンを押すたびに、キャンバスが追加されます。

HTML

<button id="btn">追加</button>
<div id="canvas-wrap" class="canvas-wrap" ></div>

JavaScript

(()=>{
    const layerCanvas = function ( id , width , height ) {
        const wrap = document.getElementById( id );
        let count = 0;

        return {
            addCanvas:() => {
                const cvs = document.createElement("canvas");
                cvs.classList.add("canvas");
                cvs.setAttribute("width",width);
                cvs.setAttribute("height",height);
                cvs.style.zIndex = count;
                wrap.appendChild( cvs );
                count ++;
                return cvs;
            }
        };
    };
    window.addEventListener( "DOMContentLoaded" , ()=> {
        const lcv = layerCanvas("canvas-wrap" , 600 , 300);
        let count=0;

        document.getElementById("btn").addEventListener( "click" ,
            () => {
                const newCanvas = lcv.addCanvas();
                count ++;
                const ctx = newCanvas.getContext("2d");
                ctx.fillText( "layer" + count , count * 20 , count * 20);
            });
    });
})();

更新日:2023/01/30

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

スポンサーリンク

記事の内容について

null

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

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

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

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

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

 

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