Add files via upload
This commit is contained in:
parent
0ddefad000
commit
5cd2b434ad
|
|
@ -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);
|
||||
// }
|
||||
// });
|
||||
|
|
@ -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();
|
||||
|
|
@ -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">0°</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>
|
||||
|
|
@ -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%;
|
||||
}
|
||||
Loading…
Reference in New Issue