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