Spaces:
Running
Running
| // Bootstrap + per-frame loop. Wires camera -> hand tracking -> coordinate mapping -> game. | |
| import { startCamera, CameraError } from "./camera.js"; | |
| import { initHandTracking, detectHand } from "./handTracking.js"; | |
| import { makeMapper, createSmoother } from "./coords.js"; | |
| import { initAudio, unlock } from "./audio.js"; | |
| import { createConfetti } from "./particles.js"; | |
| import { createGame } from "./game.js"; | |
| const $ = (id) => document.getElementById(id); | |
| const video = $("video"); | |
| const stage = $("stage"); | |
| const fruitsLayer = $("fruits-layer"); | |
| const handIcon = $("hand-icon"); | |
| const confettiCanvas = $("confetti"); | |
| const hint = $("hint"); | |
| const overlay = $("overlay"); | |
| const startScreen = $("start-screen"); | |
| const startBtn = $("start-btn"); | |
| const loading = $("loading"); | |
| const loadingText = $("loading-text"); | |
| const errorBox = $("error"); | |
| const errorText = $("error-text"); | |
| const retryBtn = $("retry-btn"); | |
| let game = null; | |
| let mapper = null; | |
| let smoother = null; | |
| let lastTs = 0; | |
| let noHandFrames = 0; | |
| function showLoading(msg) { | |
| startScreen.hidden = true; | |
| errorBox.hidden = true; | |
| loading.hidden = false; | |
| loadingText.textContent = msg; | |
| } | |
| function showError(err) { | |
| loading.hidden = true; | |
| startScreen.hidden = true; | |
| errorBox.hidden = false; | |
| errorText.textContent = | |
| err instanceof CameraError || err instanceof Error | |
| ? err.message | |
| : "Something went wrong. Please try again."; | |
| console.error(err); | |
| } | |
| function hideOverlay() { | |
| overlay.hidden = true; | |
| } | |
| async function start() { | |
| try { | |
| unlock(); // must happen inside the click gesture | |
| showLoading("Getting the voices ready…"); | |
| await initAudio(); | |
| showLoading("Turning on the camera… (please allow it)"); | |
| await startCamera(video); | |
| showLoading("Loading hand tracking… (first time takes a few seconds)"); | |
| await initHandTracking(); | |
| hideOverlay(); | |
| const confetti = createConfetti(confettiCanvas); | |
| mapper = makeMapper(video, stage); | |
| smoother = createSmoother(0.35); | |
| game = createGame({ | |
| stage, | |
| fruitsLayer, | |
| bannerMain: $("banner-main"), | |
| bannerSub: $("banner-sub"), | |
| scoreValue: $("score-value"), | |
| confetti, | |
| }); | |
| game.newRound(); | |
| requestAnimationFrame(loop); | |
| } catch (err) { | |
| showError(err); | |
| } | |
| } | |
| function positionHandIcon(p) { | |
| handIcon.style.transform = | |
| "translate(" + p.x + "px, " + p.y + "px) translate(-50%, -50%)"; | |
| } | |
| function loop(ts) { | |
| const dt = lastTs ? Math.min(100, ts - lastTs) : 16; // clamp to survive tab switches | |
| lastTs = ts; | |
| if (video.readyState >= 2) { | |
| const hand = detectHand(video, ts); | |
| if (hand) { | |
| const screen = mapper(hand.x, hand.y); | |
| const p = smoother.update(screen); | |
| if (p) { | |
| positionHandIcon(p); | |
| handIcon.classList.add("visible"); | |
| } | |
| hint.classList.remove("show"); | |
| noHandFrames = 0; | |
| game.update(p, dt); | |
| } else { | |
| noHandFrames += 1; | |
| smoother.reset(); | |
| if (noHandFrames > 15) { | |
| handIcon.classList.remove("visible"); | |
| hint.classList.add("show"); | |
| } | |
| game.update(null, dt); | |
| } | |
| } | |
| requestAnimationFrame(loop); | |
| } | |
| startBtn.addEventListener("click", start); | |
| retryBtn.addEventListener("click", () => location.reload()); | |
| $("replay").addEventListener("click", () => game && game.repeatPrompt()); | |