<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>유아용 도형 퍼즐 게임</title>
<style>
body {
font-family: 'Inter', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
background-color: #f0f9ff; /* 하늘색 배경 */
margin: 0;
padding: 20px;
box-sizing: border-box;
}
#game-container {
background-color: #ffffff;
padding: 20px;
border-radius: 16px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
text-align: center;
}
canvas {
border: 2px solid #3b82f6; /* 파란색 테두리 */
border-radius: 8px;
cursor: grab;
touch-action: none; /* 터치 이벤트 중복 방지 */
}
.message-box {
margin-top: 20px;
padding: 12px 20px;
border-radius: 8px;
font-size: 1.1rem;
font-weight: 500;
}
.success {
background-color: #dcfce7; /* 연한 녹색 */
color: #166534; /* 진한 녹색 */
}
.info {
background-color: #e0f2fe; /* 연한 파란색 */
color: #075985; /* 진한 파란색 */
}
button {
margin-top: 20px;
padding: 12px 24px;
font-size: 1rem;
font-weight: 600;
color: white;
background-color: #2563eb; /* 파란색 버튼 */
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #1d4ed8; /* 더 진한 파란색 */
}
.piece-container {
margin-top: 20px;
display: flex;
justify-content: space-around;
align-items: center;
gap: 10px; /* 조각들 사이 간격 */
}
.draggable-piece-display {
width: 60px;
height: 60px;
border-radius: 8px; /* 기본 모양, JS에서 도형별로 덮어쓸 수 있음 */
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
color: white;
font-weight: bold;
}
</style>
</head>
<body>
<div id="game-container" class="w-full max-w-md">
<h1 class="text-3xl font-bold text-blue-600 mb-6">도형 퍼즐 맞추기</h1>
<canvas id="puzzleCanvas" width="300" height="300" class="mx-auto"></canvas>
<div id="pieceDisplayContainer" class="piece-container">
</div>
<div id="messageArea" class="message-box info">도형을 올바른 위치로 옮겨보세요!</div>
<button id="resetButton">다시 시작</button>
</div>
<script>
const canvas = document.getElementById('puzzleCanvas');
const ctx = canvas.getContext('2d');
const messageArea = document.getElementById('messageArea');
const resetButton = document.getElementById('resetButton');
const pieceDisplayContainer = document.getElementById('pieceDisplayContainer');
let pieces = []; // 퍼즐 조각 배열
let targets = []; // 목표 위치 배열
let draggingPiece = null; // 현재 드래그 중인 조각
let offsetX, offsetY; // 드래그 시작 시 마우스와 조각의 상대 위치
const pieceSize = 50; // 조각 크기
const snapThreshold = 20; // 목표 위치에 근접했다고 판단하는 거리
// 퍼즐 조각 및 목표 위치 정의
const shapeData = [
{ shape: 'square', color: '#ef4444', name: '네모' }, // 빨간색 네모
{ shape: 'circle', color: '#3b82f6', name: '동그라미' }, // 파란색 동그라미
{ shape: 'triangle', color: '#22c55e', name: '세모' } // 초록색 세모
];
// 게임 초기화 함수
function initGame() {
pieces = [];
targets = [];
pieceDisplayContainer.innerHTML = ''; // 조각 표시 영역 초기화
draggingPiece = null;
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
// 목표 위치 설정 (캔버스 중앙에 배치)
const targetSpacing = pieceSize * 2;
const totalTargetWidth = (shapeData.length -1) * targetSpacing;
const startTargetX = (canvasWidth - totalTargetWidth) / 2 - pieceSize /2;
shapeData.forEach((data, index) => {
// 목표 위치
targets.push({
x: startTargetX + index * targetSpacing + pieceSize / 2,
y: canvasHeight / 2,
width: pieceSize,
height: pieceSize,
shape: data.shape,
color: data.color, // 목표 위치 색상도 조각과 동일하게
isOccupied: false // 해당 목표 위치에 조각이 놓였는지 여부
});
// 퍼즐 조각 (초기 위치는 캔버스 하단 또는 별도 영역)
// 여기서는 pieceDisplayContainer에 HTML 요소로 표시합니다.
const pieceElement = document.createElement('div');
pieceElement.classList.add('draggable-piece-display');
pieceElement.style.backgroundColor = data.color;
pieceElement.textContent = data.name;
if (data.shape === 'circle') {
pieceElement.style.borderRadius = '50%';
} else if (data.shape === 'triangle') {
// CSS로 삼각형 만들기 (간단한 버전)
pieceElement.style.width = '0';
pieceElement.style.height = '0';
pieceElement.style.borderLeft = `${pieceSize/2}px solid transparent`;
pieceElement.style.borderRight = `${pieceSize/2}px solid transparent`;
pieceElement.style.borderBottom = `${pieceSize}px solid ${data.color}`;
pieceElement.style.backgroundColor = 'transparent'; // 배경색 제거
pieceElement.textContent = ''; // 삼각형에는 텍스트 숨김
// 삼각형 텍스트를 위한 별도 요소 (선택 사항)
const triangleText = document.createElement('span');
triangleText.textContent = data.name;
triangleText.style.color = '#333'; // 어두운 텍스트 색상
triangleText.style.position = 'absolute';
triangleText.style.bottom = '-20px'; // 삼각형 아래에 위치
triangleText.style.left = '50%';
triangleText.style.transform = 'translateX(-50%)';
pieceElement.style.position = 'relative'; // 부모 요소에 relative 추가
pieceElement.appendChild(triangleText);
}
// 드래그 시작을 위한 데이터 추가
pieceElement.dataset.shape = data.shape;
pieceElement.dataset.color = data.color;
pieceElement.dataset.name = data.name;
pieceElement.dataset.id = index; // 조각 고유 ID
// 개별 조각 객체 생성 (캔버스에 그려질 때 사용)
pieces.push({
id: index,
x: 0, // 실제 캔버스 x 위치 (드래그 시작 시 업데이트)
y: 0, // 실제 캔버스 y 위치 (드래그 시작 시 업데이트)
width: pieceSize,
height: pieceSize,
shape: data.shape,
color: data.color,
name: data.name,
isDragging: false,
isPlaced: false, // 목표 위치에 놓였는지 여부
originalElement: pieceElement // HTML 요소 참조
});
pieceDisplayContainer.appendChild(pieceElement);
});
draw(); // 초기 화면 그리기
updateMessage('도형을 올바른 위치로 옮겨보세요!', 'info');
}
// 도형 그리기 함수
function drawShape(shapeObj, x, y, w, h, color, isOutline = false) {
ctx.fillStyle = color;
ctx.strokeStyle = color; // 테두리 색상도 동일하게
ctx.lineWidth = 3;
const centerX = x; // 중심 x
const centerY = y; // 중심 y
if (shapeObj.shape === 'square') {
if (isOutline) ctx.strokeRect(centerX - w/2, centerY - h/2, w, h);
else ctx.fillRect(centerX - w/2, centerY - h/2, w, h);
} else if (shapeObj.shape === 'circle') {
ctx.beginPath();
ctx.arc(centerX, centerY, w / 2, 0, Math.PI * 2);
if (isOutline) ctx.stroke();
else ctx.fill();
} else if (shapeObj.shape === 'triangle') {
ctx.beginPath();
ctx.moveTo(centerX, centerY - h / 2); // Top point
ctx.lineTo(centerX - w / 2, centerY + h / 2); // Bottom-left point
ctx.lineTo(centerX + w / 2, centerY + h / 2); // Bottom-right point
ctx.closePath();
if (isOutline) ctx.stroke();
else ctx.fill();
}
}
// 전체 화면 그리기 함수
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height); // 캔버스 초기화
// 목표 위치 그리기 (테두리만)
targets.forEach(target => {
if (!target.isOccupied) { // 아직 조각이 놓이지 않은 목표 위치만 그리기
drawShape(target, target.x, target.y, target.width, target.height, target.color, true);
}
});
// 퍼즐 조각 그리기 (드래그 중이거나 이미 배치된 조각만 캔버스에 그림)
pieces.forEach(piece => {
if (piece.isDragging || piece.isPlaced) {
drawShape(piece, piece.x, piece.y, piece.width, piece.height, piece.color);
}
});
}
// 메시지 업데이트 함수
function updateMessage(text, type) {
messageArea.textContent = text;
messageArea.className = `message-box ${type}`;
}
// 마우스/터치 이벤트 좌표 얻기
function getMousePos(evt) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
if (evt.touches && evt.touches.length > 0) { // 터치 이벤트
return {
x: (evt.touches[0].clientX - rect.left) * scaleX,
y: (evt.touches[0].clientY - rect.top) * scaleY
};
}
// 마우스 이벤트
return {
x: (evt.clientX - rect.left) * scaleX,
y: (evt.clientY - rect.top) * scaleY
};
}
// 드래그 시작 (HTML 요소에서 시작)
function handleDragStart(e) {
const targetElement = e.target.closest('.draggable-piece-display');
if (!targetElement) return;
const pieceId = parseInt(targetElement.dataset.id);
const piece = pieces.find(p => p.id === pieceId);
if (piece && !piece.isPlaced) {
draggingPiece = piece;
const mousePos = getMousePos(e);
// 드래그 시작 시 조각을 캔버스 중앙으로 옮겨서 시작하는 것처럼 보이게 함
// 또는 마우스 위치에 따라 조각의 초기 캔버스 위치 설정
draggingPiece.x = mousePos.x;
draggingPiece.y = mousePos.y;
offsetX = mousePos.x - draggingPiece.x; // 사실상 0
offsetY = mousePos.y - draggingPiece.y; // 사실상 0
draggingPiece.isDragging = true;
targetElement.style.opacity = '0.5'; // 드래그 중인 HTML 조각은 반투명하게
canvas.style.cursor = 'grabbing';
draw(); // 드래그 시작 시 캔버스에 조각 그리기
}
e.preventDefault(); // 기본 브라우저 드래그 방지
}
// 드래그 중
function handleDrag(e) {
if (!draggingPiece) return;
const mousePos = getMousePos(e);
draggingPiece.x = mousePos.x - offsetX;
draggingPiece.y = mousePos.y - offsetY;
draw();
e.preventDefault();
}
// 드래그 종료
function handleDragEnd(e) {
if (!draggingPiece) return;
let placedCorrectly = false;
for (let target of targets) {
// 목표 위치와 조각 모양이 같고, 아직 해당 목표 위치에 다른 조각이 놓이지 않았으며, 거리가 가까운 경우
if (target.shape === draggingPiece.shape && !target.isOccupied &&
Math.abs(draggingPiece.x - target.x) < snapThreshold &&
Math.abs(draggingPiece.y - target.y) < snapThreshold) {
draggingPiece.x = target.x; // 정확한 위치로 스냅
draggingPiece.y = target.y;
draggingPiece.isPlaced = true;
target.isOccupied = true; // 목표 위치 점유 표시
placedCorrectly = true;
draggingPiece.originalElement.style.display = 'none'; // HTML 조각 숨기기
break;
}
}
if (!placedCorrectly) {
// 잘못된 위치에 놓으면 원래 HTML 조각 표시 영역으로 돌려보내는 효과 (여기서는 그냥 다시 보이게)
draggingPiece.originalElement.style.opacity = '1';
}
draggingPiece.isDragging = false;
draggingPiece = null;
canvas.style.cursor = 'grab';
draw();
checkCompletion();
e.preventDefault();
}
// 퍼즐 완성 확인 함수
function checkCompletion() {
const allPlaced = pieces.every(p => p.isPlaced);
if (allPlaced) {
updateMessage('참 잘했어요! 퍼즐을 완성했어요!', 'success');
}
}
// 이벤트 리스너 등록
// pieceDisplayContainer에 이벤트 위임 사용
pieceDisplayContainer.addEventListener('mousedown', handleDragStart);
pieceDisplayContainer.addEventListener('touchstart', handleDragStart, { passive: false });
// 캔버스 외부에서도 mousemove와 mouseup을 감지하도록 window에 등록
window.addEventListener('mousemove', handleDrag);
window.addEventListener('touchmove', handleDrag, { passive: false });
window.addEventListener('mouseup', handleDragEnd);
window.addEventListener('touchend', handleDragEnd, { passive: false });
resetButton.addEventListener('click', initGame);
// 게임 시작
initGame();
</script>
</body>
</html>