diff options
author | elioat <elioat@tilde.institute> | 2024-06-30 16:54:02 -0400 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2024-06-30 16:54:02 -0400 |
commit | b89b4715200a404354c8ef8d2ad5b57827b964d4 (patch) | |
tree | 40587cf98b5c9a6b8c3f094f794b9136aef7780b | |
parent | afdf31268d6be9264171dde6a6ebab745ec981fe (diff) | |
download | tour-b89b4715200a404354c8ef8d2ad5b57827b964d4.tar.gz |
*
-rw-r--r-- | js/MAP.md | 3 | ||||
-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); |