From 5cd2b434ad32afe3679dae6c999ed7b4ab38d400 Mon Sep 17 00:00:00 2001 From: Firman <158026541+Hidoui@users.noreply.github.com> Date: Fri, 3 Jul 2026 20:58:36 +0700 Subject: [PATCH] Add files via upload --- game.js | 641 +++++++++++++++++++++++++++++++++++++++++++++++++++++ gesture.js | 292 ++++++++++++++++++++++++ index.html | 207 +++++++++++++++++ style.css | 413 ++++++++++++++++++++++++++++++++++ 4 files changed, 1553 insertions(+) create mode 100644 game.js create mode 100644 gesture.js create mode 100644 index.html create mode 100644 style.css diff --git a/game.js b/game.js new file mode 100644 index 0000000..a60ede7 --- /dev/null +++ b/game.js @@ -0,0 +1,641 @@ +const canvas = document.getElementById("gameCanvas"); +const ctx = canvas.getContext("2d"); +const miniCanvas = document.getElementById("miniMapCanvas"); +const miniCtx = miniCanvas.getContext("2d"); + +const tileCountX = 24; +const tileCountY = 21; + +const tileSize = Math.floor(canvas.width / tileCountX); +canvas.height = tileSize * tileCountY; + +const headUp = new Image(); headUp.src = "assets/snake/head_up.png"; +const headDown = new Image(); headDown.src = "assets/snake/head_down.png"; +const headLeft = new Image(); headLeft.src = "assets/snake/head_left.png"; +const headRight = new Image(); headRight.src = "assets/snake/head_right.png"; + +const bodyHor = new Image(); bodyHor.src = "assets/snake/body_horizontal.png"; +const bodyVer = new Image(); bodyVer.src = "assets/snake/body_vertical.png"; + +const tailUp = new Image(); tailUp.src = "assets/snake/tail_up.png"; +const tailDown = new Image(); tailDown.src = "assets/snake/tail_down.png"; +const tailLeft = new Image(); tailLeft.src = "assets/snake/tail_left.png"; +const tailRight = new Image(); tailRight.src = "assets/snake/tail_right.png"; + +const appleImg = new Image(); +appleImg.src = "assets/apple.png"; + +const upSound = document.getElementById("upSound"); +const downSound = document.getElementById("downSound"); +const leftSound = document.getElementById("leftSound"); +const rightSound = document.getElementById("rightSound"); +const eatSound = document.getElementById("eatSound"); +const hitSound = document.getElementById("hitSound"); +const levelUpSound = document.getElementById("levelUpSound"); + +let isSoundOn = localStorage.getItem("sound") !== "off"; + +let snake, prevSnake, velocity, directionQueue, food; +let score; +let highscore = sessionStorage.getItem("highscore") || 0; +let lives = 3; + +let isPaused = false; +let isCountingDown = false; +let isHit = false; +let isGameOver = false; + +let level = 1; +let lastMoveTime = 0; +let moveDelay = 250; +let progress = 0; + +let frameCount = 0; +let fpsLastTime = performance.now(); +let currentFPS = 0; + +let blinkCount = 0; +let maxBlink = 5; +let blinkTimer = 0; + +let lastFaceDir = { dx: 1, dy: 0 }; +let guideSource = null; + +document.getElementById("highscore").innerText = highscore; + +function unlockAudio() { + const sounds = [upSound, downSound, leftSound, rightSound, eatSound, hitSound]; + + sounds.forEach(sound => { + sound.volume = 0; + sound.play().then(() => { + sound.pause(); + sound.currentTime = 0; + sound.volume = 1; + }).catch(() => {}); + }); +} + +function playSound(sound) { + if (!isSoundOn) return; + + const clone = sound.cloneNode(); + clone.play(); +} + +function startGame() { + unlockAudio(); + + document.getElementById("startScreen").style.display = "none"; + document.getElementById("gameScreen").style.display = "block"; + + initGame(); +} + +function initGame() { + snake = [{ x: 6, y: 10 }, { x: 5, y: 10 }]; + prevSnake = JSON.parse(JSON.stringify(snake)); + velocity = { x: 0, y: 0 }; + directionQueue = []; + lastFaceDir = { dx: 1, dy: 0 }; + + food = { x: 18, y: 10 }; + score = 0; + lives = 3; + + level = 1; + moveDelay = 250; + + document.getElementById("score").innerText = score; + document.getElementById("levelText").innerText = "Level 1"; + updateLivesUI(); +} + +function gameLoop(time = 0) { + frameCount++; + const elapsed = performance.now() - fpsLastTime; + + if (elapsed >= 1000) { + currentFPS = Math.round((frameCount * 1000) / elapsed); + document.getElementById("game-fps").innerText = currentFPS + " FPS"; + + frameCount = 0; + fpsLastTime = performance.now(); + } + + update(time); + draw(); + requestAnimationFrame(gameLoop); +} +requestAnimationFrame(gameLoop); + +function togglePause() { + if (isGameOver) return; + isPaused = !isPaused; + + document.getElementById("pauseScreen").style.display = isPaused ? "flex" : "none"; +} + +function resumeGame() { + document.getElementById("pauseScreen").style.display = "none"; + + const overlay = document.getElementById("countdown-overlay"); + const text = document.getElementById("countdown-text"); + + isCountingDown = true; + overlay.style.display = "flex"; + + let count = 3; + text.innerText = count; + + const countdownInterval = setInterval(() => { + count--; + + if (count > 0) { + text.innerText = count; + } else { + clearInterval(countdownInterval); + overlay.style.display = "none"; + + isCountingDown = false; + isPaused = false; + } + }, 1000); +} + +function exitGame() { + isPaused = false; + + document.getElementById("pauseScreen").style.display = "none"; + document.getElementById("gameScreen").style.display = "none"; + document.getElementById("startScreen").style.display = "flex"; + + initGame(); +} + +function update(time) { + if (!snake) return; + + if (isHit) { + if (time - blinkTimer > 100) { + blinkTimer = time; + blinkCount++; + } + } + + if (isPaused) return; + + if (isGameOver) { + if (time - blinkTimer > 100) { + blinkTimer = time; + blinkCount++; + + if (blinkCount >= maxBlink) { + document.getElementById("gameScreen").style.display = "none"; + document.getElementById("startScreen").style.display = "flex"; + + isGameOver = false; + initGame(); + } + } + return; + } + + if (time - lastMoveTime > moveDelay) { + prevSnake = JSON.parse(JSON.stringify(snake)); + + if (directionQueue.length > 0) { + velocity = directionQueue.shift(); + + if (gestureChangeTimestamp > 0) { + const inputLag = Math.round(performance.now() - gestureChangeTimestamp); + document.getElementById("game-latency").innerText = inputLag + " ms"; + gestureChangeTimestamp = 0; + } + } + + const head = { + x: snake[0].x + velocity.x, + y: snake[0].y + velocity.y + }; + + if (velocity.x === 0 && velocity.y === 0) return; + + if ( + head.x < 0 || head.x >= tileCountX || + head.y < 0 || head.y >= tileCountY + ) return gameOver(); + + for (let part of snake) { + if (part.x === head.x && part.y === head.y) { + return gameOver(); + } + } + + snake.unshift(head); + + if (head.x === food.x && head.y === food.y) { + playSound(eatSound); + score++; + document.getElementById("score").innerText = score; + updateHighscore(); + updateLevel(); + spawnFood(); + } else { + snake.pop(); + } + lastMoveTime = time; + } + progress = Math.min((time - lastMoveTime) / moveDelay, 1); +} + +function gameOver() { + playSound(hitSound); + + lives--; + updateLivesUI(); + + if (lives > 0) { + isHit = true; + isPaused = true; + blinkCount = 0; + blinkTimer = 0; + + setTimeout(() => { + snake = [{ x: 6, y: 10 }, { x: 5, y: 10 }]; + prevSnake = JSON.parse(JSON.stringify(snake)); + velocity = { x: 0, y: 0 }; + directionQueue = []; + lastFaceDir = { dx: 1, dy: 0 }; + + food = { x: 18, y: 10 }; + moveDelay = 250; + + isHit = false; + isPaused = false; + }, 500); + + return; + } + + isGameOver = true; + blinkCount = 0; + blinkTimer = 0; + + if (score > highscore) { + highscore = score; + sessionStorage.setItem("highscore", highscore); + document.getElementById("highscore").innerText = highscore; + } +} + +function draw() { + if (!snake) return; + + ctx.fillStyle = "black"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + drawGrid(); + + if ((!isGameOver && !isHit) || blinkCount % 2 === 0) { + for (let i = 0; i < snake.length; i++) { + const current = snake[i]; + const previous = prevSnake[i] || current; + + const interpX = previous.x + (current.x - previous.x) * progress; + const interpY = previous.y + (current.y - previous.y) * progress; + + const x = interpX * tileSize; + const y = interpY * tileSize; + + if (i === 0) { + let dx = current.x - previous.x; + let dy = current.y - previous.y; + + if (dx !== 0 || dy !== 0) { + lastFaceDir = { dx, dy }; + } else { + dx = lastFaceDir.dx; + dy = lastFaceDir.dy; + } + + let img, w, h; + if (dx > 0) { img = headRight; w = 120; h = 108; } + else if (dx < 0) { img = headLeft; w = 120; h = 108; } + else if (dy > 0) { img = headDown; w = 108; h = 120; } + else { img = headUp; w = 108; h = 120; } + + drawScaledImage(img, x, y, w, h); + + } else if (i === snake.length - 1) { + let dx = previous.x - current.x; + let dy = previous.y - current.y; + + if (dx === 0 && dy === 0) { + dx = -lastFaceDir.dx; + dy = -lastFaceDir.dy; + } + + let img, w, h; + if (dx > 0) { img = tailLeft; w = 100; h = 92; } + else if (dx < 0) { img = tailRight; w = 100; h = 92; } + else if (dy > 0) { img = tailUp; w = 92; h = 100; } + else { img = tailDown; w = 92; h = 100; } + + drawScaledImage(img, x, y, w, h); + + } else { + const parent = snake[i - 1]; + let img, w, h; + + if (current.y === parent.y) { + img = bodyHor; w = 100; h = 92; + } else { + img = bodyVer; w = 92; h = 100; + } + + drawScaledImage(img, x, y, w, h); + } + } + } + + ctx.drawImage( + appleImg, + food.x * tileSize, + food.y * tileSize, + tileSize, + tileSize + ); + + drawMiniMap(); +} + +function drawGrid() { + for (let y = 0; y < tileCountY; y++) { + for (let x = 0; x < tileCountX; x++) { + ctx.fillStyle = (x + y) % 2 === 0 ? "#AAD751" : "#A2D149"; + ctx.fillRect(x * tileSize, y * tileSize, tileSize + 1, tileSize + 1); + } + } +} + +function drawScaledImage(img, x, y, baseW, baseH) { + const scale = tileSize / 100; + const width = baseW * scale; + const height = baseH * scale; + + const offsetX = (tileSize - width) / 2; + const offsetY = (tileSize - height) / 2; + + ctx.drawImage(img, x + offsetX, y + offsetY, width + 1, height + 1); +} + +function drawMiniMap() { + if (!snake) return; + + const scaleX = miniCanvas.width / tileCountX; + const scaleY = miniCanvas.height / tileCountY; + + miniCtx.fillStyle = "#1F2020"; + miniCtx.fillRect(0, 0, miniCanvas.width, miniCanvas.height); + + miniCtx.fillStyle = "#E7471D"; + miniCtx.fillRect( + food.x * scaleX, + food.y * scaleY, + scaleX, + scaleY + ); + + snake.forEach((part, index) => { + miniCtx.fillStyle = index === 0 ? "#6AB04A" : "#6AB04A"; + miniCtx.fillRect( + part.x * scaleX, + part.y * scaleY, + scaleX, + scaleY + ); + }); + + const head = snake[0]; + const cx = head.x * scaleX + scaleX / 2; + const cy = head.y * scaleY + scaleY / 2; + + miniCtx.strokeStyle = "rgba(255,255,255,0.5)"; + miniCtx.lineWidth = 1; + + miniCtx.beginPath(); + miniCtx.moveTo(cx, 0); + miniCtx.lineTo(cx, miniCanvas.height); + + miniCtx.moveTo(0, cy); + miniCtx.lineTo(miniCanvas.width, cy); + miniCtx.stroke(); + + miniCtx.fillStyle = "white"; + miniCtx.font = "10px Arial"; + miniCtx.fillText("N", miniCanvas.width / 2 - 3, 10); + miniCtx.fillText("S", miniCanvas.width / 2 - 3, miniCanvas.height - 2); + miniCtx.fillText("W", 2, miniCanvas.height / 2 + 3); + miniCtx.fillText("E", miniCanvas.width - 10, miniCanvas.height / 2 + 3); +} + +function spawnFood() { + let valid = false; + + while (!valid) { + food.x = Math.floor(Math.random() * (tileCountX - 2)) + 1; + food.y = Math.floor(Math.random() * (tileCountY - 2)) + 1; + + valid = !snake.some(p => p.x === food.x && p.y === food.y); + } +} + +function isGameRunning() { + return document.getElementById("gameScreen").style.display === "block"; +} + +function updateHighscore() { + if (score > highscore) { + highscore = score; + sessionStorage.setItem("highscore", highscore); + document.getElementById("highscore").innerText = highscore; + } +} + +function updateLivesUI() { + const hearts = [ + document.getElementById("life1"), + document.getElementById("life2"), + document.getElementById("life3") + ]; + + for (let i = 0; i < 3; i++) { + hearts[i].src = i < lives + ? "assets/heart-full.png" + : "assets/heart.png"; + } +} + +function updateLevel() { + const newLevel = Math.floor(score / 10) + 1; + + if (newLevel !== level) { + level = newLevel; + document.getElementById("levelText").innerText = "Level " + level; + + playSound(levelUpSound); + } + moveDelay = Math.max(100, 250 - (level - 1) * 10); +} + +function toggleSound() { + isSoundOn = !isSoundOn; + localStorage.setItem("sound", isSoundOn ? "on" : "off"); + updateSoundUI(); +} + +function updateSoundUI() { + const soundBtn = document.getElementById("soundToggle"); + if (soundBtn) { + soundBtn.innerText = isSoundOn ? "Efek Suara (ON)" : "Efek Suara (OFF)"; + } +} + +window.onload = () => { + updateSoundUI(); +}; + +function showGuide(source) { + guideSource = source; + + document.getElementById("guidePopup").style.display = "flex"; + + const guideBox = document.querySelector(".guide-box"); + guideBox.scrollTop = 0; +} + +function closeGuide() { + document.getElementById("guidePopup").style.display = "none"; + document.activeElement.blur(); +} + +function showTutorial() { + onboardingComplete = false; + currentStep = 1; + + document.getElementById("startScreen").style.display = "none"; + + const onboardingScreen = document.getElementById("onboardingScreen"); + onboardingScreen.style.display = "flex"; + + document.getElementById("step1").style.display = "block"; + document.getElementById("step2").style.display = "none"; + document.getElementById("step3").style.display = "none"; + + const statusBox = document.getElementById("cameraStatus"); + if (statusBox) { + statusBox.innerText = "Mendeteksi Tangan..."; + statusBox.classList.remove("ready"); + } +} + +const hand = document.querySelector(".demo-hand"); +const gestureLabel = document.getElementById("gestureLabel"); + +const animations = [ + { + x: "-50%", + y: "-150%", + text: "Atas" + }, + { + x: "100%", + y: "-50%", + text: "Kanan" + }, + { + x: "-50%", + y: "50%", + text: "Bawah" + }, + { + x: "-200%", + y: "-50%", + text: "Kiri" + } +]; + +let index = 0; + +setInterval(() => { + const anim = animations[index]; + + hand.style.transform = `translate(${anim.x}, ${anim.y})`; + gestureLabel.innerText = anim.text; + + index = (index + 1) % animations.length; +}, 1000); + +// document.addEventListener("keydown", (e) => { + +// if (e.key === "Escape") { + +// const guideVisible = document.getElementById("guidePopup").style.display === "flex"; + +// if (guideVisible) { +// closeGuide(); + +// if (guideSource === "pause") { +// document.getElementById("pauseScreen").style.display = "flex"; +// } else { +// document.getElementById("startScreen").style.display = "flex"; +// } +// return; +// } + +// if (isGameRunning()) { +// togglePause(); +// return; +// } +// } + +// if (isPaused) return; + +// let newDir = null; +// let sound = null; + +// switch (e.key) { +// case "ArrowUp": newDir = { x: 0, y: -1 }; sound = upSound; break; +// case "ArrowDown": newDir = { x: 0, y: 1 }; sound = downSound; break; +// case "ArrowLeft": newDir = { x: -1, y: 0 }; sound = leftSound; break; +// case "ArrowRight": newDir = { x: 1, y: 0 }; sound = rightSound; break; +// } + +// if (!newDir) return; + +// if (velocity.x === 0 && velocity.y === 0) { +// const defaultDir = { +// x: snake[0].x - snake[1].x, +// y: snake[0].y - snake[1].y +// }; + +// if (newDir.x === -defaultDir.x && newDir.y === -defaultDir.y) return; + +// playSound(sound); +// velocity = newDir; +// return; +// } + +// const lastDir = directionQueue.length > 0 +// ? directionQueue[directionQueue.length - 1] +// : velocity; + +// 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); +// } +// }); \ No newline at end of file diff --git a/gesture.js b/gesture.js new file mode 100644 index 0000000..b690b89 --- /dev/null +++ b/gesture.js @@ -0,0 +1,292 @@ +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(); \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..e4c7ceb --- /dev/null +++ b/index.html @@ -0,0 +1,207 @@ + + + + + + + Snake Game + + + + +
+ + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/style.css b/style.css new file mode 100644 index 0000000..c9aed4c --- /dev/null +++ b/style.css @@ -0,0 +1,413 @@ +body { + font-family: 'Jersey 10', sans-serif; + font-size: 20px; + text-align: center; + background-image: url('assets/background.png'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-attachment: fixed; +} + +body, +button, +.onboarding-box button { + cursor: none !important; +} + +#virtual-cursor { + width: 20px; + height: 20px; + background: #6AB04A; + border: 2px solid white; + border-radius: 50%; + display: none; + position: fixed; + pointer-events: none; + z-index: 10000; + transition: transform 1s ease; +} + +button { + font-family: 'Jersey 10', sans-serif; + font-size: 20px; + color: #6AB04A; + background: white; + width: 200px; + padding: 10px; + border: 2px solid #6AB04A; + transition: 0.3s; +} + +button:hover { + color: white; + background: #6AB04A; + border: 2px solid transparent; +} + +button.finger-hover, +#soundToggleMenu.finger-hover { + color: white !important; + background-color: #6AB04A !important; +} + +#onboardingScreen, +#startScreen { + display: none; +} + +.center-screen { + height: 500px; + display: flex; + align-items: center; + justify-content: center; +} + +.menu-box { + gap: 15px; + display: flex; + align-items: center; + flex-direction: column; +} + +.game-title { + font-family: 'Jersey 10', sans-serif; + font-size: 150px; + color: #6AB04A; + font-weight: 1000; + margin-bottom: 10px; +} + +.onboarding-box { + width: 600px; + min-height: 200px; + background: white; + display: flex; + flex-direction: column; + justify-content: flex-start; +} + +.onboarding-step { + margin-top: -20px; +} + +.onboarding-step h1 { + font-size: 30px; + margin-bottom: 10px; + letter-spacing: 1px; +} + +.onboarding-box button { + width: 100%; + margin: 0 auto; + padding: 12px 20px; + transition: 0.3s; +} + +.camera-status { + color: white; + background: #E7471D; + padding: 14px 20px; + transition: all 0.3s ease; +} + +.camera-status.ready { + color: white; + background: #6AB04A; +} + +.image-box { + background: white; + width: 200px; + height: 200px; + display: flex; + align-items: center; + justify-content: center; +} + +.image-box img { + max-width: 100%; +} + +.popup { + width: 100%; + height: 100%; + background: rgba(0,0,0,0.5); + position: fixed; + top: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.popup-content { + padding: 20px; + overflow-y: auto; +} + +.guide-box { + width: 600px; + background: white; + max-height: 87.2vh; + overflow-y: auto; +} + +.guide-title { + font-size: 40px; + color: #6AB04A; + text-align: center; + margin: 0 0 30px; + letter-spacing: 1px; +} + +.guide-section { + margin-bottom: 30px; +} + +.guide-section h1 { + font-size: 30px; + margin-bottom: 10px; + letter-spacing: 1px; +} + +.guide-text { + font-size: 20px; + margin-top: -5px; + margin-bottom: 10px; +} + +.rule-list { + display: flex; + gap: 10px; + flex-direction: column; +} + +.rule-item { + font-size: 20px; + background: #F5F5F5; + display: flex; + padding: 10px; +} + +.rule-icon { + width: 20px; + height: 20px; + margin-right: 10px; +} + +.guide-box button { + width: 100%; + margin-top: -10px; +} + +.guide-box::-webkit-scrollbar { + width: 5px; +} + +.guide-box::-webkit-scrollbar-thumb { + background: #6AB04A; +} + +.gesture-demo { + width: 300px; + height: 200px; + border: 2px solid #6AB04A; + position: relative; + overflow: hidden; + margin: 0 auto; +} + +.gesture-demo::before, +.gesture-demo::after { + background: #F5F5F5; + content: ""; + position: absolute; + z-index: 1; +} + +.gesture-demo::before { + width: 2px; + height: 100%; + top: 0; + left: 50%; + transform: translateX(-50%); +} + +.gesture-demo::after { + width: 100%; + height: 2px; + top: 50%; + left: 0; + transform: translateY(-50%); +} + +.demo-hand { + width: 50px; + left: 50%; + top: 50%; + position: absolute; + transform: translate(-50%, -50%); + transition: transform 1s ease; + z-index: 2; +} + +.gesture-label { + font-size: 20px; + color: #6AB04A; + top: 5px; + left: 10px; + position: absolute; +} + +.game-wrapper { + width: 600px; + margin: 0 auto; +} + +.game-container { + margin-top: 10px; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +#gameCanvas { + background: black; + margin-top: 10px; +} + +.top-bar { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 10px; + padding-bottom: 10px; +} + +.score-group { + gap: 20px; + display: flex; + align-items: center; +} + +.score-box, +.lives-box { + gap: 5px; + display: flex; + align-items: center; +} + +.icon { + width: 20px; + height: 20px; + object-fit: contain; + display: block; +} + +#levelText { + color: black; + font-size: 20px; +} + +#pauseScreen { + width: 100%; + height: 100%; + background: rgba(0,0,0,0.5); + position: fixed; + top: 0; + left: 0; +} + +#countdown-overlay { + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.5); + display: flex; + align-items: center; + justify-content: center; + position: absolute; +} + +#countdown-text { + font-size: 50px; + color: white; + font-weight: bold; + animation: pulse 1s infinite; +} + +@keyframes pulse { + 0% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.5); opacity: 1; } + 100% { transform: scale(1); opacity: 1; } +} + +.camera-container { + width: 300px; + height: 200px; + top: 20px; + right: 20px; + z-index: 10; + position: absolute; +} + +#webcam { + display: none; +} + +#output_canvas { + width: 100%; + height: 100%; + transform: scaleX(-1); +} + +.gesture-stats { + font-family: 'Jersey 10', sans-serif; + width: 300px; + box-sizing: border-box; +} + +.stat-item { + font-size: 20px; + margin-top: 10px; + display: flex; + align-items: center; + justify-content: flex-start; +} + +.stat-label { + color: black; + width: 110px; + text-align: left; + display: inline-block; +} + +.stat-colon { + color: black; + width: 20px; + text-align: left; + display: inline-block; +} + +.stat-value { + color: black; + text-align: left; +} + +.minimap-container { + width: 180px; + height: 180px; + top: 20px; + left: 20px; + z-index: 10; + position: absolute; +} + +#miniMapCanvas { + width: 100%; + height: 100%; +} \ No newline at end of file