const $ = id => document.getElementById(id); const limit = 1200; let note = "", stream = null, running = false, palmFrames = 0; function encode(text) { let binary = ""; for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte); return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } function decode(encoded) { if (!/^[A-Za-z0-9_-]+$/.test(encoded)) throw Error("Invalid link"); const binary = atob(encoded.replace(/-/g, "+").replace(/_/g, "/")); const text = new TextDecoder("utf-8", {fatal: true}).decode(Uint8Array.from(binary, c => c.charCodeAt(0))); if (!text || text.length > limit) throw Error("Invalid note"); return text; } function showQr(url) { const box = $("qr-code"); box.replaceChildren(); $("qr-wrap").classList.add("hidden"); $("qr-status").textContent = ""; if (url.length > 450) { $("qr-status").textContent = "This note is too long for an easy-to-scan QR code. Use Copy instead."; return; } if (!window.qrcode) { $("qr-status").textContent = "QR code could not load. Use Copy instead."; return; } try { const qr = qrcode(0, "M"); qr.addData(url); qr.make(); box.innerHTML = qr.createSvgTag({cellSize: 4, margin: 16, scalable: true}); $("qr-wrap").classList.remove("hidden"); } catch { $("qr-status").textContent = "This note could not fit in a QR code. Use Copy instead."; } } function load() { const match = /^#note=([A-Za-z0-9_-]+)$/.exec(location.hash); try { note = match ? decode(match[1]) : ""; } catch { note = ""; } $("note-panel").classList.add("hidden"); $("status").textContent = note ? "Note loaded. Show an open palm or press Reveal note." : location.hash ? "This share link is invalid." : "Open a share link to load a note."; } function reveal() { if (!note) { $("status").textContent = "Open a share link first."; return; } $("received").textContent = note; $("note-panel").classList.remove("hidden"); $("status").textContent = "Note revealed."; } function distance(a,b) { return Math.hypot(a[0]-b[0],a[1]-b[1]); } function openPalm(p) { const wrist = p[0]; const extended = [[8,6],[12,10],[16,14],[20,18]].filter(([tip,mid]) => distance(p[tip],wrist)>distance(p[mid],wrist)*1.22); return extended.length===4 && distance(p[4],p[17])>distance(p[3],p[17])*1.15; } async function detect(model) { running = true; while (running && stream) { try { const hands = await model.estimateHands($("camera")); const palm = hands.length>0 && openPalm(hands[0].landmarks); palmFrames = palm ? palmFrames+1 : 0; $("gesture").textContent = palm ? "Open palm detected" : "Show an open palm"; if (palmFrames===5) reveal(); } catch(error) { $("status").textContent = `Gesture detection stopped: ${error.message}`; break; } await new Promise(resolve => setTimeout(resolve,120)); } running = false; } function stop() { running = false; palmFrames = 0; stream?.getTracks().forEach(track => track.stop()); stream = null; $("camera").srcObject = null; $("camera").style.display = "none"; $("placeholder").style.display = "block"; $("gesture").textContent = "Camera off"; $("start").disabled = false; $("stop").disabled = true; } async function start() { if (stream) return; try { if (!navigator.mediaDevices?.getUserMedia) throw Error("Camera access is unavailable. Use Reveal note."); stream = await navigator.mediaDevices.getUserMedia({video:{facingMode:"user"},audio:false}); $("camera").srcObject = stream; $("camera").style.display = "block"; $("placeholder").style.display = "none"; $("start").disabled = true; $("stop").disabled = false; await $("camera").play(); $("status").textContent = "Loading hand model…"; if (!window.handpose) throw Error("Hand model could not load. Use Reveal note."); const model = await handpose.load(); if (!stream) return; $("status").textContent = "Camera ready. Hold an open palm in view."; detect(model); } catch(error) { stop(); $("status").textContent = `Camera unavailable: ${error.message}`; } } $("message").addEventListener("input", () => { $("count").textContent = $("message").value.length; }); $("create").addEventListener("click", () => { const text = $("message").value.trim(); if (!text) { $("message").focus(); return; } $("share-link").value = `${location.origin}${location.pathname}#note=${encode(text)}`; $("link-panel").classList.remove("hidden"); showQr($("share-link").value); }); $("copy-link").addEventListener("click", async () => { try { await navigator.clipboard.writeText($("share-link").value); $("copy-link").textContent="Copied"; } catch { $("share-link").select(); $("copy-link").textContent="Select and copy"; } }); $("copy-note").addEventListener("click", async () => { try { await navigator.clipboard.writeText(note); $("copy-note").textContent="Copied"; } catch { $("status").textContent="Select the note text to copy it."; } }); $("reveal").addEventListener("click",reveal); $("start").addEventListener("click",start); $("stop").addEventListener("click",stop); window.addEventListener("hashchange",load); window.addEventListener("pagehide",stop); load();