We Blog Weblog

クリックではめるパズル

Web

2025年4月15日

みなさんこんにちは。
ケミストのWeb担当みやのです。

今回はドラッグ&ドロップではなくクリックでピースをはめるパズルを作ってみました。

下の「ピースエリア」のピースをクリックして選択し、上の「ドロップエリア」のマスをクリックしてピースをはめてください。

「ヘルプミー」ボタンを押すと、ランダムでピースをはめてくれます。

さあレッツトライ!

drop area
piece area

HTML

ピースを配置しておく「ピースエリア」
ピースをはめる「ドロップエリア」「ドロップゾーン」
ランダムでピースをはめてくれる「ヘルプミーボタン」を配置します。

<div class="drop-area">
  <div class="dropzone" data-element="One">1</div>
  <div class="dropzone" data-element="Two">2</div>
  <div class="dropzone" data-element="Three">3</div>
  <div class="dropzone" data-element="Four">4</div>
  <div class="dropzone" data-element="Five">5</div>
  <div class="dropzone" data-element="Six">6</div>
  <div class="dropzone" data-element="Seven">7</div>
  <div class="dropzone" data-element="Eight">8</div>
  <div class="dropzone" data-element="Nine">9</div>
</div>

<button id="random-place-btn">ヘルプミー</button>

<div class="piece-area">
  <div class="element" data-element="One" data-color="#dc3545"><span class="symbol">One</span><span class="name">英語</span> </div>
  <div class="element" data-element="Two" data-color="#ff1493"><span class="symbol">Two</span><span class="name">英語</span> </div>
  <div class="element" data-element="Three" data-color="#ff8300"><span class="symbol">Three</span><span class="name">英語</span></div>
  <div class="element" data-element="Four" data-color="#007bff"><span class="symbol">Four</span><span class="name">英語</span></div>
  <div class="element" data-element="Five" data-color="#910082"><span class="symbol">Five</span><span class="name">英語</span></div>
  <div class="element" data-element="Six" data-color="#ffc107"><span class="symbol">Six</span><span class="name">英語</span></div>
  <div class="element" data-element="Seven" data-color="#32bdeb"><span class="symbol">Seven</span><span class="name">英語</span></div>
  <div class="element" data-element="Eight" data-color="#28a745"><span class="symbol">Eight</span><span class="name">英語</span></div>
  <div class="element" data-element="Nine" data-color="#0f0"><span class="symbol">Nine</span><span class="name">英語</span></div>
</div>

CSS

CSSは以下の通りです。

body {
  touch-action: manipulation;
}
.drop-area {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(3, 1fr);
  gap: 10px;
  width: fit-content;
  margin: 20px auto;
  background-color: transparent;
  padding: 10px;
  border: 2px solid #ccc;
}
.dropzone {
  width: 100px;
  height: 100px;
  border: 1px dashed #ccc;
  text-align: center;
  line-height: 100px;
  font-size: 50px;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  box-sizing: border-box;
}
.dropzone.correct {
  color: white!important;
  border: none!important;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  cursor: default;
}
.dropzone .symbol {
  font-size: 25px;
  font-weight: 700;
}
.dropzone .name {
  font-size: 12px;
}
.dropzone.occupied {
  font-size: 20px;
  font-weight: 700;
  line-height: 28px;
  border: none!important;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: default;
}
.piece-area {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
  margin-top: 0;
  padding: 10px;
  width: 100%;
  background-color: transparent;
  border: 2px solid #ccc;
}
.element {
  width: 100px;
  height: 100px;
  background-color: #e0e0e0;
  border: 1px solid #999;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  font-size: 16px;
  font-weight: 700;
  color: #333;
  flex-direction: column;
  box-sizing: border-box;
}
.element .symbol {
  font-size: 25px;
  font-weight: 700;
}
.element .name {
  font-size: 12px;
}
.element.selected {
  border: 3px solid #4285f4;
  cursor: pointer;
}

選択されたピースに青い枠線がつくようにしました。ただし、枠線がついたらレイアウトが崩れることがあったので、box-sizingをborder-boxにしておきます。

JavaScript

スクリプトは超長いので、いくつかに分けて説明します。

画面読み込み時にピースをランダムに配置する関数です。

function shufflePieces() {
  const piecesContainer = document.querySelector('.pieces');
  const pieces = Array.from(piecesContainer.children);
  for (let i = pieces.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [pieces[i], pieces[j]] = [pieces[j], pieces[i]];
  }
  pieces.forEach(piece => piecesContainer.appendChild(piece));
}
window.addEventListener('DOMContentLoaded', shufflePieces);

パズルが完成したかどうかをチェックする関数です。

function checkPuzzleCompletion() {
  const totalDropzones = document.querySelectorAll('.dropzone[data-element]');
  const completedDropzones = document.querySelectorAll('.dropzone.occupied');
  if (totalDropzones.length === completedDropzones.length) {
    fireworks();
  }
}

選択したピースとクリックしたドロップゾーンのdata-elementが一致したら、ピースがはまるようにします。

document.addEventListener("DOMContentLoaded", () => {
  let selectedPiece = null;
  document.querySelectorAll(".element").forEach(piece => {
    piece.addEventListener("click", () => {
      if (selectedPiece) {
        selectedPiece.classList.remove("selected");
      }
      selectedPiece = piece;
      selectedPiece.classList.add("selected");
    });
  });
  document.querySelectorAll(".dropzone").forEach(dropzone => {
    dropzone.addEventListener("click", () => {
      if (selectedPiece && !dropzone.classList.contains("occupied")) {
        const pieceElement = selectedPiece.getAttribute("data-element");
        const dropzoneElement = dropzone.getAttribute("data-element");
        if (pieceElement === dropzoneElement) {
          animatePiecePlacement(selectedPiece, dropzone);
        } else {
          console.log(`Incorrect placement: ${pieceElement} cannot go in ${dropzoneElement}`);
        }
      }
    });
  });
  function placePiece(selectedPiece, dropzone) {
    const symbol = selectedPiece.querySelector('.symbol').textContent;
    const name = selectedPiece.querySelector('.name').textContent;
    const color = selectedPiece.dataset.color || '#cccccc';
    dropzone.innerHTML = `
    
${symbol}
${name}
`; dropzone.classList.add('occupied'); dropzone.style.backgroundColor = color; dropzone.style.color = 'white'; selectedPiece.remove(); checkPuzzleCompletion(); } });

ピースをアニメーションさせる関数です。

function animatePiecePlacement(selectedPiece, dropzone) {
  const pieceOffset = $(selectedPiece).offset();
  const dropzoneOffset = $(dropzone).offset();
  const animatedPiece = $(selectedPiece).clone().css({
    position: "absolute",
    top: pieceOffset.top,
    left: pieceOffset.left,
    width: $(selectedPiece).outerWidth(),
    height: $(selectedPiece).outerHeight(),
    zIndex: 1000,
  }).appendTo("body");
  animatedPiece.animate({
    top: dropzoneOffset.top,
    left: dropzoneOffset.left,
  }, 500, "swing", () => {
    placePiece(selectedPiece, dropzone);
    animatedPiece.remove();
  });
}

ランダムにピースを正解の位置にはめる関数です。


function performRandomPlacement() {
  const remainingPieces = document.querySelectorAll('.element:not(.placed)');
  if (remainingPieces.length === 0) {
    alert('すべてのピースが配置されました!');
    clearInterval(intervalId);
    isPressing = false;
    return;
  }
  const randomPiece = remainingPieces[Math.floor(Math.random() * remainingPieces.length)];
  const elementSymbol = randomPiece.querySelector('.symbol').textContent;
  const correctDropzone = document.querySelector(`.dropzone[data-element="${elementSymbol}"]:not(.occupied)`);
  if (correctDropzone) {
    animatePiecePlacement(randomPiece, correctDropzone);
  }
}

「ヘルプミーボタン」にさっきの関数と、さらに長押し機能を組み込みました。

let intervalId;
let isPressing = false;
let touchTimer;
const randomPlaceButton = document.getElementById('random-place-btn');
randomPlaceButton.addEventListener('mousedown', startLongPress);
randomPlaceButton.addEventListener('touchstart', (e) => {
  e.preventDefault();
  touchTimer = setTimeout(() => {
    startLongPress();
  }, 500);
});
randomPlaceButton.addEventListener('mouseup', handleShortTap);
randomPlaceButton.addEventListener('mouseleave', stopLongPress);
randomPlaceButton.addEventListener('touchend', (e) => {
  clearTimeout(touchTimer);
  if (!isPressing) {
    handleShortTap();
  }
  stopLongPress();
});
function startLongPress() {
  if (isPressing) return;
  isPressing = true;
  intervalId = setInterval(() => {
    performRandomPlacement();
  }, 200);
}
function handleShortTap() {
  if (isPressing) return;
  performRandomPlacement();
}
function stopLongPress() {
  isPressing = false;
  clearInterval(intervalId);
}

今後の課題

・ヘルプミーボタンの挙動がおかしい(長押ししていないのに複数回関数が実行されたり、1つのピースに対して2重に関数が実行されている?)

・ヘルプミーボタンに回数制限やクールタイムをつけたい

・ヘルプミーボタンを1回も使用せずにクリアしたら何か起こるようにしたい

・リトライボタンの実装(何度やってもうまくいきませんでした)

・ドラッグ&ドロップにも対応させる

いつか挑戦してみたいです!

この記事を書いた人
みやの
Web・DTP担当

Contact Us

ご意見、ご相談、料金のお見積もりなど、お気軽にお問い合わせください。

お問い合わせはこちら

TOP