fruitlearn / js /main.js
Nguyen Van Manh
a
4cfb14f
Raw
History Blame Contribute Delete
3.34 kB
// 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());