Gesture-Drop / script.js
Faheemkabeer's picture
Add QR code for phone scanning
3754f42 verified
Raw History Blame Contribute Delete
5.23 kB
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();