(async () => { const meta = await (await fetch("/slide_meta")).json(); const slides = meta.slides; let current = 0; const slideImg = document.getElementById("slide"); const count = document.getElementById("count"); function show(i) { if (slides.length === 0) { count.innerText = "No slides in /slides folder"; return; } current = Math.max(0, Math.min(slides.length - 1, i)); slideImg.src = "/slides/" + slides[current]; count.innerText = `${current + 1} / ${slides.length}`; } show(0); // Webcam + Mediapipe const video = document.getElementById("webcam"); const canvas = document.getElementById("overlay"); const ctx = canvas.getContext("2d"); let lastX = null, lastTime = null, cooldown = 0; function onResults(res) { ctx.clearRect(0, 0, canvas.width, canvas.height); if (res.multiHandLandmarks && res.multiHandLandmarks.length > 0) { const lm = res.multiHandLandmarks[0]; window.drawLandmarks(ctx, lm); const x = lm[0].x; const now = performance.now(); if (lastX !== null) { const dx = x - lastX; const dt = (now - lastTime) / 1000; const speed = dx / dt; if (now > cooldown) { if (speed > 1.3) { show(current + 1); // Next slide cooldown = now + 800; } if (speed < -1.3) { show(current - 1); // Previous slide cooldown = now + 800; } } } lastX = x; lastTime = now; } } const hands = new Hands({ locateFile: f => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${f}` }); hands.setOptions({ maxNumHands: 1, modelComplexity: 0, minDetectionConfidence: 0.6, minTrackingConfidence: 0.6 }); hands.onResults(onResults); const camera = new Camera(video, { onFrame: async () => await hands.send({ image: video }), width: 320, height: 240 }); camera.start(); })();