MENU

JavaScriptミニゲーム15スライドパズル

【JavaScript】 15スライドパズルを作る[1]:キャンバスと背景

更新日:2020/07/09

 

 

今から約30年前、バイトで貯めた40万円でPC9801を購入した僕は、付属していたn88basicでプログラムの勉強をしていました。

 

そして初めて完成させたのが15スライドパズルです。
友達にすごいと言われて、鼻高々でした。

 

そんなことを今になって思い出したので、初心に帰ってJavaScriptで15スライドパズルを作成してみます。

 

↓完成したスライドパズルです↓

シャッフルしてください

 

 

■ピース数や画像変更機能を付加した、公開版スライドパズル
スライドパズルで遊ぼう!!

この記事について

 

スライドパズルの作成は、全3回にてお伝えします。

 

【JavaScript】 15スライドパズルを作る[1]:キャンバスと背景 ← 今読んでいる記事
【JavaScript】 15スライドパズルを作る[2]:ピースの描画と移動
【JavaScript】 15スライドパズルを作る[3]:シャッフルとゲームクリア処理

 

第一回は、キャンバスを動的に作成してゲーム盤を描画します。

 

なお掲載しているソースファイルは、完成したコードより抜き出したあと整形して貼り付けています。
その際チェックをしていますが、誤って削除してしまったり、余計な文字を入力している可能性があります。

 

完全なコードは、3回目に掲載しているのでこちらをご覧ください。

 

またコードは記事の流れでお伝えしやすいように構成を変えています。
そのため見直す点が多くありことをご了承ください。

html/cssの作成

 

まずはhtmlを用意します。

 

15スライドパズル用html

 


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>15 スライドパズル</title>
    <script src="slidepuzzle.js"  type="text/javascript"></script>
    <style>
        #slidepuzzle{
            box-sizing : border-box;
            padding : 0;
            position : relative;
            width: 500px;
            max-width: 100%;
            height: 500px;
            max-height: 95vh;
            margin: 0 auto;
        }
        #slidepuzzle canvas{
            box-sizing : border-box;
            padding : 0;
            margin: 0;
            position: absolute;
        }
    </style>

</head>
<body>
<div id="slidepuzzle"></div>
</body>
</html>

 

 

bodyは、slidepuzzleをIDにもつdiv要素のみ作成します。
この要素はスタイルで幅と高さの最大値を500pxに設定しています。

 

scriptタグで読み込んでいるslidepuzzle.jsは、これから作成するJavaScriptファイルです。

スクリプトファイルの準備

 

プログラムの本体となる、slidepuzzle.jsを用意します。

 

slidepuzzle.js: 初期状態


(()=>{

 /* ここにコードを作成 */

})();

 

変数のグローバル汚染を防ぐために、即時関数内で処理をおこないます。
即時関数については、次の記事を参考にしてください。

 

参考記事:【JavaScript】 即時関数の挙動について調べてみた

スライドパズルの設定

 

まずはスライドパズルを作成するにあたり、後から変更可能な設定をオブジェクトとして用意しておきます。

 

slidepuzzle.js:キャンバスのサイズ設定

 


    const puzzleScreenInfo ={
        size : 500,         // キャンバスのサイズ
        frameSize : 20 ,    // ゲーム盤外枠の幅
        pieceNum : 4,       // 横方向のピースの数
        animeTime: 200,      // アニメーション時間
    };

 

キャンバスのサイズは、実際に描画をおこなう領域のサイズです。
ブラウザ上に表示されるサイズとは異なります。

 

横方向のピースの数は縦方向にも適用されます。
ピースの総数は、pieceNum × pieceNum -1(空きスペース) になります。

 

またピースが移動する様子をアニメーションで表現するために、移動時間を設定しています。

 

スライドパズルのサイズ

キャンバス(レイヤー)の作成

 

今回のスライドパズルは、キャンバスを3枚重ねて、レイヤーを表現します。

 

各レイヤ層は、次の目的を持っています。

 

最下層:背景レイヤー ゲーム盤(背景)の表示
中層:ピースレイヤー ピースの表示
最上層:アニメレイヤー ピースの移動アニメ

 

 

各レイヤーごとにオブジェクト化して、キャンバスの描画やスタイルのセットをおこなうメソッドを作成します。

 

slidepuzzle.js : レイヤー(キャンバス)操作ヘルパーオブジェクト

 


    /**
     * レイヤー(キャンバス)操作ヘルパーオブジェクト
     * @param canvas
     * @param zindex
     */
    const layerCtrl = function( canvas , zindex ) {
        const context = canvas.getContext( "2d" );
        this.getCanvas = () => canvas;
        this.getContext = () => context;

        let beforeIndex = zindex;

        this.setIndex = index => {
            beforeIndex = canvas.style.zIndex;
            canvas.style.zIndex = index;
        };
        this.resetIndex = () => canvas.style.zIndex = beforeIndex;
    };
    layerCtrl.prototype = {
        /**
         * キャンバスにスタイルをセット
         * @param styleObj 例:{ widt: "100px" , height : "100px" }
         */
        setStyle( styleObj ){
            const canvas = this.getCanvas();
            Object.keys( styleObj ).forEach( e => canvas.style[e] = styleObj[e]);
        },
        /**
         * キャンバスサイズをセット
         * @param w
         * @param h
         */
        setSize( w , h ){
            const canvas = this.getCanvas();
            canvas.setAttribute("width", w );
            canvas.setAttribute("height", h );
        },
        /**
         * 四角を描画する
         * @param rect [ 左座標 , 上座標 , 幅 , 高さ ]
         * @param fillColor 塗りつぶし職 null ... 塗りつぶしなし
         * @param strokeColor 線色 null ... 線なし
         */
        rect( rect , fillColor , strokeColor = null ){
            const context = this.getContext();
            context.save();
            if( fillColor !== null ) {
                context.fillStyle = fillColor;context.fillRect( ...rect );
            }
            if( strokeColor !== null ) {
                context.strokeColor = strokeColor;context.strokeRect( ...rect );
            }
            context.restore();
            return this;
        },
        /**
         * テキストの描画
         * @param style  テキスト属性のオブジェクト
         * @param textString 描画する文字列
         * @param x 描画位置
         * @param y 描画位置
         */
        text( style , textString ,x , y){
            const context = this.getContext();
            context.save();
            Object.keys( style ).forEach( e => context[e] = style[e]);
            context.strokeText( textString , x ,  y);
            context.fillText( textString , x , y );
            context.restore();
            return this;
        },
        /**
         * イメージデータの取得
         * @param rect
         * @returns {ImageData}
         */
        getImageData( rect ){
            return this.getContext().getImageData( ...rect );
        },
        /**
         * イメージの描画
         * @param image
         * @param x
         * @param y
         * @returns {layerCtrl}
         */
        putImageData( image , x , y ){
            this.getContext().putImageData( image , x ,y );
            return this;
        },
        /**
         * キャンバス全面クリア
         */
        clear(){
            const cvs = this.getCanvas();
            this.getContext().clearRect( 0 , 0 , cvs.width , cvs.height  );
        },
        /**
         * キャンバスクリア
         * @param rect [ 左座標 , 上座標 , 幅 , 高さ ]
         * @returns {layerCtrl}
         */
        clearRect( rect ) {
            this.getContext().clearRect( ...rect  );
            return this;
        },
        /**
         * キャンバスのサイズと表示サイズの比率
         * @returns {number}
         */
        getScale (){
            const canvas = this.getCanvas();
            return canvas.clientWidth / canvas.width;
        }
    };

 

次にdiv要素内に、キャンバス(レイヤー)を3枚セットする関数を作成します。

 

slidepuzzle.js : キャンバスの作成

 


     /**
     * キャンバスを作成して配置
     * @param divId キャンバスを作成するDIV要素のID
     * @returns {[canvas,canvas]} [背景,パズル]のキャンバス配列
     */
    const makeSlidePuzzle = function ( divId  ) {

            // div要素取得
        const parentDiv = document.getElementById( divId );
        if( parentDiv === null ) throw new Error("id:" + divId + "がない");

            // キャンバスの表示サイズ
        const viewSize = Math.min( parentDiv.clientWidth , parentDiv.clientHeight );

            // キャンバスのスタイル設定
        const style ={ width : viewSize + "px" , height : viewSize + "px" ,
            top : ((parentDiv.clientHeight - viewSize) / 2).toString() + "px" ,
            left : ((parentDiv.clientWidth - viewSize) / 2).toString() + "px" };

            // キャンバスを3枚作成
        return [1,2,3].map( zindex => {
            const cv = document.createElement("canvas" );
            const layer = new layerCtrl( cv ,zindex );
            layer.setSize(puzzleScreenInfo.size,puzzleScreenInfo.size );

            layer.setStyle( style );

            parentDiv.appendChild( cv );
            return layer;
        });
    };

 

makeSlidePuzzle関数は、キャンバスを3枚作成してそれぞれに、幅や高さをなどのスタイルを設定します。

 

キャンバスのサイズは固定(puzzleScreenInfo.size)ですが、表示サイズはコンテナとなるdiv要素の大きさによって変化します。

 

div要素はブラウザの画面サイズにより大きさが変化するので、、幅と高さの短い方をキャンバスの表示サイズに設定しています。(※パズルは正方形)

 

また、div要素の中央にキャンバスを配置しています。

 

なおキャンバスの描画サイズと表示サイズは、異なる意味を持っています。詳しくは次の記事を見てください。
参考記事:【JavaScript】 Canvasのサイズが難解で困る

 

設定後にdiv要素にキャンバスを追加し、レイヤーオブジェクトの配列をリターンしています。

背景の描画

 

作成した背景レイヤーにパズルの背景を描画します。
ここでの背景とは、パズルのゲーム盤です。

 

slidepuzzle.js : 背景の描画

 


    /**
     * 背景描画用オブジェクト
     * @param layer
     */
    const backGroundDrawFunc = function ( layer ) {

        this.getLayer = ()=> layer;

        this.frame = null; // 外周 [ 始点x , 始点y , 幅 , 高さ ]
        this.innerFrame = null; // 内周( パズルエリア )

        this.frameColor = "burlywood"; // 外周色
        this.frameLineColor = "dimgray"; // 外周の線色
        this.bottomPlateColor ="tan"; // 底板の色
    };
    backGroundDrawFunc.prototype={
        /**
         * 背景描画に必要な座標を計算
         */
        init(){
            const   {size,frameSize} = {...puzzleScreenInfo};

            // 外周,内周セット
            this.frame = [0 , 0 , size , size];
            this.innerFrame = [ frameSize , frameSize,size - frameSize*2,size - frameSize*2  ];
            return this;
        },
        /**
         * init()で計算した情報をもとに背景を描画
         * @param context
         */
        draw(  ){

            this.getLayer().clearRect( this.frame )
                .rect( this.frame , this.frameColor , null )
                .rect( this.innerFrame , this.bottomPlateColor , this.frameLineColor );
        }
    };

 

このオブジェクトのinit()メソッドは、描画に必要な座標データを計算しています。

 

続くdraw()メソッドは、計算した座標を使用して、背景を描画しています。

 

キャンバスの描画は簡単なものでも呼び出すメソッドやプロパティが多くなる傾向があります。
そのためダラダラとコードが長くなりがちですが、レイヤーオブジェクトに機能をまとめることで、簡潔な記述となっています。

テスト

 

ここまで作成したコードをブラウザで表示してみます。

 

DOMの構築が終わりDOMContentLoadedイベントが発生したタイミングで、処理をおこないます。

 

slidepuzzle.js : 背景描画までのテスト

 

(()=>{

/**
   ここに
     キャンバスのサイズ設定 レイヤー(キャンバス)操作ヘルパーオブジェクトキャンバスの作成背景の描画
  のコードを記述
*/

    window.addEventListener( "DOMContentLoaded" , ()=> {
         const [backGroundLyer,puzzleLayer,animeLayer] = makeSlidePuzzle("slidepuzzle"  );

         new backGroundDrawFunc( backGroundLyer ).init().draw( );
    });
})();

 

実行すると、次のような画面が表示されます。

 

スライドパズル 背景

 

背景画面はパズルの動作に直接的な影響がないので、色を変更したり、全て画像に置き換えたりと自由に変更できます。

記事の内容について

 

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


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

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

そんなときは、ご意見もらえたら嬉しいです。

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