const videoElement = document.getElementById('webcam'); const canvasElement = document.getElementById('output_canvas'); const canvasCtx = canvasElement.getContext('2d'); const virtualCursor = document.getElementById('virtual-cursor'); let lastGesture = ""; let hoverTimer; let currentHoverElement = null; let lastScrollTime = 0; let pauseTimer = null; let gestureChangeTimestamp = 0; let onboardingComplete = localStorage.getItem("onboarding_done") === "true"; let currentStep = 1; let smoothedX = 0.5; let smoothedY = 0.5; const SMOOTHING_FACTOR = 0.50; window.addEventListener("DOMContentLoaded", () => { const onboardingScreen = document.getElementById("onboardingScreen"); const startScreen = document.getElementById("startScreen"); if (onboardingComplete) { startScreen.style.display = "flex"; onboardingScreen.style.display = "none"; } else { onboardingScreen.style.display = "flex"; startScreen.style.display = "none"; } }); const hands = new Hands({ locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}` }); const THRESHOLD = { min: 0.40, max: 0.60 }; hands.setOptions({ maxNumHands: 1, modelComplexity: 1, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5 }); hands.onResults(onResults); function onResults(results) { canvasCtx.save(); canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height); drawGuides(); let gestureText = "Tidak Ada"; let accuracyText = "0%"; let angleText = "0°"; if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) { const confidence = (results.multiHandedness[0].score * 100).toFixed(1); accuracyText = confidence + "%"; if (!onboardingComplete) { if (currentStep === 1) { const statusBox = document.getElementById("cameraStatus"); if (statusBox && !statusBox.classList.contains("ready")) { statusBox.innerText = "Tangan Terdeteksi"; statusBox.classList.add("ready"); setTimeout(() => { if (currentStep === 1 && !onboardingComplete) { nextOnboardingStep(2); } }, 1000); } } } for (const landmarks of results.multiHandLandmarks) { drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS, {color: '#FFFFFF', lineWidth: 2}); drawLandmarks(canvasCtx, landmarks, {color: '#6AB04A', lineWidth: 2, radius: 2}); const wrist = landmarks[0]; const middleMcp = landmarks[9]; const rad = Math.atan2(middleMcp.y - wrist.y, middleMcp.x - wrist.x); let angle = Math.round(rad * (180 / Math.PI)); angle = (angle + 90) % 360; angleText = angle + "°"; const indexFinger = landmarks[8]; const thumbTip = landmarks[4]; const pinkyTip = landmarks[20]; const guidePopup = document.getElementById("guidePopup"); if (guidePopup.style.display === "flex") { handleScrollInteraction(indexFinger.y); } const handSpan = Math.hypot(thumbTip.x - pinkyTip.x, thumbTip.y - pinkyTip.y); if (isGameRunning() && !isPaused && !isGameOver && handSpan > 0.2) { if (!pauseTimer) { pauseTimer = setTimeout(() => { togglePause(); pauseTimer = null; }, 1000); } } else { clearTimeout(pauseTimer); pauseTimer = null; } if (!isGameRunning() || isPaused && !isCountingDown) { if (virtualCursor) { virtualCursor.style.display = 'block'; smoothedX = smoothedX + (indexFinger.x - smoothedX) * SMOOTHING_FACTOR; smoothedY = smoothedY + (indexFinger.y - smoothedY) * SMOOTHING_FACTOR; const cursorX = (1 - smoothedX) * window.innerWidth; const cursorY = smoothedY * window.innerHeight; virtualCursor.style.left = cursorX + 'px'; virtualCursor.style.top = cursorY + 'px'; handleMenuInteraction(cursorX, cursorY); } } else { if (virtualCursor) virtualCursor.style.display = 'none'; resetHover(); } gestureText = getActiveDirection(indexFinger.x, indexFinger.y) || "Tengah"; processGesture(indexFinger.x, indexFinger.y); } } else { if (virtualCursor) virtualCursor.style.display = 'none'; resetHover(); } document.getElementById("current-gesture").innerText = gestureText; document.getElementById("current-accuracy").innerText = accuracyText; document.getElementById("game-angle").innerText = angleText; canvasCtx.restore(); } function handleMenuInteraction(x, y) { const element = document.elementFromPoint(x, y); if (element && element.tagName === "BUTTON") { if (currentHoverElement !== element) { resetHover(); currentHoverElement = element; element.classList.add('finger-hover'); hoverTimer = setTimeout(() => { element.click(); resetHover(); }, 1000); } } else { resetHover(); } } function handleScrollInteraction(y) { const guideBox = document.querySelector(".guide-box"); if (!guideBox) return; const now = Date.now(); if (now - lastScrollTime < 30) return; if (y < 0.25) { guideBox.scrollTop -= 15; lastScrollTime = now; } else if (y > 0.75) { guideBox.scrollTop += 15; lastScrollTime = now; } } function resetHover() { clearTimeout(hoverTimer); if (currentHoverElement) { currentHoverElement.classList.remove('finger-hover'); currentHoverElement = null; } } function getActiveDirection(x, y) { if (x < THRESHOLD.min) return "Kanan"; if (x > THRESHOLD.max) return "Kiri"; if (y < THRESHOLD.min) return "Atas"; if (y > THRESHOLD.max) return "Bawah"; return null; } function processGesture(x, y) { if (isPaused || isGameOver || !isGameRunning()) return; let newDir = null; let sound = null; if (x < THRESHOLD.min) { newDir = { x: 1, y: 0 }; sound = rightSound; } else if (x > THRESHOLD.max) { newDir = { x: -1, y: 0 }; sound = leftSound; } else if (y < THRESHOLD.min) { newDir = { x: 0, y: -1 }; sound = upSound; } else if (y > THRESHOLD.max) { newDir = { x: 0, y: 1 }; sound = downSound; } else { lastGesture = "Tengah"; return; } const gestureLabel = getActiveDirection(x, y); if (lastGesture !== gestureLabel) { lastGesture = gestureLabel; gestureChangeTimestamp = performance.now(); handleGestureInput(newDir, sound); } } function handleGestureInput(newDir, sound) { let lastDir = directionQueue.length > 0 ? directionQueue[directionQueue.length - 1] : velocity; if (lastDir.x === 0 && lastDir.y === 0) { if (newDir.x === -1) return; } else { if (newDir.x === -lastDir.x && newDir.y === -lastDir.y) return; } if (newDir.x === lastDir.x && newDir.y === lastDir.y) return; playSound(sound); if (directionQueue.length < 3) { directionQueue.push(newDir); } } function drawGuides() { const w = canvasElement.width; const h = canvasElement.height; canvasCtx.strokeStyle = "#FFFFFF"; canvasCtx.lineWidth = 1; canvasCtx.beginPath(); canvasCtx.moveTo(w / 2, 0); canvasCtx.lineTo(w / 2, h); canvasCtx.moveTo(0, h / 2); canvasCtx.lineTo(w, h / 2); canvasCtx.stroke(); canvasCtx.strokeStyle = "#E7471D"; canvasCtx.strokeRect( w * THRESHOLD.min, h * THRESHOLD.min, w * (THRESHOLD.max - THRESHOLD.min), h * (THRESHOLD.max - THRESHOLD.min) ); } function nextOnboardingStep(step) { currentStep = step; document.querySelectorAll('.onboarding-step').forEach(el => el.style.display = 'none'); const targetStep = document.getElementById(`step${step}`); if (targetStep) { targetStep.style.display = 'block'; } else { finishOnboarding(); } } function finishOnboarding() { onboardingComplete = true; localStorage.setItem("onboarding_done", "true"); document.getElementById("onboardingScreen").style.display = "none"; document.getElementById("startScreen").style.display = "flex"; } const camera = new Camera(videoElement, { onFrame: async () => { await hands.send({ image: videoElement }); }, width: 300, height: 200 }); camera.start();