File size: 5,227 Bytes
108f534
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3754f42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108f534
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3754f42
108f534
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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();