about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorelioat <elioat@tilde.institute>2024-06-30 16:54:02 -0400
committerelioat <elioat@tilde.institute>2024-06-30 16:54:02 -0400
commitb89b4715200a404354c8ef8d2ad5b57827b964d4 (patch)
tree40587cf98b5c9a6b8c3f094f794b9136aef7780b
parentafdf31268d6be9264171dde6a6ebab745ec981fe (diff)
downloadtour-b89b4715200a404354c8ef8d2ad5b57827b964d4.tar.gz
*
-rw-r--r--js/MAP.md3
-rw-r--r--js/pico-cam/index.html (renamed from js/dither-video/index.html)8
-rw-r--r--js/pico-cam/video.js (renamed from js/dither-video/video.js)46
3 files changed, 35 insertions, 22 deletions
diff --git a/js/MAP.md b/js/MAP.md
index eb51662..a692676 100644
--- a/js/MAP.md
+++ b/js/MAP.md
@@ -8,8 +8,6 @@
 - `canvas`, an exploration of the HTML canvas, lets you move a little sprite
   around a canvas
 - `dither`, Floyd-Steinberg dithering
-- `dither-vide`, naive application of Floyd-Steinberg dithering to streaming
-  webcam video
 - `game-frame`, my attempt at creating a generic starting point for HTML/JS game
   dev
 - `games`, some games from other people
@@ -25,6 +23,7 @@
 - `name-gen`, a funny random name generator and HTML canvas shape drawer
 - `notes`, really bad note taking surface
 - `peep`, a pulsing shape toy...not done, not nearly a complete thought
+- `pico-cam`, sort of like the Gameboy Camera but for the web
 - `pixel-art`, a pixel-art drawing tool
 - `pomo`, a sort of threatening pomodoro timer
 - `princess`, an idle game about a princess who maybe wants to over throw a
diff --git a/js/dither-video/index.html b/js/pico-cam/index.html
index f6918c7..4e3a416 100644
--- a/js/dither-video/index.html
+++ b/js/pico-cam/index.html
@@ -4,16 +4,12 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>Webcam with Dithering</title>
-    <style>
-        body {
-            background-color: beige;
-        }
-    </style>
 </head>
 <body>
     <button id="toggleCamera">Turn On Camera</button>
+    <button id="captureFrame" disabled>Capture Frame</button>
     <video id="webcam" autoplay playsinline style="display:none;"></video>
     <canvas id="ditheredOutput"></canvas>
     <script src="video.js"></script>
 </body>
-</html>
\ No newline at end of file
+</html>
diff --git a/js/dither-video/video.js b/js/pico-cam/video.js
index 124fab2..33d08df 100644
--- a/js/dither-video/video.js
+++ b/js/pico-cam/video.js
@@ -1,25 +1,27 @@
 const toggleCameraButton = document.getElementById('toggleCamera');
+const captureFrameButton = document.getElementById('captureFrame');
 const video = document.getElementById('webcam');
 const canvas = document.getElementById('ditheredOutput');
 const context = canvas.getContext('2d');
 
-// IDEA: Turn this into camera, where you can capture frames from the video feed and save them as .png files!?
-
 let stream = null;
 let isCameraOn = false;
-
-// FIXME: Sort out how to crop the image rather than squish and distort it.
 const lowResWidth = 160; // Gameboy camera resolution: 160×144
 const lowResHeight = 120; // If I use the GB camera resolution the image gets distorted
 
 toggleCameraButton.addEventListener('click', async () => {
     if (!isCameraOn) {
         try {
-            stream = await navigator.mediaDevices.getUserMedia({ video: true });
+            let constraints = { video: true };
+            if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
+                constraints = { video: { facingMode: 'environment' } };
+            }
+            stream = await navigator.mediaDevices.getUserMedia(constraints);
             video.srcObject = stream;
             video.style.display = 'block';
             isCameraOn = true;
             toggleCameraButton.textContent = 'Turn Camera Off';
+            captureFrameButton.disabled = false;
             video.addEventListener('loadeddata', () => {
                 canvas.width = lowResWidth;
                 canvas.height = lowResHeight;
@@ -33,24 +35,45 @@ toggleCameraButton.addEventListener('click', async () => {
         video.style.display = 'none';
         isCameraOn = false;
         toggleCameraButton.textContent = 'Turn Camera On';
+        captureFrameButton.disabled = true;
     }
 });
 
+captureFrameButton.addEventListener('click', () => {
+    const link = document.createElement('a');
+    link.href = canvas.toDataURL('image/png');
+    link.download = 'frame.png';
+    link.click();
+});
+
 function processFrame() {
     if (!isCameraOn) return;
 
-    context.drawImage(video, 0, 0, lowResWidth, lowResHeight);
+    // Crop and draw the central part of the video frame
+    const videoAspect = video.videoWidth / video.videoHeight;
+    const canvasAspect = lowResWidth / lowResHeight;
+    let sx, sy, sw, sh;
+    if (videoAspect > canvasAspect) {
+        sw = video.videoHeight * canvasAspect;
+        sh = video.videoHeight;
+        sx = (video.videoWidth - sw) / 2;
+        sy = 0;
+    } else {
+        sw = video.videoWidth;
+        sh = video.videoWidth / canvasAspect;
+        sx = 0;
+        sy = (video.videoHeight - sh) / 2;
+    }
+    context.drawImage(video, sx, sy, sw, sh, 0, 0, lowResWidth, lowResHeight);
     const frame = context.getImageData(0, 0, lowResWidth, lowResHeight);
     applyFloydSteinbergDithering(frame);
     context.putImageData(frame, 0, 0);
     requestAnimationFrame(processFrame);
 }
 
-
 function applyFloydSteinbergDithering(imageData) {
     const { data, width, height } = imageData;
 
-    // Helpers to access and set pixel values
     const getPixelIndex = (x, y) => (y * width + x) * 4;
     const getPixelValue = (x, y) => data[getPixelIndex(x, y)];
     const setPixelValue = (x, y, value) => {
@@ -60,17 +83,14 @@ function applyFloydSteinbergDithering(imageData) {
         data[index + 2] = value;
     };
 
-    // Helper that clamps a value between 0 and 255
     const clamp = (value) => Math.max(0, Math.min(255, value));
 
-    // Process a single pixel
     const processPixel = (x, y) => {
         const oldPixel = getPixelValue(x, y);
-        const newPixel = oldPixel < 128 ? 0 : 255; // Threshold of the pixel value
+        const newPixel = oldPixel < 128 ? 0 : 255; // 1-bit black and white
         setPixelValue(x, y, newPixel);
         const quantError = oldPixel - newPixel;
 
-        // Spreads the error to neighboring pixels
         if (x + 1 < width) {
             const idx = getPixelIndex(x + 1, y);
             data[idx] = clamp(data[idx] + quantError * 7 / 16);
@@ -89,8 +109,6 @@ function applyFloydSteinbergDithering(imageData) {
         }
     };
 
-    // TODO: This seems like a mad inefficient way to do this, but it does work. Noodle on if it is worth making this more efficient.
-    // Process all of the pixels
     for (let y = 0; y < height; y++) {
         for (let x = 0; x < width; x++) {
             processPixel(x, y);