Gravity Hands
by GoldenBear802171 lines33.6 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gravity Hands</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at 50% 20%, #17132d, #07070d 50%, #020204);
color: white;
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
}
header {
width: min(900px, 92vw);
display: flex;
justify-content: space-between;
align-items: center;
padding: 22px 0 10px;
}
.title {
font-size: 32px;
font-weight: 800;
letter-spacing: 2px;
background: linear-gradient(
90deg,
#a855f7,
#6366f1,
#00e5ff
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.status {
padding: 8px 14px;
border-radius: 20px;
background: rgba(0,255,255,.05);
border: 1px solid rgba(0,255,255,.25);
font-size: 13px;
}
.subtitle {
color: #9999aa;
margin: 4px 0 18px;
}
.subtitle span {
color: #00e5ff;
}
#restartButton {
display: none;
position: absolute;
left: 50%;
top: 60%;
transform: translate(-50%, -50%);
z-index: 100;
pointer-events: auto;
padding: 14px 28px;
border: 1px solid rgba(255,255,255,0.25);
border-radius: 12px;
background: rgba(20, 20, 30, 0.9);
color: white;
font-size: 16px;
font-weight: 600;
cursor: pointer;
backdrop-filter: blur(10px);
transition:
transform 0.2s,
background 0.2s;
}
#restartButton:hover {
transform:
translate(-50%, -50%)
scale(1.05);
background: rgba(40, 40, 55, 0.95);
}
#gameArea {
position: relative;
width: min(900px, 92vw);
aspect-ratio: 16 / 10;
overflow: hidden;
border-radius: 22px;
border: 1px solid rgba(0,220,255,.35);
background: #030307;
box-shadow:
0 0 50px rgba(0,100,255,.15),
inset 0 0 60px rgba(80,0,255,.08);
}
#webcam {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
transform: scaleX(-1);
opacity: .35;
}
#canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 5;
}
#message {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 20;
text-align: center;
pointer-events: none;
font-size: 28px;
font-weight: bold;
text-shadow: 0 0 20px #00e5ff;
opacity: 0;
transition: opacity .3s;
}
#hud {
width: min(900px, 92vw);
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-top: 15px;
}
.card {
background: rgba(255,255,255,.035);
border: 1px solid rgba(255,255,255,.08);
border-radius: 14px;
padding: 12px 15px;
}
.label {
font-size: 10px;
color: #77778a;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 5px;
}
.value {
color: #00e5ff;
font-weight: bold;
}
#gravityState.active {
color: #c084fc;
text-shadow: 0 0 12px #a855f7;
}
@media(max-width:650px) {
#hud {
grid-template-columns: repeat(2,1fr);
}
.title {
font-size: 23px;
}
}
/* Screen shake effect */
#gameArea.shake {
animation: screenShake 0.35s ease;
}
@keyframes screenShake {
0% { transform: translate(0, 0); }
15% { transform: translate(-8px, 5px); }
30% { transform: translate(7px, -6px); }
45% { transform: translate(-6px, -4px); }
60% { transform: translate(5px, 6px); }
75% { transform: translate(-3px, -3px); }
100% { transform: translate(0, 0); }
}
</style>
</head>
<body>
<header>
<div class="title">
🪐 GRAVITY HANDS
</div>
<div class="status">
🟢 HAND TRACKER
</div>
</header>
<div class="subtitle">
Move your <span>finger</span> • Pinch to control gravity
</div>
<div id="gameArea">
<video
id="webcam"
autoplay
playsinline>
</video>
<canvas id="canvas"></canvas>
<div id="message"></div>
<button id="restartButton" >
🔄 Restart Game
</button>
</div>
<div id="hud">
<div class="card">
<div class="label">
Tracking
</div>
<div
class="value"
id="trackingState">
SEARCHING
</div>
</div>
<div class="card">
<div class="label">
Gravity
</div>
<div
class="value"
id="gravityState">
READY
</div>
</div>
<div class="card">
<div class="label">
Level
</div>
<div
class="value"
id="level">
1
</div>
</div>
<div class="card">
<div class="label">
Lives
</div>
<div
class="value"
id="lives">
❤️❤️❤️
</div>
</div>
<div class="card">
<div class="label">
Time
</div>
<div
class="value"
id="timer">
20
</div>
</div>
<div class="card">
<div class="label">
Score
</div>
<div
class="value"
id="score">
0
</div>
</div>
<div class="card">
<div class="label">
Goal
</div>
<div class="value">
Reach Portal
</div>
</div>
</div>
<script type="module">
import {
HandLandmarker,
FilesetResolver
}
from
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/vision_bundle.mjs";
// =====================================================
// ELEMENTS
// =====================================================
const video =
document.getElementById("webcam");
const canvas =
document.getElementById("canvas");
const ctx =
canvas.getContext("2d");
const trackingState =
document.getElementById("trackingState");
const gravityState =
document.getElementById("gravityState");
const scoreElement =
document.getElementById("score");
const message =
document.getElementById("message");
// =====================================================
// CANVAS
// =====================================================
function resizeCanvas() {
const rect =
canvas.getBoundingClientRect();
canvas.width =
rect.width;
canvas.height =
rect.height;
}
window.addEventListener(
"resize",
resizeCanvas
);
resizeCanvas();
// =====================================================
// GAME VARIABLES
// =====================================================
let handLandmarker;
let gravityActive = false;
let score = 0;
let level = 1;
let lives = 3;
let gameOver = false;
let mineCooldown = false;
let timeLeft = 20;
let timerRunning = false;
let timerInterval = null;
let gameWon = false;
// =====================================================
// BALL
// =====================================================
const ball = {
x: 100,
y: 100,
vx: 0,
vy: 0,
radius: 15
};
// =====================================================
// PORTAL
// =====================================================
const portal = {
x: 700,
y: 300,
radius: 42
};
// =====================================================
// PARTICLES
// =====================================================
const particles = [];
const mines = [];
// =====================================================
// RANDOM PORTAL
// =====================================================
function movePortal() {
portal.x =
80 +
Math.random() *
(canvas.width - 160);
portal.y =
80 +
Math.random() *
(canvas.height - 160);
}
function createMines(amount) {
mines.length = 0;
for (let i = 0; i < amount; i++) {
let mine;
// Keep trying until we find a reasonable position
do {
mine = {
x: 70 + Math.random() * (canvas.width - 140),
y: 70 + Math.random() * (canvas.height - 140),
radius: 18
};
} while (
Math.hypot(
mine.x - ball.x,
mine.y - ball.y
) < 120 ||
Math.hypot(
mine.x - portal.x,
mine.y - portal.y
) < 120
);
mines.push(mine);
}
}
// Draw the mines 🔴
function drawMines() {
const time =
performance.now() * 0.003;
for (const mine of mines) {
const pulse =
1 + Math.sin(
time + mine.x
) * 0.12;
// Outer glow
const glow =
ctx.createRadialGradient(
mine.x,
mine.y,
0,
mine.x,
mine.y,
45
);
glow.addColorStop(
0,
"rgba(255, 40, 70, 0.35)"
);
glow.addColorStop(
0.5,
"rgba(255, 0, 40, 0.12)"
);
glow.addColorStop(
1,
"rgba(255, 0, 0, 0)"
);
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(
mine.x,
mine.y,
45,
0,
Math.PI * 2
);
ctx.fill();
// Mine body
ctx.fillStyle = "#26050b";
ctx.strokeStyle = "#ff304f";
ctx.lineWidth = 2;
ctx.shadowColor = "#ff1744";
ctx.shadowBlur = 15;
ctx.beginPath();
ctx.arc(
mine.x,
mine.y,
mine.radius * pulse,
0,
Math.PI * 2
);
ctx.fill();
ctx.stroke();
ctx.shadowBlur = 0;
// Core
ctx.fillStyle = "#ff304f";
ctx.beginPath();
ctx.arc(
mine.x,
mine.y,
5,
0,
Math.PI * 2
);
ctx.fill();
// Little warning spikes
ctx.strokeStyle =
"rgba(255,80,100,.7)";
ctx.lineWidth = 2;
for (let i = 0; i < 8; i++) {
const angle =
i * Math.PI / 4;
const inner = 22;
const outer = 28;
ctx.beginPath();
ctx.moveTo(
mine.x +
Math.cos(angle) * inner,
mine.y +
Math.sin(angle) * inner
);
ctx.lineTo(
mine.x +
Math.cos(angle) * outer,
mine.y +
Math.sin(angle) * outer
);
ctx.stroke();
}
}
}
// =====================================================
// RESET BALL
// =====================================================
function resetBall() {
ball.x = 80;
ball.y =
80 +
Math.random() *
(canvas.height - 160);
ball.vx = 0;
ball.vy = 0;
gameWon = false;
message.style.opacity = 0;
}
// =====================================================
// PARTICLES
// =====================================================
function spawnParticle(
x,
y,
color = "#00e5ff"
) {
particles.push({
x,
y,
vx:
(Math.random() - .5) *
2,
vy:
(Math.random() - .5) *
2,
life: 1,
color
});
if (particles.length > 200) {
particles.shift();
}
}
function updateParticles() {
for (
let i = particles.length - 1;
i >= 0;
i--
) {
const p =
particles[i];
p.x += p.vx;
p.y += p.vy;
p.life -= .025;
ctx.globalAlpha =
p.life;
ctx.fillStyle =
p.color;
ctx.beginPath();
ctx.arc(
p.x,
p.y,
2,
0,
Math.PI * 2
);
ctx.fill();
if (p.life <= 0) {
particles.splice(i,1);
}
}
ctx.globalAlpha = 1;
}
// =====================================================
// DRAW BALL
// =====================================================
function drawBall() {
const glow =
ctx.createRadialGradient(
ball.x,
ball.y,
0,
ball.x,
ball.y,
35
);
glow.addColorStop(
0,
"white"
);
glow.addColorStop(
.25,
"#00e5ff"
);
glow.addColorStop(
1,
"rgba(0,229,255,0)"
);
ctx.fillStyle =
glow;
ctx.beginPath();
ctx.arc(
ball.x,
ball.y,
35,
0,
Math.PI * 2
);
ctx.fill();
ctx.fillStyle =
"white";
ctx.beginPath();
ctx.arc(
ball.x,
ball.y,
ball.radius,
0,
Math.PI * 2
);
ctx.fill();
}
// =====================================================
// DRAW PORTAL
// =====================================================
function drawPortal() {
const time =
performance.now() *
.001;
ctx.save();
ctx.translate(
portal.x,
portal.y
);
ctx.rotate(
time
);
ctx.strokeStyle =
"#a855f7";
ctx.lineWidth = 5;
ctx.shadowColor =
"#a855f7";
ctx.shadowBlur = 25;
ctx.beginPath();
ctx.arc(
0,
0,
portal.radius,
0,
Math.PI * 2
);
ctx.stroke();
ctx.rotate(
-time * 2
);
ctx.strokeStyle =
"#00e5ff";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(
0,
0,
portal.radius - 10,
0,
Math.PI * 2
);
ctx.stroke();
ctx.restore();
// center
const glow =
ctx.createRadialGradient(
portal.x,
portal.y,
0,
portal.x,
portal.y,
portal.radius
);
glow.addColorStop(
0,
"rgba(168,85,247,.25)"
);
glow.addColorStop(
1,
"rgba(168,85,247,0)"
);
ctx.fillStyle =
glow;
ctx.beginPath();
ctx.arc(
portal.x,
portal.y,
portal.radius,
0,
Math.PI * 2
);
ctx.fill();
}
// =====================================================
// GRAVITY PHYSICS
// =====================================================
function updatePhysics(gravityX, gravityY) {
if (gameOver) return;
// ==========================================
// MAGNETIC GRAVITY
// ==========================================
if (gravityActive) {
const dx = gravityX - ball.x;
const dy = gravityY - ball.y;
const distance = Math.hypot(dx, dy);
if (distance > 5) {
// ----------------------------------
// STRONG MAGNETIC PULL
// ----------------------------------
const strength =
Math.min(
0.055,
0.018 + distance * 0.00012
);
ball.vx += dx * strength;
ball.vy += dy * strength;
// ----------------------------------
// QUICKLY REMOVE OLD MOMENTUM
// ----------------------------------
ball.vx *= 0.86;
ball.vy *= 0.86;
}
}
else {
// --------------------------------------
// WHEN GRAVITY IS OFF
// SLOW THE BALL DOWN
// --------------------------------------
ball.vx *= 0.94;
ball.vy *= 0.94;
}
// ==========================================
// SPEED LIMIT
// ==========================================
const maxSpeed = gravityActive
? 12
: 7;
const speed = Math.hypot(
ball.vx,
ball.vy
);
if (speed > maxSpeed) {
ball.vx =
(ball.vx / speed) *
maxSpeed;
ball.vy =
(ball.vy / speed) *
maxSpeed;
}
// ==========================================
// MOVE BALL
// ==========================================
ball.x += ball.vx;
ball.y += ball.vy;
// ==========================================
// WALL COLLISION
// ==========================================
if (
ball.x <
ball.radius
) {
ball.x =
ball.radius;
ball.vx *= -0.65;
}
if (
ball.x >
canvas.width -
ball.radius
) {
ball.x =
canvas.width -
ball.radius;
ball.vx *= -0.65;
}
if (
ball.y <
ball.radius
) {
ball.y =
ball.radius;
ball.vy *= -0.65;
}
if (
ball.y >
canvas.height -
ball.radius
) {
ball.y =
canvas.height -
ball.radius;
ball.vy *= -0.65;
}
// ==========================================
// PORTAL COLLISION
// ==========================================
const portalDX =
ball.x - portal.x;
const portalDY =
ball.y - portal.y;
const portalDistance =
Math.hypot(
portalDX,
portalDY
);
// ==========================================
// MINE COLLISION
// ==========================================
for (const mine of mines) {
const dx =
ball.x - mine.x;
const dy =
ball.y - mine.y;
const distance =
Math.hypot(dx, dy);
if (
distance <
ball.radius + mine.radius
) {
hitMine(mine);
return;
}
}
if (
portalDistance <
portal.radius
) {
winLevel();
}
}
// =================================================
function hitMine(mine) {
if (mineCooldown) return;
mineCooldown = true;
// Lose one life
lives--;
document.getElementById("lives").textContent =
"❤️".repeat(lives) + "🖤".repeat(3 - lives);
// ==========================================
// SCREEN SHAKE
// ==========================================
// ==========================================
// SCREEN SHAKE
// ==========================================
const gameArea =
document.getElementById("gameArea") ||
document.querySelector("canvas")?.parentElement;
if (gameArea) {
gameArea.classList.remove("shake");
void gameArea.offsetWidth;
gameArea.classList.add("shake");
setTimeout(() => {
gameArea.classList.remove("shake");
}, 400);
}
// ==========================================
// RED FLASH
// ==========================================
const flash =
document.createElement("div");
flash.style.position = "absolute";
flash.style.inset = "0";
flash.style.background =
"rgba(255, 30, 50, 0.28)";
flash.style.pointerEvents = "none";
flash.style.zIndex = "15";
gameArea.appendChild(flash);
setTimeout(() => {
flash.style.transition =
"opacity .3s";
flash.style.opacity = "0";
setTimeout(() => {
flash.remove();
}, 300);
}, 50);
// ==========================================
// EXPLOSION PARTICLES
// ==========================================
for (let i = 0; i < 80; i++) {
const angle =
Math.random() * Math.PI * 2;
const speed =
2 + Math.random() * 8;
particles.push({
x: mine.x,
y: mine.y,
vx:
Math.cos(angle) * speed,
vy:
Math.sin(angle) * speed,
life:
0.7 + Math.random() * 0.5,
color:
i % 4 === 0
? "#ffffff"
: i % 4 === 1
? "#ffb000"
: i % 4 === 2
? "#ff304f"
: "#ff6b35"
});
}
// ==========================================
// STOP BALL
// ==========================================
ball.vx = 0;
ball.vy = 0;
// ==========================================
// GAME OVER
// ==========================================
if (lives <= 0) {
gameOver = true;
timerRunning = false;
clearInterval(timerInterval);
ball.vx = 0;
ball.vy = 0;
message.innerHTML =
"💀 GAME OVER<br>" +
"<small>All lives lost</small>";
message.style.opacity = 1;
document.getElementById("restartButton")
.style.display = "block";
return;
}
// ==========================================
// RESET AFTER HIT
// ==========================================
message.innerHTML =
"💥 MINE HIT!<br>" +
"<small>❤️ " + lives +
" lives remaining</small>";
message.style.opacity = 1;
setTimeout(() => {
resetBall();
message.style.opacity = 0;
mineCooldown = false;
}, 900);
}
///////////////////////////////////////////
function startTimer() {
if (gameOver) return;
clearInterval(timerInterval);
timeLeft = Math.max(
20 - (level - 1) * 2,
8
);
timerRunning = true;
document.getElementById("timer").textContent =
timeLeft;
timerInterval = setInterval(() => {
if (!timerRunning) return;
timeLeft--;
document.getElementById("timer").textContent =
timeLeft;
if (timeLeft <= 0) {
timerRunning = false;
clearInterval(timerInterval);
timeUp();
}
}, 1000);
}
////////////////////////////////
function restartGame() {
console.log("RESTART CLICKED");
// Stop current timer
clearInterval(timerInterval);
// Reset game state
lives = 3;
level = 1;
score = 0;
gameOver = false;
mineCooldown = false;
timerRunning = false;
// Reset HUD
document.getElementById("lives").textContent =
"❤️❤️❤️";
document.getElementById("level").textContent =
"1";
scoreElement.textContent =
"0";
// Hide game-over UI
message.style.opacity = 0;
document.getElementById("restartButton")
.style.display = "none";
// Reset game objects
resetBall();
movePortal();
createMines(3);
// Start fresh timer
startTimer();
console.log("GAME RESTARTED");
}
const restartButton =
document.getElementById("restartButton");
restartButton.addEventListener(
"click",
restartGame
);
////////////////////////////////
function timeUp() {
message.innerHTML =
"⏱️ TIME'S UP!<br><small>Try again...</small>";
message.style.opacity = 1;
ball.vx = 0;
ball.vy = 0;
setTimeout(() => {
resetBall();
startTimer();
}, 1000);
}
// =====================================================
// WIN
// =====================================================
function winLevel() {
if (gameOver) return;
if (gameWon) return;
gameWon = true;
score++;
level++;
scoreElement.textContent =
score;
document.getElementById("level")
.textContent = level;
timerRunning = false;
clearInterval(timerInterval);
scoreElement.textContent =
score;
message.innerHTML =
"🌀 PORTAL REACHED!<br><small>Next level...</small>";
message.style.opacity = 1;
for (
let i = 0;
i < 50;
i++
) {
spawnParticle(
portal.x,
portal.y,
i % 2
? "#00e5ff"
: "#a855f7"
);
}
setTimeout(() => {
movePortal();
resetBall();
createMines(
Math.min(3 + score, 8)
);
startTimer();
}, 1200);
}
// =====================================================
// GRAVITY ORB
// =====================================================
function drawGravityOrb(
x,
y,
active
) {
const time =
performance.now() *
.001;
const outer =
active
? 105
: 75;
const glow =
ctx.createRadialGradient(
x,
y,
0,
x,
y,
outer
);
glow.addColorStop(
0,
active
? "rgba(168,85,247,.40)"
: "rgba(0,229,255,.25)"
);
glow.addColorStop(
.4,
"rgba(99,102,241,.12)"
);
glow.addColorStop(
1,
"rgba(0,0,0,0)"
);
ctx.fillStyle =
glow;
ctx.beginPath();
ctx.arc(
x,
y,
outer,
0,
Math.PI * 2
);
ctx.fill();
// target ring
ctx.save();
ctx.translate(
x,
y
);
ctx.rotate(
time * .5
);
ctx.strokeStyle =
active
? "#c084fc"
: "#00e5ff";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(
0,
0,
active ? 62 : 52,
0,
Math.PI * 2
);
ctx.stroke();
// ticks
for (
let i = 0;
i < 12;
i++
) {
const a =
i *
Math.PI *
2 /
12;
const r1 =
active ? 62 : 52;
const r2 =
active ? 70 : 60;
ctx.beginPath();
ctx.moveTo(
Math.cos(a) * r1,
Math.sin(a) * r1
);
ctx.lineTo(
Math.cos(a) * r2,
Math.sin(a) * r2
);
ctx.stroke();
}
ctx.restore();
// rotating ellipse
ctx.save();
ctx.translate(
x,
y
);
ctx.rotate(
time
);
ctx.strokeStyle =
"#a855f7";
ctx.lineWidth = 3;
ctx.shadowColor =
"#a855f7";
ctx.shadowBlur = 15;
ctx.beginPath();
ctx.ellipse(
0,
0,
active ? 48 : 40,
active ? 15 : 12,
0,
0,
Math.PI * 2
);
ctx.stroke();
ctx.restore();
// second ring
ctx.save();
ctx.translate(
x,
y
);
ctx.rotate(
-time * 1.5
);
ctx.strokeStyle =
"#00e5ff";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.ellipse(
0,
0,
active ? 34 : 29,
active ? 10 : 8,
0,
0,
Math.PI * 2
);
ctx.stroke();
ctx.restore();
// central orb
const pulse =
(active ? 18 : 14) +
Math.sin(time * 5) *
3;
const orb =
ctx.createRadialGradient(
x - 4,
y - 4,
1,
x,
y,
pulse
);
orb.addColorStop(
0,
"#fff"
);
orb.addColorStop(
.25,
"#00ffff"
);
orb.addColorStop(
.6,
active
? "#a855f7"
: "#6366f1"
);
orb.addColorStop(
1,
"rgba(100,0,255,0)"
);
ctx.fillStyle =
orb;
ctx.shadowColor =
active
? "#a855f7"
: "#00e5ff";
ctx.shadowBlur =
active ? 35 : 25;
ctx.beginPath();
ctx.arc(
x,
y,
pulse,
0,
Math.PI * 2
);
ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle =
"white";
ctx.beginPath();
ctx.arc(
x,
y,
3,
0,
Math.PI * 2
);
ctx.fill();
}
// =====================================================
// HAND TRACKING
// =====================================================
let lastVideoTime = -1;
let handX = null;
let handY = null;
function detectHands() {
ctx.clearRect(
0,
0,
canvas.width,
canvas.height
);
updateParticles();
if (
video.readyState >= 2 &&
video.currentTime !==
lastVideoTime
) {
lastVideoTime =
video.currentTime;
const results =
handLandmarker
.detectForVideo(
video,
performance.now()
);
if (
results.landmarks &&
results.landmarks.length
) {
const landmarks =
results.landmarks[0];
const index =
landmarks[8];
const thumb =
landmarks[4];
handX =
(1 - index.x) *
canvas.width;
handY =
index.y *
canvas.height;
const distance =
Math.hypot(
thumb.x - index.x,
thumb.y - index.y
);
gravityActive =
distance < .07;
trackingState.textContent =
"ACTIVE";
if (gravityActive) {
gravityState.textContent =
"ACTIVE";
gravityState.classList
.add("active");
}
else {
gravityState.textContent =
"READY";
gravityState.classList
.remove("active");
}
drawGravityOrb(
handX,
handY,
gravityActive
);
updatePhysics(
handX,
handY
);
}
else {
trackingState.textContent =
"SEARCHING";
gravityActive = false;
gravityState.textContent =
"READY";
gravityState.classList
.remove("active");
updatePhysics(
canvas.width / 2,
canvas.height / 2
);
}
}
drawMines();
drawPortal();
drawBall();
requestAnimationFrame(
detectHands
);
}
// =====================================================
// MEDIAPIPE
// =====================================================
async function createHandTracker() {
console.log(
"Loading MediaPipe..."
);
const vision =
await FilesetResolver
.forVisionTasks(
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm"
);
handLandmarker =
await HandLandmarker
.createFromOptions(
vision,
{
baseOptions: {
modelAssetPath:
"https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task"
},
runningMode:
"VIDEO",
numHands: 1
}
);
console.log(
"🖐️ Hand tracker ready!"
);
try {
const stream =
await navigator.mediaDevices
.getUserMedia({
video: true
});
video.srcObject =
stream;
console.log(
"Camera is working!"
);
video.addEventListener(
"loadeddata",
() => {
detectHands();
},
{ once: true }
);
}
catch (error) {
console.error(
"Camera error:",
error
);
}
}
// =====================================================
// START
// =====================================================
movePortal();
resetBall();
createMines(3);
startTimer();
createHandTracker();
</script>
</body>
</html>Game Source: Gravity Hands
Creator: GoldenBear80
Libraries: none
Complexity: complex (2171 lines, 33.6 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: gravity-hands-goldenbear80" to link back to the original. Then publish at arcadelab.ai/publish.