Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Rock Paper Scissors for Kids</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest"></script> | |
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/handpose@latest"></script> | |
<style> | |
@import url('https://fonts.googleapis.com/css2?family=Comic+Neue:wght@400;700&display=swap'); | |
body { | |
font-family: 'Comic Neue', cursive; | |
background-color: #f0f9ff; | |
} | |
.hand-animation { | |
animation: bounce 0.5s infinite alternate; | |
} | |
@keyframes bounce { | |
from { transform: translateY(0); } | |
to { transform: translateY(-10px); } | |
} | |
.result-bubble { | |
animation: pop 0.3s ease-out; | |
} | |
@keyframes pop { | |
0% { transform: scale(0.5); opacity: 0; } | |
80% { transform: scale(1.1); } | |
100% { transform: scale(1); opacity: 1; } | |
} | |
.emoji { | |
font-size: 2rem; | |
} | |
.animate-pulse { | |
animation: pulse 0.5s ease-in-out; | |
} | |
@keyframes pulse { | |
0%, 100% { transform: scale(1); } | |
50% { transform: scale(1.2); } | |
} | |
</style> | |
</head> | |
<body class="min-h-screen flex flex-col items-center justify-center p-4"> | |
<div class="max-w-4xl w-full bg-white rounded-3xl shadow-xl overflow-hidden"> | |
<!-- Header --> | |
<div class="bg-gradient-to-r from-blue-500 to-purple-600 p-6 text-center"> | |
<h1 class="text-4xl font-bold text-white mb-2">π Rock Paper Scissors π</h1> | |
<p class="text-white text-lg">Show your hand to the camera to play!</p> | |
</div> | |
<!-- Game Area --> | |
<div class="p-6 md:p-8 grid grid-cols-1 md:grid-cols-2 gap-8"> | |
<!-- Camera Section --> | |
<div class="bg-blue-50 rounded-2xl p-4 flex flex-col items-center"> | |
<h2 class="text-2xl font-bold text-blue-800 mb-4">Your Move</h2> | |
<div class="relative w-full aspect-square max-w-md bg-gray-200 rounded-xl overflow-hidden mb-4"> | |
<video id="video" class="w-full h-full object-cover" playsinline autoplay muted></video> | |
<div id="overlay" class="absolute inset-0"></div> | |
</div> | |
<div class="flex gap-4 mb-4"> | |
<button id="startCamera" class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded-full transition"> | |
Start Camera | |
</button> | |
<button id="stopCamera" class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-full transition"> | |
Stop Camera | |
</button> | |
</div> | |
<div class="text-center"> | |
<p class="text-gray-700 mb-2">Show one of these to the camera:</p> | |
<div class="flex justify-center gap-6"> | |
<div class="text-center"> | |
<div class="emoji">β</div> | |
<p class="text-sm">Rock</p> | |
</div> | |
<div class="text-center"> | |
<div class="emoji">β</div> | |
<p class="text-sm">Paper</p> | |
</div> | |
<div class="text-center"> | |
<div class="emoji">βοΈ</div> | |
<p class="text-sm">Scissors</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Game Section --> | |
<div class="bg-purple-50 rounded-2xl p-4 flex flex-col items-center justify-between"> | |
<h2 class="text-2xl font-bold text-purple-800 mb-4">Game</h2> | |
<!-- Score Board --> | |
<div class="w-full bg-white rounded-xl p-4 mb-6 shadow-md"> | |
<div class="flex justify-between items-center"> | |
<div class="text-center"> | |
<p class="text-sm text-gray-600">You</p> | |
<p id="playerScore" class="text-3xl font-bold text-blue-600">0</p> | |
</div> | |
<div class="text-2xl font-bold text-gray-500">VS</div> | |
<div class="text-center"> | |
<p class="text-sm text-gray-600">Computer</p> | |
<p id="computerScore" class="text-3xl font-bold text-purple-600">0</p> | |
</div> | |
</div> | |
</div> | |
<!-- Moves Display --> | |
<div class="w-full flex justify-center gap-8 mb-8"> | |
<div class="text-center"> | |
<p class="text-sm text-gray-600 mb-2">You</p> | |
<div id="playerMove" class="text-6xl bg-white rounded-full w-20 h-20 flex items-center justify-center shadow-md">?</div> | |
</div> | |
<div class="text-center"> | |
<p class="text-sm text-gray-600 mb-2">Computer</p> | |
<div id="computerMove" class="text-6xl bg-white rounded-full w-20 h-20 flex items-center justify-center shadow-md">?</div> | |
</div> | |
</div> | |
<!-- Result Display --> | |
<div id="resultDisplay" class="mb-6 text-center hidden"> | |
<div class="result-bubble inline-block bg-yellow-100 text-yellow-800 px-6 py-3 rounded-full text-xl font-bold"> | |
Let's Play! | |
</div> | |
</div> | |
<!-- Controls --> | |
<div class="flex flex-col items-center w-full"> | |
<button id="playButton" class="bg-gradient-to-r from-pink-500 to-orange-500 hover:from-pink-600 hover:to-orange-600 text-white font-bold py-3 px-8 rounded-full text-xl shadow-lg transition transform hover:scale-105 mb-4"> | |
Play! | |
</button> | |
<button id="resetButton" class="text-gray-600 hover:text-gray-800 font-medium"> | |
Reset Game | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- Instructions --> | |
<div class="bg-gray-100 p-6"> | |
<h3 class="text-xl font-bold text-gray-800 mb-3">How to Play:</h3> | |
<ol class="list-decimal pl-5 space-y-2 text-gray-700"> | |
<li>Click "Start Camera" and allow access to your camera</li> | |
<li>Show your hand to the camera (rock β, paper β, or scissors βοΈ)</li> | |
<li>Click "Play!" when you're ready</li> | |
<li>The computer will choose its move and show who won!</li> | |
</ol> | |
</div> | |
</div> | |
<script> | |
// Game variables | |
let playerScore = 0; | |
let computerScore = 0; | |
let playerMove = ''; | |
let computerMove = ''; | |
let gameActive = false; | |
let videoStream = null; | |
let handposeModel = null; | |
// DOM elements | |
const playerScoreElement = document.getElementById('playerScore'); | |
const computerScoreElement = document.getElementById('computerScore'); | |
const playerMoveElement = document.getElementById('playerMove'); | |
const computerMoveElement = document.getElementById('computerMove'); | |
const resultDisplay = document.getElementById('resultDisplay'); | |
const playButton = document.getElementById('playButton'); | |
const resetButton = document.getElementById('resetButton'); | |
const startCameraButton = document.getElementById('startCamera'); | |
const stopCameraButton = document.getElementById('stopCamera'); | |
const videoElement = document.getElementById('video'); | |
const overlayElement = document.getElementById('overlay'); | |
// Initialize the game | |
function initGame() { | |
updateScores(); | |
resetMoves(); | |
// Event listeners | |
playButton.addEventListener('click', playRound); | |
resetButton.addEventListener('click', resetGame); | |
startCameraButton.addEventListener('click', startCamera); | |
stopCameraButton.addEventListener('click', stopCamera); | |
// Load handpose model | |
loadHandposeModel(); | |
} | |
// Load TensorFlow handpose model | |
async function loadHandposeModel() { | |
try { | |
handposeModel = await handpose.load(); | |
console.log('Handpose model loaded'); | |
} catch (error) { | |
console.error('Error loading handpose model:', error); | |
} | |
} | |
// Start camera | |
async function startCamera() { | |
try { | |
videoStream = await navigator.mediaDevices.getUserMedia({ | |
video: { | |
facingMode: 'user', | |
width: { ideal: 640 }, | |
height: { ideal: 480 } | |
}, | |
audio: false | |
}); | |
videoElement.srcObject = videoStream; | |
gameActive = true; | |
detectHandGesture(); | |
} catch (error) { | |
console.error('Error accessing camera:', error); | |
alert('Could not access the camera. Please make sure you have granted permission.'); | |
} | |
} | |
// Stop camera | |
function stopCamera() { | |
if (videoStream) { | |
videoStream.getTracks().forEach(track => track.stop()); | |
videoElement.srcObject = null; | |
gameActive = false; | |
} | |
} | |
// Detect hand gesture from camera | |
async function detectHandGesture() { | |
if (!gameActive || !handposeModel) return; | |
try { | |
// Predict hand landmarks | |
const predictions = await handposeModel.estimateHands(videoElement); | |
// Clear overlay | |
overlayElement.innerHTML = ''; | |
if (predictions.length > 0) { | |
const hand = predictions[0]; | |
const fingersUp = countFingersUp(hand.landmarks); | |
// Determine move based on fingers up | |
if (fingersUp === 0) { | |
playerMove = 'rock'; | |
playerMoveElement.textContent = 'β'; | |
} else if (fingersUp >= 4) { | |
playerMove = 'paper'; | |
playerMoveElement.textContent = 'β'; | |
} else if (fingersUp === 2) { | |
playerMove = 'scissors'; | |
playerMoveElement.textContent = 'βοΈ'; | |
} else { | |
playerMove = ''; | |
playerMoveElement.textContent = '?'; | |
} | |
// Draw landmarks (for visualization) | |
drawLandmarks(hand.landmarks); | |
} else { | |
playerMove = ''; | |
playerMoveElement.textContent = '?'; | |
} | |
} catch (error) { | |
console.error('Hand detection error:', error); | |
} | |
// Continue detecting | |
requestAnimationFrame(detectHandGesture); | |
} | |
// Count how many fingers are up | |
function countFingersUp(landmarks) { | |
// Simplified finger detection (thumb, index, middle, ring, pinky) | |
const fingerTips = [4, 8, 12, 16, 20]; // Landmark indices for finger tips | |
const fingerPips = [2, 6, 10, 14, 18]; // Landmark indices for finger PIP joints | |
let count = 0; | |
for (let i = 1; i < 5; i++) { // Skip thumb (i=0) | |
const tip = landmarks[fingerTips[i]]; | |
const pip = landmarks[fingerPips[i]]; | |
// Check if finger is extended (tip is above PIP joint) | |
if (tip[1] < pip[1]) { // Compare y-coordinates | |
count++; | |
} | |
} | |
return count; | |
} | |
// Draw hand landmarks on overlay | |
function drawLandmarks(landmarks) { | |
const canvas = document.createElement('canvas'); | |
canvas.width = videoElement.videoWidth; | |
canvas.height = videoElement.videoHeight; | |
const ctx = canvas.getContext('2d'); | |
// Draw landmarks | |
ctx.fillStyle = 'red'; | |
landmarks.forEach(landmark => { | |
ctx.beginPath(); | |
ctx.arc(landmark[0], landmark[1], 5, 0, 2 * Math.PI); | |
ctx.fill(); | |
}); | |
// Draw connections (simplified) | |
ctx.strokeStyle = 'blue'; | |
ctx.lineWidth = 2; | |
// Palm connections | |
drawConnection(ctx, landmarks, [0, 1, 2, 5, 9, 13, 17, 0]); | |
// Thumb | |
drawConnection(ctx, landmarks, [1, 2, 3, 4]); | |
// Index finger | |
drawConnection(ctx, landmarks, [5, 6, 7, 8]); | |
// Middle finger | |
drawConnection(ctx, landmarks, [9, 10, 11, 12]); | |
// Ring finger | |
drawConnection(ctx, landmarks, [13, 14, 15, 16]); | |
// Pinky finger | |
drawConnection(ctx, landmarks, [17, 18, 19, 20]); | |
overlayElement.innerHTML = ''; | |
overlayElement.appendChild(canvas); | |
} | |
// Helper function to draw connections between landmarks | |
function drawConnection(ctx, landmarks, indices) { | |
ctx.beginPath(); | |
for (let i = 0; i < indices.length; i++) { | |
const landmark = landmarks[indices[i]]; | |
if (i === 0) { | |
ctx.moveTo(landmark[0], landmark[1]); | |
} else { | |
ctx.lineTo(landmark[0], landmark[1]); | |
} | |
} | |
ctx.stroke(); | |
} | |
// Play a round of the game | |
function playRound() { | |
if (!playerMove) { | |
showResult('Show your hand to the camera! βββοΈ'); | |
playerMoveElement.classList.add('animate-pulse'); | |
setTimeout(() => { | |
playerMoveElement.classList.remove('animate-pulse'); | |
}, 1000); | |
return; | |
} | |
// Computer chooses random move | |
const moves = ['rock', 'paper', 'scissors']; | |
computerMove = moves[Math.floor(Math.random() * moves.length)]; | |
// Update computer move display | |
computerMoveElement.textContent = | |
computerMove === 'rock' ? 'β' : | |
computerMove === 'paper' ? 'β' : 'βοΈ'; | |
// Determine winner | |
const result = determineWinner(playerMove, computerMove); | |
// Update scores and display result | |
if (result === 'win') { | |
playerScore++; | |
showResult('You win! π'); | |
} else if (result === 'lose') { | |
computerScore++; | |
showResult('Computer wins! π’'); | |
} else { | |
showResult("It's a tie! π€"); | |
} | |
updateScores(); | |
} | |
// Determine the winner of a round | |
function determineWinner(player, computer) { | |
if (player === computer) return 'tie'; | |
if ( | |
(player === 'rock' && computer === 'scissors') || | |
(player === 'paper' && computer === 'rock') || | |
(player === 'scissors' && computer === 'paper') | |
) { | |
return 'win'; | |
} | |
return 'lose'; | |
} | |
// Show game result | |
function showResult(message) { | |
resultDisplay.classList.remove('hidden'); | |
const resultElement = resultDisplay.querySelector('div'); | |
// Update styling based on result | |
if (message.includes('win')) { | |
resultElement.className = 'result-bubble inline-block bg-green-100 text-green-800 px-6 py-3 rounded-full text-xl font-bold'; | |
} else if (message.includes('lose')) { | |
resultElement.className = 'result-bubble inline-block bg-red-100 text-red-800 px-6 py-3 rounded-full text-xl font-bold'; | |
} else if (message.includes('tie')) { | |
resultElement.className = 'result-bubble inline-block bg-yellow-100 text-yellow-800 px-6 py-3 rounded-full text-xl font-bold'; | |
} else { | |
resultElement.className = 'result-bubble inline-block bg-blue-100 text-blue-800 px-6 py-3 rounded-full text-xl font-bold'; | |
} | |
resultElement.textContent = message; | |
} | |
// Update score displays | |
function updateScores() { | |
playerScoreElement.textContent = playerScore; | |
computerScoreElement.textContent = computerScore; | |
} | |
// Reset moves display | |
function resetMoves() { | |
playerMove = ''; | |
computerMove = ''; | |
playerMoveElement.textContent = '?'; | |
computerMoveElement.textContent = '?'; | |
showResult("Let's Play!"); | |
} | |
// Reset the entire game | |
function resetGame() { | |
playerScore = 0; | |
computerScore = 0; | |
updateScores(); | |
resetMoves(); | |
} | |
// Initialize the game when the page loads | |
window.addEventListener('DOMContentLoaded', initGame); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 𧬠<a href="https://enzostvs-deepsite.hf.space?remix=vs4vijay/rock-paper-scissors" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |