diff options
author | elioat <elioat@tilde.institute> | 2024-06-30 16:23:29 -0400 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2024-06-30 16:23:29 -0400 |
commit | a0ca7781ca50924ce89a1a6187c6f7cabef70a7f (patch) | |
tree | 61b781c9d74ea319d8ca78fad4e5010316e906a5 | |
parent | 3900b10bd2eb73d9a3b2e52973b89546b4ec43ee (diff) | |
download | tour-a0ca7781ca50924ce89a1a6187c6f7cabef70a7f.tar.gz |
*
-rw-r--r-- | js/dither-video/index.html | 19 | ||||
-rw-r--r-- | js/dither-video/video.js | 101 |
2 files changed, 120 insertions, 0 deletions
diff --git a/js/dither-video/index.html b/js/dither-video/index.html new file mode 100644 index 0000000..f6918c7 --- /dev/null +++ b/js/dither-video/index.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <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> + <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 diff --git a/js/dither-video/video.js b/js/dither-video/video.js new file mode 100644 index 0000000..0da0157 --- /dev/null +++ b/js/dither-video/video.js @@ -0,0 +1,101 @@ +const toggleCameraButton = document.getElementById('toggleCamera'); +const video = document.getElementById('webcam'); +const canvas = document.getElementById('ditheredOutput'); +const context = canvas.getContext('2d'); + +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 }); + video.srcObject = stream; + video.style.display = 'block'; + isCameraOn = true; + toggleCameraButton.textContent = 'Turn Camera Off'; + video.addEventListener('loadeddata', () => { + canvas.width = lowResWidth; + canvas.height = lowResHeight; + requestAnimationFrame(processFrame); + }); + } catch (err) { + console.error('Failed to access the camera: ', err); + } + } else { + stream.getTracks().forEach(track => track.stop()); + video.style.display = 'none'; + isCameraOn = false; + toggleCameraButton.textContent = 'Turn Camera On'; + } +}); + +function processFrame() { + if (!isCameraOn) return; + + context.drawImage(video, 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) => { + const index = getPixelIndex(x, y); + data[index] = value; + data[index + 1] = value; + 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 + 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); + } + if (x - 1 >= 0 && y + 1 < height) { + const idx = getPixelIndex(x - 1, y + 1); + data[idx] = clamp(data[idx] + quantError * 3 / 16); + } + if (y + 1 < height) { + const idx = getPixelIndex(x, y + 1); + data[idx] = clamp(data[idx] + quantError * 5 / 16); + } + if (x + 1 < width && y + 1 < height) { + const idx = getPixelIndex(x + 1, y + 1); + data[idx] = clamp(data[idx] + quantError * 1 / 16); + } + }; + + // 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); + } + } +} + +function clamp(value) { + return Math.max(0, Math.min(255, value)); +} |