クリックではめるパズル
2025年4月15日
みなさんこんにちは。
ケミストのWeb担当みやのです。
今回はドラッグ&ドロップではなくクリックでピースをはめるパズルを作ってみました。
下の「ピースエリア」のピースをクリックして選択し、上の「ドロップエリア」のマスをクリックしてピースをはめてください。
「ヘルプミー」ボタンを押すと、ランダムでピースをはめてくれます。
さあレッツトライ!
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回も使用せずにクリアしたら何か起こるようにしたい
・リトライボタンの実装(何度やってもうまくいきませんでした)
・ドラッグ&ドロップにも対応させる
いつか挑戦してみたいです!