1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScriptでジグソーパズルゲームを実装する手順とポイント

1
Last updated at Posted at 2026-01-11

HTML5 CanvasとJavaScriptを使って、ブラウザで動作するジグソーパズルゲームを実装してみました。画像の分割処理からドラッグ&ドロップ、スナップ判定まで、実際に動くコードを書きながら説明していきます。

この記事で実装すること

  • HTML5 Canvasを使った画像のグリッド分割
  • マウスイベントを利用したドラッグ&ドロップ機能
  • 距離計算によるスナップ判定の実装
  • 完成状態の自動チェック機能

技術スタックは、HTML、JavaScript、Canvas APIです。jQueryなどのライブラリは使わず、バニラJavaScriptで実装しています。

▼独学の迷子を卒業。基本が詰まった一冊をどうぞ。

プロジェクト構成と初期設定

まず、基本的なHTMLファイルを用意します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ジグソーパズルゲーム</title>
  <style>
    body {
      margin: 0;
      padding: 20px;
      font-family: 'Segoe UI', sans-serif;
      background-color: #f5f5f5;
    }
    #container {
      max-width: 800px;
      margin: 0 auto;
      background: white;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    #puzzle-area {
      position: relative;
      margin: 20px 0;
    }
    .piece {
      position: absolute;
      border: 2px solid #333;
      cursor: grab;
      transition: box-shadow 0.2s;
    }
    .piece:hover {
      box-shadow: 0 4px 8px rgba(0,0,0,0.3);
    }
    .piece:active {
      cursor: grabbing;
    }
    .placed {
      border-color: #4CAF50;
      box-shadow: 0 0 10px rgba(76, 175, 80, 0.5);
    }
  </style>
</head>
<body>
  <div id="container">
    <h1>ジグソーパズル</h1>
    <div id="puzzle-area"></div>
    <div id="status"></div>
  </div>
  <script src="puzzle.js"></script>
</body>
</html>

Canvas要素は動的に生成するため、HTMLには配置場所(puzzle-area)だけを用意しています。

画像読み込みとグリッド分割の実装

元画像を読み込んで、指定した行数・列数で分割する処理を実装します。

class JigsawPuzzle {
  constructor(imageSrc, rows = 3, cols = 3) {
    this.sourceImage = new Image();
    this.sourceImage.src = imageSrc;
    this.rows = rows;
    this.cols = cols;
    this.pieces = [];
    this.activePiece = null;
    this.dragOffset = { x: 0, y: 0 };
    this.snapThreshold = 30;

    this.sourceImage.onload = () => {
      this.createPieces();
      this.renderPieces();
      this.setupEventListeners();
    };
  }

  createPieces() {
    const pieceWidth = this.sourceImage.width / this.cols;
    const pieceHeight = this.sourceImage.height / this.rows;

    for (let row = 0; row < this.rows; row++) {
      for (let col = 0; col < this.cols; col++) {
        const canvas = document.createElement('canvas');
        canvas.width = pieceWidth;
        canvas.height = pieceHeight;
        canvas.className = 'piece';
        
        const ctx = canvas.getContext('2d');
        const sourceX = col * pieceWidth;
        const sourceY = row * pieceHeight;

        ctx.drawImage(
          this.sourceImage,
          sourceX, sourceY, pieceWidth, pieceHeight,
          0, 0, pieceWidth, pieceHeight
        );

        this.pieces.push({
          canvas: canvas,
          correctRow: row,
          correctCol: col,
          correctX: col * pieceWidth,
          correctY: row * pieceHeight,
          currentX: Math.random() * 400,
          currentY: Math.random() * 400 + 600,
          isPlaced: false
        });
      }
    }

    // ピースをシャッフル
    this.shufflePieces();
  }
}

drawImageメソッドの引数について:

  • 第1引数:描画元の画像オブジェクト
  • 第2〜5引数:元画像から切り出す領域(x, y, width, height)
  • 第6〜9引数:Canvasに描画する領域(x, y, width, height)

このメソッドを使うことで、元画像の一部分だけを別のCanvasに描画できます。

ピースをシャッフルする関数も追加しておきます。

shufflePieces() {
  for (let i = this.pieces.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [this.pieces[i], this.pieces[j]] = [this.pieces[j], this.pieces[i]];
  }
  
  // シャッフル後の位置をランダムに設定
  this.pieces.forEach((piece, index) => {
    piece.currentX = Math.random() * 400;
    piece.currentY = 600 + Math.random() * 300;
  });
}

ドラッグ&ドロップ機能の実装

マウスイベントを使って、ピースをドラッグできるようにします。

ポイントは、マウス座標とピース座標の相対位置(オフセット)を記録することです。これがないと、クリックした瞬間にピースがマウス位置にジャンプしてしまいます。

setupEventListeners() {
  this.pieces.forEach(piece => {
    piece.canvas.addEventListener('mousedown', (e) => {
      this.handleMouseDown(e, piece);
    });
  });

  document.addEventListener('mousemove', (e) => {
    this.handleMouseMove(e);
  });

  document.addEventListener('mouseup', (e) => {
    this.handleMouseUp(e);
  });
}

handleMouseDown(e, piece) {
  if (piece.isPlaced) return; // 配置済みのピースは動かせない

  this.activePiece = piece;
  const rect = piece.canvas.getBoundingClientRect();
  const containerRect = document.getElementById('puzzle-area').getBoundingClientRect();

  this.dragOffset.x = e.clientX - rect.left;
  this.dragOffset.y = e.clientY - rect.top;

  piece.canvas.style.zIndex = '1000';
  e.preventDefault();
}

handleMouseMove(e) {
  if (!this.activePiece) return;

  const containerRect = document.getElementById('puzzle-area').getBoundingClientRect();
  const newX = e.clientX - containerRect.left - this.dragOffset.x;
  const newY = e.clientY - containerRect.top - this.dragOffset.y;

  this.activePiece.canvas.style.left = newX + 'px';
  this.activePiece.canvas.style.top = newY + 'px';
  this.activePiece.currentX = newX;
  this.activePiece.currentY = newY;
}

handleMouseUp(e) {
  if (!this.activePiece) return;

  this.checkSnap(this.activePiece);
  this.activePiece.canvas.style.zIndex = '1';
  this.activePiece = null;
}

getBoundingClientRect()を使うと、要素の画面座標系での位置を取得できます。これを利用して、マウス座標(clientX, clientY)と要素の相対位置を計算しています。

スナップ判定の実装

ピースが正しい位置に近づいた時に、自動的に配置するスナップ機能を実装します。

判定方法は、ピースの現在位置と正しい位置の距離を計算して、閾値以下なら配置する、というシンプルなものです。

checkSnap(piece) {
  const dx = piece.currentX - piece.correctX;
  const dy = piece.currentY - piece.correctY;
  const distance = Math.sqrt(dx * dx + dy * dy);

  if (distance < this.snapThreshold) {
    piece.canvas.style.left = piece.correctX + 'px';
    piece.canvas.style.top = piece.correctY + 'px';
    piece.currentX = piece.correctX;
    piece.currentY = piece.correctY;
    piece.isPlaced = true;
    piece.canvas.classList.add('placed');

    this.checkCompletion();
  }
}

距離計算には、ユークリッド距離の公式(三平方の定理)を使っています。

distance = ((x2 - x1)² + (y2 - y1)²)

snapThresholdの値を調整することで、スナップの感度を変更できます。値が小さいほど厳密になり、大きいほど緩くなります。

実際にプレイしてみて、操作感を調整するのが良いでしょう。

完成判定の実装

全てのピースが正しい位置に配置されたかをチェックする処理を追加します。

checkCompletion() {
  const allPlaced = this.pieces.every(piece => piece.isPlaced);

  if (allPlaced) {
    this.onComplete();
  }
}

onComplete() {
  const statusDiv = document.getElementById('status');
  statusDiv.innerHTML = '<h2>🎉 パズル完成!</h2>';
  statusDiv.style.color = '#4CAF50';
  
  // 演出を追加
  this.pieces.forEach(piece => {
    piece.canvas.style.transition = 'transform 0.3s';
    setTimeout(() => {
      piece.canvas.style.transform = 'scale(1.05)';
      setTimeout(() => {
        piece.canvas.style.transform = 'scale(1)';
      }, 300);
    }, Math.random() * 500);
  });
}

Array.every()メソッドは、全ての要素が条件を満たしている場合にtrueを返します。全てのピースのisPlacedがtrueになれば、パズルが完成したということです。

ピースの表示処理

作成したピースを画面に配置する処理です。

renderPieces() {
  const puzzleArea = document.getElementById('puzzle-area');
  puzzleArea.innerHTML = ''; // 既存の要素をクリア

  this.pieces.forEach(piece => {
    piece.canvas.style.left = piece.currentX + 'px';
    piece.canvas.style.top = piece.currentY + 'px';
    puzzleArea.appendChild(piece.canvas);
  });
}

ピースの初期位置は、シャッフル処理でランダムに設定しています。プレイエリアの下側に配置することで、正しい位置への移動が分かりやすくなります。

使い方と初期化

Puzzleクラスをインスタンス化して、ゲームを開始します。

// ページ読み込み時にゲームを開始
window.addEventListener('DOMContentLoaded', () => {
  const puzzle = new JigsawPuzzle('puzzle-image.jpg', 3, 3);
  
  // 難易度を変更したい場合
  // const puzzle = new JigsawPuzzle('puzzle-image.jpg', 4, 4); // 4×4グリッド
});

コンストラクタの引数は、画像パス、行数、列数の順です。分割数を増やすと難易度が上がります。

よくある問題とデバッグのコツ

実装中に遭遇しやすい問題と対処法をまとめます。

座標のずれ

Canvas座標と画面座標は基準が異なります。getBoundingClientRect()を使って、要素の画面座標を取得してから計算するのが確実です。

デバッグ時は、座標の値をconsole.logで出力して確認しましょう。

console.log({
  mouseX: e.clientX,
  mouseY: e.clientY,
  rect: piece.canvas.getBoundingClientRect(),
  offset: { x: this.dragOffset.x, y: this.dragOffset.y }
});

イベントの伝播

イベントが意図しない要素で発火することがあります。e.stopPropagation()やe.preventDefault()を使って、イベントの伝播を制御します。

z-indexの管理

ドラッグ中のピースが他のピースに隠れるのを防ぐため、z-indexを動的に変更しています。ドラッグ開始時は大きな値、終了時は小さな値に戻すのがポイントです。

パフォーマンスの考慮

ピースの数が増えると、描画処理やイベント処理の負荷が気になる場合があります。

  • ピースの数は、用途に応じて3×3〜5×5程度が実用的
  • requestAnimationFrameを使った最適化も可能
  • タッチイベント対応でモバイルでも動作させる場合は、touchstart/touchmove/touchendも実装する

今後の拡張アイデア

基本機能が動いたら、以下のような機能追加も検討できます。

  • タイマー機能:完成までの時間を計測
  • スコアシステム:配置速度や精度に応じた点数
  • カスタム画像アップロード:任意の画像でパズルを作成
  • 難易度選択:分割数やスナップ閾値を変更
  • アニメーション:スナップ時のエフェクトを強化

まとめ

HTML5 CanvasとJavaScriptだけで、ブラウザ上で動作するジグソーパズルゲームを実装できました。

主な実装ポイントは以下の通りです。

  • Canvas APIのdrawImageメソッドで画像を分割
  • マウスイベントと座標計算でドラッグ&ドロップを実現
  • 距離計算によるスナップ判定
  • Array.every()を使った完成判定

コードはクラスベースで整理しているため、機能追加や保守がしやすくなっています。分割数やスナップ閾値は、用途に応じて調整してください。

Canvas APIやイベント処理の基礎を理解していれば、同様の手法で他のインタラクティブなゲームも実装できるはずです。

HTML5とJavaScriptでのゲーム制作に慣れたら、Unityなどの本格的なゲームエンジンに挑戦してみるのも良いかもしれません。より複雑なゲームシステムや3D表現にも対応できるようになります。ゲーム開発の幅を広げたい方は、Unity入門の森ショップの教材も参考にしてみてください。

▼独学の迷子を卒業。基本が詰まった一冊をどうぞ。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?