Add files via upload

This commit is contained in:
Firman 2026-07-03 20:58:36 +07:00 committed by GitHub
parent 0ddefad000
commit 5cd2b434ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 1553 additions and 0 deletions

641
game.js Normal file
View File

@ -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);
// }
// });

292
gesture.js Normal file
View File

@ -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();

207
index.html Normal file
View File

@ -0,0 +1,207 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="logo" href="assets/logo.png" />
<title>Snake Game</title>
<link
href="https://fonts.googleapis.com/css2?family=Jersey+10&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="virtual-cursor"></div>
<!-- Panduan -->
<div id="onboardingScreen" class="popup">
<div class="popup-content onboarding-box">
<h1 class="guide-title">Panduan</h1>
<div id="step1" class="onboarding-step">
<h1>Aktifkan Kamera & Tampilkan Tangan</h1>
<p class="guide-text">
Pastikan tangan terlihat jelas pada kamera supaya gerakan dapat
terdeteksi dengan baik
</p>
<div class="camera-status" id="cameraStatus">
Mendeteksi Tangan...
</div>
</div>
<div id="step2" class="onboarding-step" style="display: none">
<h1>Cara Menekan Tombol</h1>
<p class="guide-text">
Arahkan kursor hijau ke tombol di bawah dan tahan selama 1 detik
untuk menekan tombol
</p>
<button id="testButton" onclick="nextOnboardingStep(3)">
Coba Tekan
</button>
</div>
<div id="step3" class="onboarding-step" style="display: none">
<h1>Cara Pause & Scroll</h1>
<p class="guide-text">
Lebarkan jempol dan kelingking selama 1 detik untuk menjeda
permainan
</p>
<p class="guide-text">
Gerakkan kursor hijau ke atas atau bawah untuk menggulir halaman
</p>
<button onclick="finishOnboarding()">Mengerti</button>
</div>
</div>
</div>
<!-- Menu Utama -->
<div id="startScreen" class="center-screen">
<div class="menu-box">
<h1 class="game-title">Snake</h1>
<button onclick="startGame()">Mulai</button>
<button onclick="showGuide('start')">Petunjuk</button>
<button onclick="showTutorial()">Panduan</button>
<button id="soundToggle" onclick="toggleSound()">
Efek Suara (ON)
</button>
</div>
</div>
<!-- Jeda -->
<div id="pauseScreen" class="center-screen" style="display: none">
<div class="menu-box">
<div class="image-box">
<img src="assets/pause.png" class="pause-img" />
</div>
<button onclick="resumeGame()">Lanjut</button>
<button onclick="showGuide('pause')">Petunjuk</button>
<button onclick="exitGame()">Keluar</button>
</div>
</div>
<!-- Petunjuk -->
<div id="guidePopup" class="popup" style="display: none">
<div class="popup-content guide-box">
<h1 class="guide-title">Petunjuk</h1>
<div class="guide-section">
<h1>Kontrol Gestur</h1>
<p class="guide-text">
Gerakkan jari telunjuk di depan kamera untuk mengendalikan ular
</p>
<div class="gesture-demo">
<img src="assets/finger.png" class="demo-hand" />
<div class="gesture-label" id="gestureLabel">Tengah</div>
</div>
</div>
<div class="guide-section">
<h1>Aturan Permainan</h1>
<div class="rule-list">
<div class="rule-item">
<img src="assets/apple.png" class="rule-icon" />
<span>Makan apel untuk menambah skor</span>
</div>
<div class="rule-item">
<img src="assets/snake/head_right.png" class="rule-icon" />
<span>Setiap apel membuat tubuh ular semakin panjang</span>
</div>
<div class="rule-item">
<img src="assets/speed.png" class="rule-icon" />
<span
>Kecepatan ular akan meningkat seiring bertambahnya skor</span
>
</div>
<div class="rule-item">
<img src="assets/wall.png" class="rule-icon" />
<span>Hindari menabrak dinding atau tubuh ular sendiri</span>
</div>
<div class="rule-item">
<img src="assets/heart-full.png" class="rule-icon" />
<span>Pemain memiliki 3 nyawa</span>
</div>
<div class="rule-item">
<img src="assets/star.png" class="rule-icon" />
<span>Raih skor tertinggi sebanyak mungkin</span>
</div>
</div>
</div>
<button onclick="closeGuide()">Tutup</button>
</div>
</div>
<!-- Permainan -->
<div id="gameScreen" class="game-container" style="display: none">
<div class="game-wrapper">
<div class="minimap-container">
<canvas id="miniMapCanvas" width="180" height="180"></canvas>
</div>
<div class="top-bar">
<div class="score-group">
<div class="score-box">
<img src="assets/apple.png" class="icon" />
<span id="score">0</span>
</div>
<div class="score-box">
<img src="assets/star.png" class="icon" />
<span id="highscore">0</span>
</div>
<div class="lives-box">
<img id="life1" src="assets/heart-full.png" class="icon" />
<img id="life2" src="assets/heart-full.png" class="icon" />
<img id="life3" src="assets/heart-full.png" class="icon" />
</div>
</div>
<span id="levelText">Level 1</span>
</div>
<div class="camera-container">
<video id="webcam" autoplay playsinline></video>
<canvas id="output_canvas"></canvas>
<div class="gesture-stats">
<div class="stat-item">
<span class="stat-label">Gestur Tangan</span>
<span class="stat-colon">:</span>
<span id="current-gesture" class="stat-value">Tidak Ada</span>
</div>
<div class="stat-item">
<span class="stat-label">Akurasi</span>
<span class="stat-colon">:</span>
<span id="current-accuracy" class="stat-value">0%</span>
</div>
<div class="stat-item">
<span class="stat-label">Sudut Tangan</span>
<span class="stat-colon">:</span>
<span id="game-angle" class="stat-value"></span>
</div>
<div class="stat-item">
<span class="stat-label">Waktu Respon</span>
<span class="stat-colon">:</span>
<span id="game-latency" class="stat-value">0 ms</span>
</div>
<div class="stat-item">
<span class="stat-label">Kinerja</span>
<span class="stat-colon">:</span>
<span id="game-fps" class="stat-value">0 FPS</span>
</div>
</div>
</div>
<div id="countdown-overlay" style="display: none">
<span id="countdown-text">3</span>
</div>
<canvas id="gameCanvas" width="600" height="600"></canvas>
</div>
</div>
<!-- Efek Suara -->
<audio id="upSound" src="sounds/up.mp3" preload="auto"></audio>
<audio id="downSound" src="sounds/down.mp3" preload="auto"></audio>
<audio id="leftSound" src="sounds/left.mp3" preload="auto"></audio>
<audio id="rightSound" src="sounds/right.mp3" preload="auto"></audio>
<audio id="eatSound" src="sounds/eat.mp3" preload="auto"></audio>
<audio id="hitSound" src="sounds/hit.mp3" preload="auto"></audio>
<audio id="levelUpSound" src="sounds/level-up.mp3"></audio>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"></script>
<script src="game.js"></script>
<script src="gesture.js"></script>
</body>
</html>

413
style.css Normal file
View File

@ -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%;
}