diff --git a/websnap/index.html b/websnap/index.html
new file mode 100644
index 0000000..76fad8f
--- /dev/null
+++ b/websnap/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+websnap
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/websnap/run.sh b/websnap/run.sh
new file mode 100755
index 0000000..d485c16
--- /dev/null
+++ b/websnap/run.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+
+BG=false
+for arg in "$@"; do
+ [[ "$arg" == "bg" || "$arg" == "--bg" ]] && BG=true
+done
+
+log() { $BG || echo "$@"; }
+
+# Find the HTML file
+HTML_FILE=$(find . -maxdepth 2 -name "*.html" | head -1)
+
+if [ -z "$HTML_FILE" ]; then
+ log "No HTML file found."
+ exit 1
+fi
+
+# Pick a free port
+PORT=8000
+while lsof -i :$PORT &>/dev/null; do
+ PORT=$((PORT + 1))
+done
+
+DIR=$(dirname "$HTML_FILE")
+FILE=$(basename "$HTML_FILE")
+
+log "Serving $HTML_FILE on http://localhost:$PORT/$FILE"
+
+open_browser() {
+ local url=$1
+ if command -v xdg-open &>/dev/null; then
+ xdg-open "$url"
+ elif command -v open &>/dev/null; then
+ open "$url"
+ elif command -v start &>/dev/null; then
+ start "$url"
+ else
+ log "open: $url"
+ fi
+}
+
+(sleep 0.5 && open_browser "http://localhost:$PORT/$FILE") &
+
+if $BG; then
+ python3 -m http.server "$PORT" --directory "$DIR" &>/dev/null &
+ disown
+else
+ python3 -m http.server "$PORT" --directory "$DIR"
+fi
diff --git a/websnap/script/main.js b/websnap/script/main.js
new file mode 100644
index 0000000..30367ab
--- /dev/null
+++ b/websnap/script/main.js
@@ -0,0 +1,105 @@
+/* main.js - websnap */
+import { HandLandmarker, FilesetResolver }
+ from "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest";
+
+/* set html elements */
+const video = document.getElementById("video");
+const canvas = document.getElementById("canvas");
+const overlay = document.getElementById("overlay");
+const octx = overlay.getContext("2d");
+const ctx = canvas.getContext("2d");
+const flash = document.getElementById("flash");
+
+let lastShot = 0;
+
+/* function to request fullscreen */
+function requestFS() {
+ const el = document.documentElement;
+ (el.requestFullscreen || el.webkitRequestFullscreen || el.mozRequestFullScreen)
+ ?.call(el);
+}
+requestFS();
+
+/* add listeners for fullscreen */
+document.addEventListener("click", requestFS, { once: true });
+document.addEventListener("touchend", requestFS, { once: true });
+document.addEventListener("dblclick", requestFS);
+
+/* camera setup */
+const stream = await navigator.mediaDevices.getUserMedia({
+ /* ideal res: 4096x2160 ; 4k */
+ video: { width: { ideal: 4096 }, height: { ideal: 2160 }, facingMode: "user" }
+});
+video.srcObject = stream;
+
+/* import mediapipe for handgestures */
+const vision = await FilesetResolver.forVisionTasks(
+ "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm"
+);
+const handLandmarker = await HandLandmarker.createFromOptions(vision, {
+ baseOptions: {
+ modelAssetPath:
+ "https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task"
+ },
+ runningMode: "VIDEO",
+ numHands: 1
+});
+
+/* flash for when picture gets taken */
+function triggerFlash() {
+ flash.classList.add("pop");
+ requestAnimationFrame(() => requestAnimationFrame(() => flash.classList.remove("pop")));
+}
+
+/* capture a picture */
+function takePhoto() {
+ triggerFlash();
+ const w = video.videoWidth, h = video.videoHeight;
+ canvas.width = w; canvas.height = h;
+ ctx.drawImage(video, 0, 0, w, h);
+ canvas.toBlob(blob => {
+ if (!blob) return;
+ saveAs(blob, "websnap-" + Date.now() + ".png");
+ }, "image/png");
+}
+
+/* detect shit */
+function detectLoop(timestamp) {
+ const dw = overlay.offsetWidth, dh = overlay.offsetHeight;
+ if (overlay.width !== dw || overlay.height !== dh) {
+ overlay.width = dw; overlay.height = dh;
+ }
+ /* draw rect */
+ octx.clearRect(0, 0, dw, dh);
+
+ if (video.readyState < 2) { requestAnimationFrame(detectLoop); return; }
+
+ const result = handLandmarker.detectForVideo(video, timestamp);
+
+ if (result.landmarks.length) {
+ const lm = result.landmarks[0];
+
+ /* finger indexes, for peace sign */
+ const indexUp = lm[8].y < lm[6].y;
+ const middleUp = lm[12].y < lm[10].y;
+ const ringDown = lm[16].y > lm[14].y;
+ const pinkyDown = lm[20].y > lm[18].y;
+ const fingerGap = Math.abs(lm[8].x - lm[12].x);
+ const peace =
+ indexUp &&
+ middleUp &&
+ ringDown &&
+ pinkyDown &&
+ fingerGap > 0.05;
+
+ if (peace && Date.now() - lastShot > 2500) { /* wait a bit */
+ takePhoto();
+ lastShot = Date.now();
+ }
+ }
+
+ requestAnimationFrame(detectLoop);
+}
+
+requestAnimationFrame(detectLoop);
+
diff --git a/websnap/style.css b/websnap/style.css
new file mode 100644
index 0000000..6e547bf
--- /dev/null
+++ b/websnap/style.css
@@ -0,0 +1,39 @@
+*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
+
+html, body {
+ width: 100%; height: 100%;
+ background: #000;
+ overflow: hidden;
+}
+
+#wrapper {
+ position: fixed;
+ inset: 0;
+}
+
+video {
+ width: 100%; height: 100%;
+ object-fit: cover;
+ display: block;
+}
+
+#overlay {
+ position: absolute;
+ inset: 0;
+ width: 100%; height: 100%;
+ pointer-events: none;
+}
+
+#flash {
+ position: fixed;
+ inset: 0;
+ background: white;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.20s ease-in;
+}
+#flash.pop {
+ opacity: 1;
+ transition: none;
+}
+