📌Workshop01 : Chatgemini สร้างเกม AR ด้วย AI (ภาษาอังกฤษ)
📌Workshop01 : Chatgemini สร้างเกม AR ด้วย AI (ภาษาอังกฤษ)
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AR English Sentence Maker</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Google Fonts & FontAwesome -->
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;600;700&family=Sarabun:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- MediaPipe Libraries -->
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>
<style>
body {
font-family: 'Sarabun', 'Chakra Petch', sans-serif;
overflow: hidden;
user-select: none;
background-color: #0b0f19;
}
/* Glassmorphism Styles */
.glass-panel {
background: rgba(15, 23, 42, 0.55);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
}
.glass-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
/* 2.5D Card Effect */
.card-3d {
transition: transform 0.1s ease, box-shadow 0.1s ease;
transform: perspective(500px) rotateX(5deg);
box-shadow: 0 10px 0px 0px rgba(13, 148, 136, 0.4), 0 15px 20px rgba(0, 0, 0, 0.3);
}
.card-3d:active, .card-3d.active {
transform: perspective(500px) rotateX(0deg) translateY(6px);
box-shadow: 0 4px 0px 0px rgba(13, 148, 136, 0.6), 0 5px 10px rgba(0, 0, 0, 0.4);
}
/* Video Flip for Mirror Effect */
#webcam {
transform: scaleX(-1);
}
</style>
</head>
<body class="w-screen h-screen flex items-center justify-center text-white relative">
<!-- ================= BACKGROUND AUDIO / EFFECTS ================= -->
<audio id="snd-click" src="https://assets.mixkit.co/active_storage/sfx/2568/2568-84.wav" preload="auto"></audio>
<audio id="snd-success" src="https://assets.mixkit.co/active_storage/sfx/2019/2019-84.wav" preload="auto"></audio>
<audio id="snd-clear" src="https://assets.mixkit.co/active_storage/sfx/1435/1435-84.wav" preload="auto"></audio>
<audio id="snd-fail" src="https://assets.mixkit.co/active_storage/sfx/2572/2572-84.wav" preload="auto"></audio>
<!-- ================= MAIN SCREEN / START SCREEN ================= -->
<div id="screen-start" class="absolute inset-0 z-50 flex flex-col items-center justify-center bg-radial from-slate-900 to-black p-4">
<div class="glass-panel p-8 rounded-3xl max-w-lg w-full text-center border border-indigo-500/30 flex flex-col items-center">
<div class="w-24 h-24 bg-indigo-600/20 text-indigo-400 rounded-2xl flex items-center justify-center text-5xl mb-6 shadow-lg border border-indigo-500/20 animate-bounce">
<i class="fa-solid fa-wand-magic-sparkles"></i>
</div>
<h1 class="text-4xl font-bold tracking-wider mb-2 bg-gradient-to-r from-teal-400 via-indigo-400 to-purple-400 bg-clip-text text-transparent font-['Chakra_Petch']">
AR ENGLISH GAME
</h1>
<p class="text-gray-300 text-lg mb-8">เรียงประโยคภาษาอังกฤษในโลกเสมือนจริงด้วยมือของคุณ</p>
<div class="space-y-4 w-full mb-8 text-left text-sm text-slate-300 bg-slate-950/40 p-4 rounded-xl border border-slate-800">
<div class="flex items-center gap-3"><i class="fa-solid fa-camera text-teal-400 w-5"></i> ใช้กล้องเว็บแคมในการจับความเคลื่อนไหวมือ</div>
<div class="flex items-center gap-3"><i class="fa-solid fa-hand-pointer text-indigo-400 w-5"></i> นิ้วชี้คือเคอร์เซอร์ เล็งคำศัพท์ที่ต้องการ</div>
<div class="flex items-center gap-3"><i class="fa-solid fa-compress text-purple-400 w-5"></i> <strong>จีบนิ้ว (Pinch)</strong> เพื่อหนีบจับ และกางนิ้วเพื่อปล่อย</div>
<div class="flex items-center gap-3"><i class="fa-solid fa-mouse-pointer text-amber-400 w-5"></i> <strong>คลิกลากแทนได้:</strong> หากเล่นผ่านคอมทั่วไปที่ไม่มีกล้อง</div>
</div>
<button id="btn-start" class="w-full py-4 px-8 bg-gradient-to-r from-teal-500 to-indigo-600 hover:from-teal-400 hover:to-indigo-500 rounded-2xl font-bold text-xl shadow-lg shadow-indigo-500/20 hover:shadow-indigo-500/30 transition-all duration-300 transform hover:-translate-y-1">
<i class="fa-solid fa-play mr-2"></i> เข้าสู่เกม (Start)
</button>
</div>
</div>
<!-- ================= INSTRUCTION OVERLAY (ต้องเริ่มใน AR Canvas) ================= -->
<div id="instruction-overlay" class="hidden absolute inset-0 z-25 bg-black/70 flex items-center justify-center p-4">
<div class="glass-panel max-w-md w-full p-6 rounded-2xl text-center border border-emerald-500/20">
<h2 class="text-2xl font-bold text-emerald-400 mb-4"><i class="fa-regular fa-lightbulb mr-2"></i> วิธีการเล่นในโหมด AR</h2>
<p class="text-slate-200 mb-4 text-sm leading-relaxed">
เมื่อกดปุ่มด้านล่าง กล้องเว็บแคมจะทำงาน <br>
กรุณาชูมือของคุณขึ้นมาระยะที่กล้องเห็นชัดเจน <br>
<strong>ใช้ปลายนิ้วชี้ เล็งไปที่ปุ่ม "เริ่มเกม!" แล้วทำการ "จีบนิ้วชี้และนิ้วโป้งเข้าหากัน" เพื่อกดปุ่ม</strong>
</p>
<div class="flex justify-center gap-4 mb-6">
<div class="text-center p-3 glass-card rounded-lg w-24">
<span class="block text-2xl">🤏</span>
<span class="text-xs text-slate-300">จีบนิ้ว = จับ</span>
</div>
<div class="text-center p-3 glass-card rounded-lg w-24">
<span class="block text-2xl">🖐️</span>
<span class="text-xs text-slate-300">กางนิ้ว = ปล่อย</span>
</div>
</div>
<p class="text-xs text-amber-400 mb-4"><i class="fa-solid fa-info-circle"></i> หากโหลดกล้องช้าหรือไม่สามารถใช้กล้องได้ สามารถใช้เมาส์คลิกปุ่มเพื่อเริ่มได้เช่นกัน</p>
<!-- Placeholder สำหรับปุ่ม Canvas ที่วาดทับ -->
<div id="ar-ready-placeholder" class="w-full h-12 mt-2"></div>
</div>
</div>
<!-- ================= GAME PLAY CONTAINER ================= -->
<div class="relative w-full h-full flex flex-col items-center justify-center">
<!-- AR VIEWPORT (Webcam + Canvas overlay) -->
<div id="ar-container" class="relative w-full h-full flex items-center justify-center bg-slate-950">
<!-- Webcam Video -->
<video id="webcam" class="absolute w-full h-full object-cover opacity-60 z-0" autoplay playsinline muted></video>
<!-- Game Canvas for drawing words, hand skeleton, interactive buttons, and cursor (z-30 เป็นชั้นบนสุดเพื่อให้การ์ด และ Cursor ลอยทับปุ่มและสล็อตทั้งหมด) -->
<canvas id="game-canvas" class="absolute top-0 left-0 w-full h-full z-30 pointer-events-auto"></canvas>
<!-- TOP BAR HUD (เลเยอร์ z-40 ให้คลิกเลือกได้ปกติ แต่ใช้ pointer-events-none ในกรอบหลัก) -->
<div class="absolute top-4 left-4 right-4 z-40 flex flex-wrap gap-3 justify-between items-center pointer-events-none">
<!-- Level & Score -->
<div class="flex items-center gap-3">
<div class="glass-panel px-4 py-2 rounded-xl text-sm font-semibold flex items-center gap-2 pointer-events-auto">
<span class="text-slate-400">ด่าน:</span>
<span id="hud-level" class="text-teal-400 font-bold">1 / 3</span>
</div>
<div class="glass-panel px-4 py-2 rounded-xl text-sm font-semibold flex items-center gap-2 pointer-events-auto">
<span class="text-slate-400">คะแนน:</span>
<span id="hud-score" class="text-amber-400 font-bold">0</span>
</div>
</div>
<!-- Thai Hint Goal Box -->
<div class="glass-panel px-6 py-3 rounded-2xl flex flex-col items-center border border-indigo-500/30 max-w-sm pointer-events-auto">
<span class="text-xs text-indigo-300 uppercase tracking-wider font-semibold">โจทย์ภาษาไทย</span>
<span id="thai-hint" class="text-xl font-bold text-white mt-1">ฉัน รัก สุนัข</span>
</div>
<!-- System Status & Camera control -->
<div class="flex items-center gap-3">
<div id="cam-status" class="glass-panel px-3 py-2 rounded-xl text-xs flex items-center gap-2 text-rose-400 pointer-events-auto bg-rose-950/20">
<span class="relative flex h-2 w-2">
<span id="status-ping" class="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-75"></span>
<span id="status-dot" class="relative inline-flex rounded-full h-2 w-2 bg-rose-500"></span>
</span>
<span id="status-text">รอเปิดระบบกล้อง...</span>
</div>
<button id="btn-toggle-cam" class="glass-panel p-2.5 rounded-xl hover:bg-slate-800 pointer-events-auto text-slate-300 transition-colors">
<i class="fa-solid fa-video"></i>
</button>
</div>
</div>
<!-- AR HAND LOADING ADVISORY (แสดงหากยังไม่เจอมือ หรือหากมือหายไป) -->
<div id="hand-missing-tip" class="hidden absolute bottom-28 left-1/2 -translate-x-1/2 z-20 glass-panel px-4 py-2.5 rounded-full text-xs text-amber-300 flex items-center gap-2 border border-amber-500/20 shadow-lg animate-pulse">
<i class="fa-solid fa-hand-sparkles"></i>
<span>กำลังหามือของคุณ... กรุณาชูมือขึ้นใกล้กล้องเว็บแคม</span>
</div>
<!-- DROP ZONE AREA (สล็อตวางช่องคำศัพท์ อยู่ที่ชั้น z-20) -->
<div class="absolute bottom-6 left-4 right-4 z-20 flex flex-col items-center pointer-events-none">
<div class="glass-panel w-full max-w-4xl p-4 rounded-2xl border-2 border-dashed border-indigo-500/40 bg-indigo-950/15 flex flex-col items-center pointer-events-auto">
<div class="text-xs text-slate-300 mb-2 font-semibold tracking-wider uppercase">วางเรียงคำศัพท์ลงในช่องด้านล่างนี้</div>
<div id="slots-container" class="flex justify-center gap-4 w-full min-h-[100px] items-center py-2 px-4 rounded-xl bg-black/25">
<!-- Slots will be injected by JavaScript dynamically -->
</div>
</div>
</div>
</div>
</div>
<!-- ================= STAGE COMPLETED MODAL ================= -->
<div id="modal-clear" class="hidden absolute inset-0 z-20 bg-black/85 flex items-center justify-center p-4">
<div class="glass-panel max-w-md w-full p-8 rounded-3xl text-center border-2 border-teal-500/30 flex flex-col items-center">
<div class="w-20 h-20 bg-teal-500/20 text-teal-400 rounded-full flex items-center justify-center text-4xl mb-6 border border-teal-500/30 shadow-lg shadow-teal-500/10">
<i class="fa-solid fa-trophy"></i>
</div>
<h2 id="modal-title" class="text-3xl font-bold text-teal-400 mb-2">เรียบร้อยแล้ว!</h2>
<p id="modal-desc" class="text-slate-300 mb-6">คุณจัดวางประโยคภาษาอังกฤษได้ถูกต้อง</p>
<div class="bg-indigo-950/40 p-4 rounded-xl border border-indigo-500/20 w-full mb-6 text-left">
<div class="flex justify-between text-sm mb-1">
<span class="text-slate-400">ประโยคภาษาไทย:</span>
<span id="summary-thai" class="text-indigo-300 font-bold">ฉัน รัก สุนัข</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-slate-400">ประโยคภาษาอังกฤษ:</span>
<span id="summary-english" class="text-teal-400 font-bold">I love dogs.</span>
</div>
</div>
<!-- Placeholder สำหรับปุ่ม Canvas ที่วาดทับ -->
<div id="next-stage-placeholder" class="w-full h-12 mt-2"></div>
</div>
</div>
<!-- ================= GAME OVER / ALL CLEAR SCREEN ================= -->
<div id="screen-gameover" class="hidden absolute inset-0 z-20 bg-radial from-indigo-950 to-black flex items-center justify-center p-4">
<div class="glass-panel max-w-lg w-full p-8 rounded-3xl text-center border-2 border-amber-500/30 flex flex-col items-center">
<div class="w-24 h-24 bg-amber-500/20 text-amber-400 rounded-2xl flex items-center justify-center text-5xl mb-6 shadow-xl border border-amber-500/20 animate-pulse">
<i class="fa-solid fa-crown"></i>
</div>
<h1 class="text-4xl font-black text-amber-400 mb-2 tracking-wide font-['Chakra_Petch']">ALL CLEAR!</h1>
<p class="text-gray-300 text-lg mb-6">ยินดีด้วย! คุณเก่งภาษาอังกฤษมาก และเรียงประโยคได้ครบถ้วน!</p>
<!-- Total Score -->
<div class="bg-slate-900/60 border border-slate-800 p-6 rounded-2xl w-full mb-8 flex justify-around">
<div class="text-center">
<div class="text-xs text-slate-400 uppercase tracking-widest mb-1">คะแนนรวมทั้งหมด</div>
<div id="gameover-score" class="text-4xl font-extrabold text-teal-400">300</div>
</div>
<div class="border-r border-slate-800"></div>
<div class="text-center">
<div class="text-xs text-slate-400 uppercase tracking-widest mb-1">ผ่านด่านการเล่น</div>
<div class="text-4xl font-extrabold text-indigo-400">3 / 3</div>
</div>
</div>
<!-- Placeholder สำหรับปุ่ม Canvas ที่วาดทับ -->
<div id="restart-placeholder" class="w-full h-12 mt-2"></div>
</div>
</div>
<!-- ================= JAVASCRIPT LOGIC ================= -->
<script>
// --- LEVEL DATA CONFIGURATION ---
const levels = [
{
thai: "ฉัน รัก สุนัข",
english: ["I", "love", "dogs"],
emojis: { "I": "🙋♂️", "love": "❤️", "dogs": "🐕" },
points: 100
},
{
thai: "แมว ตัวนั้น น่ารัก",
english: ["The", "cat", "is", "cute"],
emojis: { "The": "👈", "cat": "🐈", "is": "✨", "cute": "🥰" },
points: 100
},
{
thai: "นก บิน อยู่ใน ท้องฟ้า",
english: ["Birds", "fly", "in", "sky"],
emojis: { "Birds": "🐦", "fly": "💸", "in": "📥", "sky": "☁️" },
points: 100
}
];
// --- GAME ENGINE STATE ---
let currentLevelIdx = 0;
let score = 0;
let gameActive = false;
let isArOverlayReady = false;
let selectedWord = null; // คำศัพท์ที่กำลังลากด้วย AR หรือเมาส์
let draggingViaMouse = false;
// Elements
const screenStart = document.getElementById('screen-start');
const instructionOverlay = document.getElementById('instruction-overlay');
const arContainer = document.getElementById('ar-container');
const modalClear = document.getElementById('modal-clear');
const screenGameOver = document.getElementById('screen-gameover');
const btnStart = document.getElementById('btn-start');
const btnToggleCam = document.getElementById('btn-toggle-cam');
const hudLevel = document.getElementById('hud-level');
const hudScore = document.getElementById('hud-score');
const thaiHint = document.getElementById('thai-hint');
const slotsContainer = document.getElementById('slots-container');
const handMissingTip = document.getElementById('hand-missing-tip');
const camStatus = document.getElementById('cam-status');
const statusText = document.getElementById('status-text');
const statusPing = document.getElementById('status-ping');
const statusDot = document.getElementById('status-dot');
// Canvas Setup
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
const video = document.getElementById('webcam');
// Size responsiveness
function resizeCanvas() {
canvas.width = canvas.parentElement.clientWidth;
canvas.height = canvas.parentElement.clientHeight;
}
window.addEventListener('resize', () => {
resizeCanvas();
recalculateSlotsAndButtons();
});
resizeCanvas();
// Sound FX helpers
function playSfx(id) {
const audio = document.getElementById(id);
if (audio) {
audio.currentTime = 0;
audio.play().catch(e => console.log('Audio error:', e));
}
}
// --- AR / CURSOR LOGIC VARIABLES ---
let handDetected = false;
let cursorX = canvas.width / 2;
let cursorY = canvas.height / 2;
let targetCursorX = canvas.width / 2;
let targetCursorY = canvas.height / 2;
// Smooth movement coefficient for interpolation
const lerpCoeff = 0.25;
let isPinching = false;
let lastPinchState = false;
let handSkeletonPoints = [];
// Status visual control (0: Normal, 1: Hover, 2: Pinch Fail, 3: Capture Success)
let cursorVisualState = 0;
const CURSOR_NORMAL = 0;
const CURSOR_HOVER = 1;
const CURSOR_PINCH_FAIL = 2;
const CURSOR_SUCCESS = 3;
// --- GAME CARD OBJECTS ---
let wordCards = [];
let dropSlots = [];
class WordCard {
constructor(text, emoji, x, y, width = 120, height = 110) {
this.text = text;
this.emoji = emoji;
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.isBeingHeld = false;
this.isPlaced = false;
this.assignedSlotIdx = -1;
this.offsetX = 0;
this.offsetY = 0;
this.angle = (Math.random() - 0.5) * 8; // Random visual tilt
}
draw(context, hovered) {
context.save();
context.translate(this.x + this.width/2, this.y + this.height/2);
context.rotate(this.angle * Math.PI / 180);
// Glassmorphism 2.5D visual
// Shadow and depth layer
context.fillStyle = this.isBeingHeld
? 'rgba(20, 184, 166, 0.5)'
: 'rgba(99, 102, 241, 0.35)';
const depth = 8;
context.beginPath();
context.roundRect(-this.width/2, -this.height/2 + depth, this.width, this.height, 16);
context.fill();
// Front face card
const gradient = context.createLinearGradient(-this.width/2, -this.height/2, this.width/2, this.height/2);
if (this.isBeingHeld) {
gradient.addColorStop(0, 'rgba(20, 184, 166, 0.95)');
gradient.addColorStop(1, 'rgba(13, 148, 136, 0.95)');
} else if (hovered) {
gradient.addColorStop(0, 'rgba(251, 191, 36, 0.85)');
gradient.addColorStop(1, 'rgba(245, 158, 11, 0.85)');
} else {
gradient.addColorStop(0, 'rgba(30, 41, 59, 0.85)');
gradient.addColorStop(1, 'rgba(15, 23, 42, 0.9)');
}
context.fillStyle = gradient;
// Border
context.strokeStyle = this.isBeingHeld
? 'rgba(255,255,255,0.8)'
: (hovered ? 'rgba(251, 191, 36, 0.6)' : 'rgba(255, 255, 255, 0.15)');
context.lineWidth = 2;
context.beginPath();
context.roundRect(-this.width/2, -this.height/2, this.width, this.height, 16);
context.fill();
context.stroke();
// Drawing Emoji
context.font = '36px Arial';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText(this.emoji, 0, -14);
// Drawing English text
context.font = 'bold 16px Sarabun';
context.fillStyle = this.isBeingHeld ? '#ffffff' : (hovered ? '#fffbeb' : '#22d3ee');
context.fillText(this.text, 0, 26);
context.restore();
}
containsPoint(px, py) {
return px >= this.x && px <= this.x + this.width &&
py >= this.y && py <= this.y + this.height;
}
}
// --- CANVAS INTERACTIVE BUTTON CLASS (สำหรับวาดทับและคลิก/จีบได้) ---
class CanvasButton {
constructor(placeholderId, text, baseColor, hoverColor, shadowColor, action) {
this.placeholderId = placeholderId;
this.text = text;
this.baseColor = baseColor;
this.hoverColor = hoverColor;
this.shadowColor = shadowColor;
this.action = action;
this.x = 0;
this.y = 0;
this.width = 0;
this.height = 0;
this.visible = false;
}
updatePosition() {
const el = document.getElementById(this.placeholderId);
if (el && this.visible) {
const rect = el.getBoundingClientRect();
const containerRect = canvas.getBoundingClientRect();
this.x = rect.left - containerRect.left;
this.y = rect.top - containerRect.top;
this.width = rect.width;
this.height = rect.height;
}
}
draw(context, cursorX, cursorY) {
if (!this.visible) return;
this.updatePosition(); // อัปเดตตำแหน่งจริงตาม Responsive HTML
if (this.width === 0) return;
const isHovered = cursorX >= this.x && cursorX <= this.x + this.width &&
cursorY >= this.y && cursorY <= this.y + this.height;
context.save();
// Shadow 2.5D
context.fillStyle = this.shadowColor;
context.beginPath();
context.roundRect(this.x, this.y + 6, this.width, this.height, 16);
context.fill();
// Main button
const grad = context.createLinearGradient(this.x, this.y, this.x, this.y + this.height);
if (isHovered) {
grad.addColorStop(0, this.hoverColor);
grad.addColorStop(1, this.baseColor);
} else {
grad.addColorStop(0, this.baseColor);
grad.addColorStop(1, this.hoverColor);
}
context.fillStyle = grad;
context.strokeStyle = '#ffffff';
context.lineWidth = 2.5;
context.beginPath();
context.roundRect(this.x, this.y, this.width, this.height, 16);
context.fill();
context.stroke();
// Text
context.fillStyle = '#ffffff';
context.font = 'bold 16px Sarabun';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText(this.text, this.x + this.width / 2, this.y + this.height / 2);
context.restore();
if (isHovered) {
cursorVisualState = CURSOR_HOVER;
}
}
containsPoint(px, py) {
return this.visible &&
px >= this.x && px <= this.x + this.width &&
py >= this.y && py <= this.y + this.height;
}
}
// --- INSTANTIATING INTERACTIVE CANVAS BUTTONS ---
const canvasButtons = {
arReady: new CanvasButton(
'ar-ready-placeholder',
'เริ่มทำงานเว็บแคม 📸',
'#10b981', // emerald-500
'#059669', // emerald-600
'rgba(5, 150, 105, 0.4)',
() => {
instructionOverlay.classList.add('hidden');
canvasButtons.arReady.visible = false;
arReadyButtonObj.visible = true;
initWebcamAndHands();
gameActive = true;
}
),
nextStage: new CanvasButton(
'next-stage-placeholder',
'เข้าสู่ด่านต่อไป 🚀',
'#14b8a6', // teal-500
'#0d9488', // teal-600
'rgba(13, 148, 136, 0.4)',
() => {
modalClear.classList.add('hidden');
canvasButtons.nextStage.visible = false;
if (currentLevelIdx < levels.length - 1) {
currentLevelIdx++;
initStage(currentLevelIdx);
gameActive = true;
} else {
// จบทุกด่าน เคลียร์เกม!
gameActive = false;
document.getElementById('gameover-score').innerText = score;
screenGameOver.classList.remove('hidden');
canvasButtons.restart.visible = true;
playSfx('snd-clear');
}
}
),
restart: new CanvasButton(
'restart-placeholder',
'เล่นใหม่อีกครั้ง 🔄',
'#f59e0b', // amber-500
'#d97706', // amber-600
'rgba(217, 119, 6, 0.4)',
() => {
screenGameOver.classList.add('hidden');
canvasButtons.restart.visible = false;
currentLevelIdx = 0;
score = 0;
hudScore.innerText = score;
initStage(currentLevelIdx);
gameActive = true;
}
)
};
// --- RECALCULATE POSITIONS ON RESIZE ---
function recalculateSlotsAndButtons() {
if (dropSlots.length > 0) {
levels[currentLevelIdx].english.forEach((word, idx) => {
const slotEl = document.getElementById(`html-slot-${idx}`);
if (slotEl) {
const rect = slotEl.getBoundingClientRect();
const containerRect = canvas.getBoundingClientRect();
const slot = dropSlots.find(s => s.idx === idx);
if (slot) {
slot.x = rect.left - containerRect.left;
slot.y = rect.top - containerRect.top;
slot.width = rect.width;
slot.height = rect.height;
if (slot.assignedCard) {
// ยึดการ์ดให้อยู่กึ่งกลางสล็อตและวาดทับด้านบนสล็อตเสมอ
slot.assignedCard.x = slot.x + (slot.width - slot.assignedCard.width) / 2;
slot.assignedCard.y = slot.y + (slot.height - slot.assignedCard.height) / 2;
}
}
}
});
}
for (const key in canvasButtons) {
canvasButtons[key].updatePosition();
}
}
// --- INITIALIZE STAGE ---
function initStage(stageIdx) {
const stage = levels[stageIdx];
hudLevel.innerText = `${stageIdx + 1} / ${levels.length}`;
thaiHint.innerText = stage.thai;
wordCards = [];
dropSlots = [];
selectedWord = null;
isPinching = false;
slotsContainer.innerHTML = '';
const cardWidth = 110;
const cardHeight = 100;
stage.english.forEach((word, idx) => {
const slotEl = document.createElement('div');
slotEl.className = "flex-1 flex flex-col items-center justify-center border-2 border-dashed border-indigo-500/20 bg-indigo-950/20 rounded-xl relative overflow-hidden transition-all duration-300";
slotEl.style.minWidth = `${cardWidth + 10}px`;
slotEl.style.height = `${cardHeight + 10}px`;
slotEl.id = `html-slot-${idx}`;
slotEl.innerHTML = `
<div class="text-xs text-slate-500 pointer-events-none uppercase tracking-widest">ตำแหน่ง ${idx + 1}</div>
<div class="text-xs font-semibold text-slate-400 mt-1 pointer-events-none">[...]</div>
`;
slotsContainer.appendChild(slotEl);
});
const wordsToShuffle = [...stage.english];
for (let i = wordsToShuffle.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[wordsToShuffle[i], wordsToShuffle[j]] = [wordsToShuffle[j], wordsToShuffle[i]];
}
const padding = 50;
const screenW = canvas.width;
const usableWidth = screenW - (padding * 2);
const step = usableWidth / (wordsToShuffle.length + 1);
wordsToShuffle.forEach((word, idx) => {
const startX = padding + step * (idx + 1) - (cardWidth / 2);
const startY = 150 + (Math.random() * 50);
const emoji = stage.emojis[word] || "❓";
wordCards.push(new WordCard(word, emoji, startX, startY, cardWidth, cardHeight));
});
setTimeout(() => {
stage.english.forEach((word, idx) => {
const slotEl = document.getElementById(`html-slot-${idx}`);
if (slotEl) {
const rect = slotEl.getBoundingClientRect();
const containerRect = canvas.getBoundingClientRect();
dropSlots.push({
idx: idx,
correctWord: word,
assignedCard: null,
x: rect.left - containerRect.left,
y: rect.top - containerRect.top,
width: rect.width,
height: rect.height
});
}
});
}, 500);
}
// --- GAME AR INTERACTION ENGINE BUTTON (IN CANVAS AR OVERLAY READY) ---
let arReadyButtonObj = {
x: 0,
y: 0,
width: 220,
height: 60,
text: "เริ่มเกม! 👉 [เริ่มด่าน 1]",
active: false,
visible: false
};
function drawARStartButton() {
if (!arReadyButtonObj.visible) return;
arReadyButtonObj.width = 240;
arReadyButtonObj.height = 65;
arReadyButtonObj.x = canvas.width / 2 - arReadyButtonObj.width / 2;
arReadyButtonObj.y = canvas.height / 2 - arReadyButtonObj.height / 2 + 50;
const isHovered = isCursorOverRect(cursorX, cursorY, arReadyButtonObj);
ctx.save();
ctx.fillStyle = isHovered ? 'rgba(16, 185, 129, 0.4)' : 'rgba(15, 23, 42, 0.4)';
ctx.beginPath();
ctx.roundRect(arReadyButtonObj.x, arReadyButtonObj.y + 6, arReadyButtonObj.width, arReadyButtonObj.height, 16);
ctx.fill();
const grad = ctx.createLinearGradient(arReadyButtonObj.x, arReadyButtonObj.y, arReadyButtonObj.x + arReadyButtonObj.width, arReadyButtonObj.y + arReadyButtonObj.height);
if (isHovered) {
grad.addColorStop(0, '#10b981');
grad.addColorStop(1, '#059669');
} else {
grad.addColorStop(0, '#3b82f6');
grad.addColorStop(1, '#1d4ed8');
}
ctx.fillStyle = grad;
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.roundRect(arReadyButtonObj.x, arReadyButtonObj.y, arReadyButtonObj.width, arReadyButtonObj.height, 16);
ctx.fill();
ctx.stroke();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 18px Sarabun';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(arReadyButtonObj.text, arReadyButtonObj.x + arReadyButtonObj.width / 2, arReadyButtonObj.y + arReadyButtonObj.height / 2);
ctx.restore();
if (isHovered) {
cursorVisualState = CURSOR_HOVER;
}
}
function isCursorOverRect(cx, cy, rect) {
return cx >= rect.x && cx <= rect.x + rect.width &&
cy >= rect.y && cy <= rect.y + rect.height;
}
// --- GENERAL GAME START ---
btnStart.addEventListener('click', () => {
playSfx('snd-click');
screenStart.classList.add('hidden');
instructionOverlay.classList.remove('hidden');
canvasButtons.arReady.visible = true; // เปิดปุ่ม AR Start
});
btnToggleCam.addEventListener('click', () => {
playSfx('snd-click');
if (video.style.display === 'none') {
video.style.display = 'block';
} else {
video.style.display = 'none';
}
});
// --- PIPELINE OF MEDIAPIPE HANDS ---
let camera = null;
const hands = new Hands({
locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`
});
hands.setOptions({
maxNumHands: 1,
modelComplexity: 1,
minDetectionConfidence: 0.55,
minTrackingConfidence: 0.55
});
hands.onResults(onHandResults);
function initWebcamAndHands() {
statusText.innerText = "กำลังติดตั้งกล้อง...";
camera = new Camera(video, {
onFrame: async () => {
await hands.send({ image: video });
},
width: 1280,
height: 720
});
camera.start()
.then(() => {
camStatus.classList.remove('text-rose-400', 'bg-rose-950/20');
camStatus.classList.add('text-emerald-400', 'bg-emerald-950/20');
statusDot.classList.remove('bg-rose-500');
statusDot.classList.add('bg-emerald-500');
statusPing.classList.remove('bg-rose-400');
statusPing.classList.add('bg-emerald-400');
statusText.innerText = "กล้องทำงานปกติ (AR-AI Active)";
isArOverlayReady = true;
})
.catch(err => {
console.error("Camera access failed, running fallback mode only.", err);
statusText.innerText = "กล้องทำงานล้มเหลว (Fallback Mouse Mode)";
isArOverlayReady = false;
});
}
function onHandResults(results) {
if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
handDetected = true;
handMissingTip.classList.add('hidden');
const landmarks = results.multiHandLandmarks[0];
handSkeletonPoints = landmarks;
const rawX = 1 - landmarks[8].x;
const rawY = landmarks[8].y;
targetCursorX = rawX * canvas.width;
targetCursorY = rawY * canvas.height;
const thumbTip = landmarks[4];
const indexTip = landmarks[8];
const dx = (thumbTip.x - indexTip.x) * canvas.width;
const dy = (thumbTip.y - indexTip.y) * canvas.height;
const pinchDistance = Math.sqrt(dx * dx + dy * dy);
isPinching = pinchDistance < 55;
} else {
handDetected = false;
handSkeletonPoints = [];
if (gameActive && isArOverlayReady) {
handMissingTip.classList.remove('hidden');
}
}
}
// --- GAME PHYSICAL INTERACTION LOOP (Pinch Hand Trigger & Physics Engine) ---
function handleGrabbingLogic() {
const clicked = isPinching && !lastPinchState;
const released = !isPinching && lastPinchState;
cursorVisualState = CURSOR_NORMAL;
// 1. ตรวจจับการกดปุ่ม Canvas ทั่วไปก่อน (เช่น ปุ่มไปด่านต่อไป หรือเริ่มใหม่)
for (const key in canvasButtons) {
const btn = canvasButtons[key];
if (btn.visible) {
if (btn.containsPoint(cursorX, cursorY)) {
cursorVisualState = CURSOR_HOVER;
if (clicked) {
playSfx('snd-click');
btn.action();
lastPinchState = isPinching;
return;
}
}
}
}
// 2. ปุ่ม AR Canvas Start Ready Button
if (arReadyButtonObj.visible) {
if (isCursorOverRect(cursorX, cursorY, arReadyButtonObj)) {
cursorVisualState = CURSOR_HOVER;
if (clicked) {
playSfx('snd-click');
arReadyButtonObj.visible = false;
initStage(currentLevelIdx);
}
}
lastPinchState = isPinching;
return;
}
// 3. จัดการกับการจับ/ลากการ์ดคำศัพท์
let cardHovered = false;
wordCards.forEach(card => {
if (!card.isPlaced) {
const hover = card.containsPoint(cursorX, cursorY);
if (hover) {
cardHovered = true;
}
if (hover && clicked && !selectedWord) {
selectedWord = card;
card.isBeingHeld = true;
card.offsetX = cursorX - card.x;
card.offsetY = cursorY - card.y;
playSfx('snd-click');
}
}
});
if (selectedWord && isPinching) {
cursorVisualState = CURSOR_SUCCESS;
selectedWord.x = cursorX - selectedWord.offsetX;
selectedWord.y = cursorY - selectedWord.offsetY;
}
if (released && selectedWord) {
selectedWord.isBeingHeld = false;
evaluateCardDrop(selectedWord);
selectedWord = null;
}
if (selectedWord) {
cursorVisualState = CURSOR_SUCCESS;
} else if (cardHovered) {
cursorVisualState = CURSOR_HOVER;
} else if (isPinching && !selectedWord) {
cursorVisualState = CURSOR_PINCH_FAIL;
}
lastPinchState = isPinching;
}
// --- EVALUATE SLOT ALIGNMENT ---
function evaluateCardDrop(card) {
let placedInCorrectSlot = false;
for (let i = 0; i < dropSlots.length; i++) {
const slot = dropSlots[i];
const centerX = card.x + card.width / 2;
const centerY = card.y + card.height / 2;
const insideSlot = (
centerX >= slot.x && centerX <= slot.x + slot.width &&
centerY >= slot.y && centerY <= slot.y + slot.height
);
if (insideSlot) {
if (slot.correctWord === card.text) {
placedInCorrectSlot = true;
card.isPlaced = true;
card.assignedSlotIdx = i;
// วางและจัดพิกัดกึ่งกลางสล็อตพอดี (เนื่องจาก Canvas อยู่เลเยอร์ z-30 ด้านบนสุด การ์ดจะวาดทับเหนือช่อง)
card.x = slot.x + (slot.width - card.width) / 2;
card.y = slot.y + (slot.height - card.height) / 2;
card.angle = 0;
slot.assignedCard = card;
const htmlSlot = document.getElementById(`html-slot-${i}`);
if (htmlSlot) {
htmlSlot.innerHTML = `
<div class="text-xs text-emerald-400 font-bold tracking-wider"><i class="fa-solid fa-circle-check"></i> ถูกต้อง</div>
<div class="text-lg font-bold text-white mt-1">${card.text}</div>
`;
htmlSlot.className = "flex-1 flex flex-col items-center justify-center border-2 border-emerald-500/50 bg-emerald-950/30 rounded-xl relative overflow-hidden transition-all duration-300 scale-105 shadow-lg shadow-emerald-500/10";
}
playSfx('snd-success');
break;
} else {
playSfx('snd-fail');
card.y -= 30;
break;
}
}
}
const allDone = dropSlots.every(slot => slot.assignedCard !== null);
if (allDone) {
triggerStageClear();
}
}
function triggerStageClear() {
gameActive = false;
score += levels[currentLevelIdx].points;
hudScore.innerText = score;
const currentStage = levels[currentLevelIdx];
document.getElementById('summary-thai').innerText = currentStage.thai;
document.getElementById('summary-english').innerText = currentStage.english.join(' ') + '.';
// ปรับเปลี่ยนข้อความบนปุ่มด่านต่อไปใน Canvas
if (currentLevelIdx < levels.length - 1) {
canvasButtons.nextStage.text = `ลุยต่อด่านที่ ${currentLevelIdx + 2} 🚀`;
} else {
canvasButtons.nextStage.text = `ดูผลการประเมินการเรียน 🏆`;
}
setTimeout(() => {
modalClear.classList.remove('hidden');
canvasButtons.nextStage.visible = true; // เปิดปุ่มเมนูด่านต่อไปให้ AR ตรวจจับ
playSfx('snd-clear');
}, 700);
}
// --- FALLBACK DRAG MOUSE LOGIC ---
canvas.addEventListener('mousedown', (e) => {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
cursorX = mx;
cursorY = my;
// ตรวจสอบการกดปุ่มทางอ้อมด้วยเมาส์
for (const key in canvasButtons) {
const btn = canvasButtons[key];
if (btn.visible && btn.containsPoint(mx, my)) {
playSfx('snd-click');
btn.action();
return;
}
}
if (arReadyButtonObj.visible && isCursorOverRect(mx, my, arReadyButtonObj)) {
playSfx('snd-click');
arReadyButtonObj.visible = false;
initStage(currentLevelIdx);
return;
}
if (gameActive) {
wordCards.forEach(card => {
if (!card.isPlaced && card.containsPoint(mx, my) && !selectedWord) {
selectedWord = card;
draggingViaMouse = true;
card.isBeingHeld = true;
card.offsetX = mx - card.x;
card.offsetY = my - card.y;
playSfx('snd-click');
}
});
}
});
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
if (!handDetected) {
cursorX = mx;
cursorY = my;
}
if (draggingViaMouse && selectedWord) {
selectedWord.x = mx - selectedWord.offsetX;
selectedWord.y = my - selectedWord.offsetY;
}
});
window.addEventListener('mouseup', () => {
if (draggingViaMouse && selectedWord) {
selectedWord.isBeingHeld = false;
evaluateCardDrop(selectedWord);
selectedWord = null;
draggingViaMouse = false;
}
});
// Touch Support for Mobile/Tablet Fallback
canvas.addEventListener('touchstart', (e) => {
if (e.touches.length > 0) {
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
const mx = touch.clientX - rect.left;
const my = touch.clientY - rect.top;
cursorX = mx;
cursorY = my;
for (const key in canvasButtons) {
const btn = canvasButtons[key];
if (btn.visible && btn.containsPoint(mx, my)) {
playSfx('snd-click');
btn.action();
return;
}
}
if (arReadyButtonObj.visible && isCursorOverRect(mx, my, arReadyButtonObj)) {
playSfx('snd-click');
arReadyButtonObj.visible = false;
initStage(currentLevelIdx);
return;
}
if (gameActive) {
wordCards.forEach(card => {
if (!card.isPlaced && card.containsPoint(mx, my) && !selectedWord) {
selectedWord = card;
draggingViaMouse = true;
card.isBeingHeld = true;
card.offsetX = mx - card.x;
card.offsetY = my - card.y;
playSfx('snd-click');
}
});
}
}
});
canvas.addEventListener('touchmove', (e) => {
if (e.touches.length > 0 && draggingViaMouse && selectedWord) {
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
const mx = touch.clientX - rect.left;
const my = touch.clientY - rect.top;
cursorX = mx;
cursorY = my;
selectedWord.x = mx - selectedWord.offsetX;
selectedWord.y = my - selectedWord.offsetY;
}
});
canvas.addEventListener('touchend', () => {
if (draggingViaMouse && selectedWord) {
selectedWord.isBeingHeld = false;
evaluateCardDrop(selectedWord);
selectedWord = null;
draggingViaMouse = false;
}
});
// --- RENDER GAME CANVAS LOOP ---
function gameRenderLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Lerp smooth tracking
cursorX += (targetCursorX - cursorX) * lerpCoeff;
cursorY += (targetCursorY - cursorY) * lerpCoeff;
// วาดปุ่ม AR Ready ด่านแรกบน Canvas
drawARStartButton();
// วาดปุ่ม Interactive บน Canvas (เปิดเว็บแคม, ผ่านด่าน, เริ่มใหม่)
for (const key in canvasButtons) {
canvasButtons[key].draw(ctx, cursorX, cursorY);
}
// วาดคำศัพท์
if (gameActive && !arReadyButtonObj.visible) {
wordCards.forEach(card => {
const isCardHovered = card.containsPoint(cursorX, cursorY) && !card.isPlaced;
card.draw(ctx, isCardHovered);
});
}
// วาดโครงสร้างมือ
if (handDetected && handSkeletonPoints.length > 0) {
drawHandSkeleton(ctx, handSkeletonPoints);
}
// วาด Cursor ชี้ตำแหน่งนิ้ว
const isAnyButtonVisible = Object.values(canvasButtons).some(b => b.visible);
if (gameActive || arReadyButtonObj.visible || isAnyButtonVisible) {
drawCursor(ctx, cursorX, cursorY, cursorVisualState);
}
// จัดการกับการวิเคราะห์ท่าทางนิ้ว
if (handDetected && (gameActive || arReadyButtonObj.visible || isAnyButtonVisible)) {
handleGrabbingLogic();
}
requestAnimationFrame(gameRenderLoop);
}
function drawHandSkeleton(context, landmarks) {
context.save();
context.lineWidth = 3;
context.strokeStyle = 'rgba(99, 102, 241, 0.4)';
context.fillStyle = '#22d3ee';
const getPt = (idx) => {
return {
x: (1 - landmarks[idx].x) * canvas.width,
y: landmarks[idx].y * canvas.height
};
};
const fingers = [
[0, 1, 2, 3, 4],
[0, 5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16],
[0, 17, 18, 19, 20]
];
const palm = [5, 9, 13, 17];
fingers.forEach(line => {
context.beginPath();
for (let i = 0; i < line.length; i++) {
const pt = getPt(line[i]);
if (i === 0) context.moveTo(pt.x, pt.y);
else context.lineTo(pt.x, pt.y);
}
context.stroke();
});
context.beginPath();
const startPt = getPt(5);
context.moveTo(startPt.x, startPt.y);
palm.forEach(jointIdx => {
const pt = getPt(jointIdx);
context.lineTo(pt.x, pt.y);
});
context.stroke();
for (let i = 0; i < landmarks.length; i++) {
const pt = getPt(i);
if (i === 4 || i === 8) {
context.fillStyle = '#f59e0b';
context.beginPath();
context.arc(pt.x, pt.y, 6, 0, 2 * Math.PI);
context.fill();
} else {
context.fillStyle = 'rgba(34, 211, 238, 0.8)';
context.beginPath();
context.arc(pt.x, pt.y, 4, 0, 2 * Math.PI);
context.fill();
}
}
context.restore();
}
function drawCursor(context, cx, cy, state) {
context.save();
switch (state) {
case CURSOR_NORMAL:
context.strokeStyle = '#22d3ee';
context.lineWidth = 3;
context.beginPath();
context.arc(cx, cy, 14, 0, 2 * Math.PI);
context.stroke();
context.strokeStyle = 'rgba(34, 211, 238, 0.3)';
context.lineWidth = 1;
context.beginPath();
context.arc(cx, cy, 22 + Math.sin(Date.now() / 150) * 3, 0, 2 * Math.PI);
context.stroke();
context.fillStyle = '#22d3ee';
context.beginPath();
context.arc(cx, cy, 4, 0, 2 * Math.PI);
context.fill();
break;
case CURSOR_HOVER:
context.strokeStyle = '#fbbf24';
context.lineWidth = 3;
context.setLineDash([4, 4]);
context.beginPath();
context.arc(cx, cy, 18, 0, 2 * Math.PI);
context.stroke();
context.fillStyle = '#fbbf24';
context.setLineDash([]);
context.beginPath();
context.arc(cx, cy, 5, 0, 2 * Math.PI);
context.fill();
break;
case CURSOR_PINCH_FAIL:
context.strokeStyle = '#ef4444';
context.lineWidth = 3;
context.beginPath();
context.arc(cx, cy, 12, 0, 2 * Math.PI);
context.stroke();
context.strokeStyle = '#ef4444';
context.lineWidth = 2;
context.beginPath();
context.moveTo(cx - 5, cy - 5);
context.lineTo(cx + 5, cy + 5);
context.moveTo(cx + 5, cy - 5);
context.lineTo(cx - 5, cy + 5);
context.stroke();
break;
case CURSOR_SUCCESS:
context.strokeStyle = '#10b981';
context.lineWidth = 4;
context.beginPath();
context.arc(cx, cy, 16, 0, 2 * Math.PI);
context.stroke();
context.fillStyle = '#10b981';
context.font = 'bold 12px Arial';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText('✔️', cx + 10, cy - 10);
context.fillStyle = '#10b981';
context.beginPath();
context.arc(cx, cy, 6, 0, 2 * Math.PI);
context.fill();
break;
}
context.restore();
}
window.onload = function() {
gameRenderLoop();
};
</script>
</body>
</html>
ผลลัพธ์ คลิก! ทดลองเกม (เกมเรียงประโยคภาษาอังกฤษ)
ผลลัพธ์ คลิก! ทดลองเกม (เรียงพยัชนะคำศัพท์ภาษาอังกฤษ)
📌Workshop02 : Chatgemini สร้างเกม AR ด้วย AI (คณิตศาสตร์)
คณิตคิดเลขเร็ว AR (Kids Edition)
สร้างเกมคณิตศาสตร์แบบ AR (Augmented Reality) สำหรับเด็กระดับประถมศึกษา โดยใช้การตรวจจับมือผ่านเว็บแคม ให้ผู้เล่นสามารถเลือกคำตอบด้วยการชี้นิ้วแทนการคลิกเมาส์
เกมต้องมีลักษณะดังนี้
สนุก น่ารัก และดึงดูดเด็ก
ใช้งานง่าย ไม่ซับซ้อน
รองรับ Desktop และ Mobile
แสดงผลลื่นไหล
ไม่มีปัญหา Layout ล้นหน้าจอ
ทำงานได้จากไฟล์ HTML เพียงไฟล์เดียว
คุณคือผู้เชี่ยวชาญด้าน
Creative Web Application Development
Educational Game Design
UI/UX Design สำหรับเด็ก
Computer Vision ด้วย MediaPipe
HTML5, CSS3 และ JavaScript
Tailwind CSS
Tone.js Audio Synthesis
หน้าที่ของคุณคือสร้างโค้ด HTML5 แบบ Full Code สำหรับเกมนี้ให้พร้อมใช้งานทันที
ส่งกลับมาเป็น
HTML5 Full Source Code เท่านั้น
ห้าม
ตัดโค้ดออกบางส่วน
ใช้เครื่องหมาย ...
ใช้คำว่า "โค้ดส่วนที่เหลือ"
แบ่งเป็นหลายไฟล์
อธิบายหลักการเพิ่มเติม
ส่งเป็น pseudo-code
ต้องสามารถ Copy → Save → Run ได้ทันที
สร้างระบบทั้งหมดภายในไฟล์เดียว
อนุญาตให้มี
HTML
CSS
JavaScript
ภายในไฟล์เดียวเท่านั้น
<script src="https://cdn.tailwindcss.com"></script>
https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js
https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js
https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js
Mali
แสดงภาพจากกล้องเว็บแคมเต็มหน้าจอ
คุณสมบัติ
เปิดกล้องอัตโนมัติเมื่อได้รับอนุญาต
แสดงภาพแบบ Mirror
ครอบคลุมพื้นที่เล่นเกมทั้งหมด
ใช้ MediaPipe Hands
ต้องแสดง
จุด Landmark
เส้น Skeleton ของมือ
Overlay ทับบนภาพกล้อง
แบบ Real-Time
ใช้ตำแหน่ง
Index Finger Tip
เป็นตัวชี้หลัก
รูปแบบ Cursor
รูปดาว
สีทองอมชมพู
มี Glow Effect
เคลื่อนไหวลื่นไหล
ติดตามปลายนิ้วแบบ Real-Time
องค์ประกอบที่สามารถเลือกได้ต้องมี Class
.hand-interact
หลักการทำงาน
ผู้เล่นชี้นิ้วไปยังปุ่ม
ระบบตรวจสอบการชนกันระหว่าง Cursor และปุ่ม
หากชี้ค้างเกิน 1.2 วินาที
ให้แสดง
วงแหวน SVG Progress
Animation นับเวลารอบวง
เมื่อครบเวลา
ให้
element.click()
อัตโนมัติ
หากเลื่อนนิ้วออกก่อนครบเวลา
ให้รีเซ็ต Progress ทันที
ใช้ Tone.js เท่านั้น
ห้ามโหลดไฟล์เสียงภายนอกทุกชนิด
เช่น
MP3
WAV
OGG
เมื่อเริ่มชี้ปุ่ม
เมื่อตอบถูก
เมื่อตอบผิด
เมื่อจบเกม
ทุกฟังก์ชันเสียงต้องครอบด้วย
try {
// audio code
} catch(error){
console.error(error);
}
เพื่อป้องกัน JavaScript Error
ออกแบบให้เหมือนของเล่นนุ่มนิ่ม 3 มิติ
ใช้
rounded-[30px]
border-4 border-white
ทุกส่วนหลัก
พร้อม
เงาการ์ตูนหนา
สีสันสดใส
ขอบชัดเจน
ใช้สีสดใสแนวเด็ก
ตัวอย่าง
เหลืองสด
ฟ้าใส
ชมพูสด
ม่วงพาสเทล
เขียวมิ้นท์
ตัวเลือก 4 ปุ่ม
ใช้สีแตกต่างกันชัดเจน
ปุ่มละ 1 สี
เช่น
เหลือง
ฟ้า
ชมพู
ม่วง
ใช้ฟอนต์
Mali
ลักษณะ
อ่านง่าย
เป็นมิตร
เหมาะสำหรับเด็กประถม
ห้ามเกิด
Vertical Scroll
Horizontal Scroll
Overflow
Layout ตกขอบ
ในทุกขนาดหน้าจอ
ทุกหน้าจอหลักต้องใช้
height:100vh;
overflow:hidden;
ควบคุมความสูงด้วย
max-height:90vh;
หรือ
max-height:95vh;
Desktop
text-4xl
ถึง
text-5xl
Tablet
text-3xl
Mobile
text-2xl
กำหนด Padding
py-3
ถึง
py-4
เท่านั้น
เพื่อป้องกันปุ่มสูงเกินไป
เมื่อผู้เล่นตอบถูก
เล่น Animation
pop-correct
ลักษณะ
ขยาย
เด้ง
เรืองแสง
มีดาวเล็กกระจายรอบทิศทาง
เมื่อผู้เล่นตอบผิด
เล่น Animation
shake-wrong
ลักษณะ
สั่นซ้ายขวา
สั้นและสนุก
ไม่รุนแรง
เมื่อเล่นครบทุกข้อ
ปล่อย Confetti จากด้านบน
ใช้อีโมจิ
🎉 ⭐ 🌈 🎈 🎊
พร้อมเสียงฉลองจาก Tone.js
แบ่งเป็น 4 หน้าจอหลัก
หน้าจอเริ่มต้น
แสดง
กำลังโหลดระบบ
กำลังเปิดกล้อง
กำลังโหลด AI
ข้อความตัวอย่าง
กำลังเตรียมกล้อง...
กรุณาอนุญาตการใช้งานกล้อง
แสดง
ชื่อเกม
คำอธิบายสั้น
ปุ่มเริ่มเกมขนาดใหญ่
แสดง
ลำดับข้อ
คะแนน
โจทย์
ตัวเลือก 4 ตัวเลือก
ตัวอย่าง
ข้อที่ 3 / 5
คะแนน : 20
12 + 8 = ?
แสดง
คะแนนรวม
จำนวนข้อถูก
ข้อความให้กำลังใจ
พร้อม
Confetti
Celebration Sound
ปุ่มเล่นใหม่
เมื่อโหลดหน้าเว็บ
ให้สร้างคำถามจำนวน
50 ข้อ
เก็บใน Array
สุ่มจาก
บวก
ลบ
คูณ
หาร
คำตอบเป็นจำนวนเต็ม
ไม่มีข้อซ้ำ
เหมาะกับเด็กประถมศึกษา
เมื่อเริ่มเล่น
สุ่มเลือก
5 ข้อ
จากคลังคำถาม
แสดงสถานะ
ข้อที่ 1 / 5
ข้อที่ 2 / 5
ข้อที่ 3 / 5
ข้อที่ 4 / 5
ข้อที่ 5 / 5
แบบ Real-Time
โค้ดต้องมีคุณภาพระดับ Production Prototype
ประกอบด้วย
Clean Code
Modular Functions
Error Handling
Responsive Design
Mobile First
Comment อธิบายส่วนสำคัญ
Performance Optimization
Memory Management ที่เหมาะสม
Event Listener Management ที่ถูกต้อง
กรุณาสร้าง HTML5 Full Source Code สำหรับเกม "คณิตคิดเลขเร็ว AR (Kids Edition)" ตามข้อกำหนดทั้งหมดข้างต้น โดยใช้ Tailwind CSS, MediaPipe Hands และ Tone.js
ต้องสามารถเปิดใช้งานได้ทันทีจากไฟล์ HTML เพียงไฟล์เดียว และต้องไม่มีปัญหา Layout Overflow หรือ Scrollbar ในทุกหน้าจอของเกม
คลิก! ทดลองเกม (คณิตศาสตร์คิดเลขเร็ว)
1. สร้างเกม AR เรื่อง การบวก ระดับประถมศึกษาปีที่ 1 มีความยาก ปานกลาง ง่าย โดยใช้นิ้วชี้และนิ้วโป้งเลือกตอบ เปิดกล้องเพื่อจับฝ่ามือ
2. ปรับเซนเซอร์การเลือกตอบให้ตรงกับมือ (เนื่องจาก 1 ไม่สมบูรณ์)
📌Workshop03 : Chatgemini สร้างเกม AR ด้วย AI (ภาษาไทย)
สร้าง เกม AR (Augmented Reality) วิชาภาษาไทย การจำแนกคำนามราชศัพท์และกริยาราชาศัพท์ โดยใช้ HTML, CSS (Tailwind) และ JavaScript ให้อยู่ในไฟล์ index.html ไฟล์เดียวจบ โดยใช้ไลบรารี MediaPipe Hands สำหรับการตรวจจับมือ
รายละเอียดของเกม:
คอนเซปต์เกม: เป็นเกม 'การจำแนกคำนามราชศัพท์ - กริยาราชาศัพท์ โดย ให้แยกคำ แต่ละประเภท ผู้เล่นจะต้องใช้มือผ่านกล้องเว็บแคม เพื่อหยิบคำที่ลอยคล้ายๆกับ ฟองอากาศ ลอยเคลื่อนไหวไปมา บริเวณหน้าจอ โดยใช้นิ้วจับมาใส่ในช่อง โดยแบ่งกลุ่มให้ถูกต้อง แบบการจำแนกคำนามราชศัพท์ – คำกริยาราชาศัพท์ อย่างละ 20 คำ รวม 40
คำ
ประกอบด้วย
🟢 คำนามราชศัพท์ ได้แก่
1. พระบรมมหาราชวัง
2. พระบรมราชโองการ
3. พระบรมราโชวาท
4. พระบรมราชชนก
5. พระราชดำริ
6. พระราชประสงค์
7. พระราชวัง
8. พระวรกาย
9. พระบรมฉายาลักษณ์
10. พระบรมรูป
11. พระฉายา
12. พระพักตร์
13. พระปราง
14. พระทนต์
15. พระชิวหา
16. พระบรมราชชนนี
17. พระที่นั่ง
18. พระตำหนัก
19. พระบรมราชูปถัมภ์
20. พระราชหัตถเลขา
🔴 คำกริยาราชาศัพท์ ได้แก่
1. เสวย
2. บรรทม
3. ตรัส
4. โปรด
5. เสด็จ
6. ประชวร
7. ประทับ
8. ทรงฟัง
9. ทรงศึกษา
10. ทรงเป็นอาจารย์
11. ทรงเป็นประธาน
12. ทรงดนตรี
13. ทรงช้าง
14. ทรงศึกษา
15. ทอดพระเนตร
16. ทรงสนพระทัย
17. ทรงพระเกษมสำราญ
18. ทรงธรรม
19. ทรงศีล
20. ทรงพระอักษร
ระบบการควบคุม (AR Control):
ใช้ MediaPipe Hands ตรวจจับมือ
สร้างเคอร์เซอร์ติดตามที่ปลายนิ้วชี้ (Index Finger Tip)
ให้แสดงการตรวจจับทั้งมือ Hand Tracking
ใช้การ 'จีบนิ้ว' (Pinch - นิ้วโป้งแตะนิ้วชี้) เพื่อ 'หยิบ' หรือ 'กดปุ่ม' และ 'กางนิ้ว' เพื่อ 'ปล่อย'
สำคัญ: ต้องมีระบบ Fallback ให้ใช้เมาส์คลิกลาก (Mouse/Touch) เล่นแทนได้ด้วย หากกล้อง หรือ AI ใช้งานไม่ได้
กราฟิกและ UI (Glassmorphism):
หน้าคำแนะนำมีปุ่ม "เริ่มเกม!" สีเขียวลอยอยู่บน Canvas (ต้องใช้มือจีบ หรือเมาส์คลิก เพื่อกดเริ่ม)
การ์ดคำศัพท์เป็นสไตล์ 2.5D มี Emoji ใหญ่ๆ ตรงกลาง (แทนโมเดล 3D) และด้านล่าง
มีช่องว่างด้านล่างสำหรับนำคำมาใส่ มีป้ายแสดงโจทย์ภาษาไทย และคะแนน (ด่านละ 20 คะแนน)
ระบบสถานะเคอร์เซอร์ (Cursor Feedback):
ปกติ: วงกลมสีฟ้า
เล็งตรงเป้าหมาย (Hover): วงกลมสีเหลืองเส้นประ
จีบนิ้ววืด (ไม่โดนเป้าหมาย): วงกลมสีแดง
จับสำเร็จ: วงกลมสีเขียว พร้อมสัญลักษณ์ ✔️
หาก AI ยังไม่เจอมือ ให้แสดงข้อความเตือน "กำลังหามือของคุณ..."
เมื่อเล่นจบให้ขึ้นข้อความ "All Clear!"
ข้อกำหนดทางเทคนิค: Canvas ต้องซ้อนทับวิดีโอ (z-index สูง)
การวาดรูป (drawLoop) ต้องแยกอิสระจากเหตุการณ์ของ MediaPipe (onResults) โดยใช้ requestAnimationFrame
สูตรคำนวณระยะห่างนิ้วโป้งกับนิ้วชี้ ให้ใช้พีทาโกรัส Math.sqrt(dx*dx + dy*dy) เพื่อความแม่นยำ"
📌Workshop04 : Chatgemini สร้างเกม AR ด้วย AI (ภาษาไทย)
คลิก! ทดลองเกม