about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorelioat <elioat@tilde.institute>2025-03-29 23:20:25 -0400
committerelioat <elioat@tilde.institute>2025-03-29 23:20:25 -0400
commit752545e3350096d7afee89d14623f9a9f9f5076e (patch)
tree420caa0711e8a3d1b7e7104a63f0f93a8786f7d3
parent237686136e5037858e1c4182645991dfe3e7ff98 (diff)
downloadtour-752545e3350096d7afee89d14623f9a9f9f5076e.tar.gz
*
-rw-r--r--js/leibovitz/blur.js138
-rw-r--r--js/leibovitz/index.html6
-rw-r--r--js/leibovitz/leibovitz.js15
3 files changed, 158 insertions, 1 deletions
diff --git a/js/leibovitz/blur.js b/js/leibovitz/blur.js
new file mode 100644
index 0000000..6c7aff9
--- /dev/null
+++ b/js/leibovitz/blur.js
@@ -0,0 +1,138 @@
+// Blur management module
+// Uses the Observer pattern to notify the main camera module of blur changes
+
+const BlurManager = {
+    // Private state
+    _currentBlur: 0, // Default blur (no blur)
+    _observers: new Set(),
+    _slider: null,
+
+    // Initialize the blur manager
+    init() {
+        this._setupEventListeners();
+    },
+
+    // Private methods
+    _setupEventListeners() {
+        this._slider = document.getElementById('blur-slider');
+        this._slider.addEventListener('input', (e) => {
+            this._currentBlur = parseInt(e.target.value);
+            // Update the display value as a percentage
+            document.getElementById('blur-value').textContent = 
+                `${Math.round((this._currentBlur / 20) * 100)}%`;
+            this._notifyObservers();
+        });
+    },
+
+    _notifyObservers() {
+        this._observers.forEach(observer => observer(this._currentBlur));
+    },
+
+    // Public methods
+    subscribe(observer) {
+        this._observers.add(observer);
+        return () => this._observers.delete(observer);
+    },
+
+    getCurrentBlur() {
+        return this._currentBlur;
+    },
+
+    // Apply Gaussian blur to an image
+    applyBlur(imageData, radius) {
+        if (!radius || radius <= 0) return imageData;
+
+        const { data, width, height } = imageData;
+        const newData = new Uint8ClampedArray(data);
+        
+        // Create Gaussian kernel
+        const kernel = this._createGaussianKernel(radius);
+        const kernelSize = kernel.length;
+        const kernelRadius = Math.floor(kernelSize / 2);
+
+        // Apply horizontal blur
+        for (let y = 0; y < height; y++) {
+            for (let x = 0; x < width; x++) {
+                const idx = (y * width + x) * 4;
+                let r = 0, g = 0, b = 0, a = 0;
+                let weightSum = 0;
+
+                for (let i = -kernelRadius; i <= kernelRadius; i++) {
+                    const px = x + i;
+                    if (px >= 0 && px < width) {
+                        const pixelIdx = (y * width + px) * 4;
+                        const weight = kernel[i + kernelRadius];
+                        r += data[pixelIdx] * weight;
+                        g += data[pixelIdx + 1] * weight;
+                        b += data[pixelIdx + 2] * weight;
+                        a += data[pixelIdx + 3] * weight;
+                        weightSum += weight;
+                    }
+                }
+
+                newData[idx] = r / weightSum;
+                newData[idx + 1] = g / weightSum;
+                newData[idx + 2] = b / weightSum;
+                newData[idx + 3] = a / weightSum;
+            }
+        }
+
+        // Apply vertical blur
+        const finalData = new Uint8ClampedArray(newData);
+        for (let y = 0; y < height; y++) {
+            for (let x = 0; x < width; x++) {
+                const idx = (y * width + x) * 4;
+                let r = 0, g = 0, b = 0, a = 0;
+                let weightSum = 0;
+
+                for (let i = -kernelRadius; i <= kernelRadius; i++) {
+                    const py = y + i;
+                    if (py >= 0 && py < height) {
+                        const pixelIdx = (py * width + x) * 4;
+                        const weight = kernel[i + kernelRadius];
+                        r += newData[pixelIdx] * weight;
+                        g += newData[pixelIdx + 1] * weight;
+                        b += newData[pixelIdx + 2] * weight;
+                        a += newData[pixelIdx + 3] * weight;
+                        weightSum += weight;
+                    }
+                }
+
+                finalData[idx] = r / weightSum;
+                finalData[idx + 1] = g / weightSum;
+                finalData[idx + 2] = b / weightSum;
+                finalData[idx + 3] = a / weightSum;
+            }
+        }
+
+        return new ImageData(finalData, width, height);
+    },
+
+    // Create a 1D Gaussian kernel
+    _createGaussianKernel(radius) {
+        const sigma = radius / 3;
+        const kernelSize = Math.ceil(radius * 2 + 1);
+        const kernel = new Array(kernelSize);
+        let sum = 0;
+
+        for (let i = 0; i < kernelSize; i++) {
+            const x = i - radius;
+            kernel[i] = Math.exp(-(x * x) / (2 * sigma * sigma));
+            sum += kernel[i];
+        }
+
+        // Normalize the kernel
+        for (let i = 0; i < kernelSize; i++) {
+            kernel[i] /= sum;
+        }
+
+        return kernel;
+    },
+
+    // Add reset method
+    reset() {
+        this._currentBlur = 0;
+        this._slider.value = 0;
+        this._notifyObservers();
+    }
+}; 
\ No newline at end of file
diff --git a/js/leibovitz/index.html b/js/leibovitz/index.html
index acc592a..0f430f0 100644
--- a/js/leibovitz/index.html
+++ b/js/leibovitz/index.html
@@ -165,6 +165,11 @@
         <input type="range" id="block-size-slider" min="1" max="12" value="4" step="1">
         <span id="block-size-value">4px</span>
     </div>
+    <div class="contrast-control">
+        <label for="blur-slider">Blur</label>
+        <input type="range" id="blur-slider" min="0" max="20" value="0" step="1">
+        <span id="blur-value">0%</span>
+    </div>
 </div>
 
 <div id="controls">
@@ -178,6 +183,7 @@
 <script src="dither.js"></script>
 <script src="contrast.js"></script>
 <script src="color.js"></script>
+<script src="blur.js"></script>
 <script src="leibovitz.js"></script>
 </body>
 </html>
diff --git a/js/leibovitz/leibovitz.js b/js/leibovitz/leibovitz.js
index 01ce5ae..73ff7eb 100644
--- a/js/leibovitz/leibovitz.js
+++ b/js/leibovitz/leibovitz.js
@@ -11,6 +11,7 @@ let stream = null;
 ColorManager.init();
 DitherManager.init();
 ContrastManager.init();
+BlurManager.init();
 
 // Set the canvas dimensions to match the window size
 function updateCanvasSize() {
@@ -73,6 +74,7 @@ function startCamera() {
                     drawVideoProportional();
                     applyContrast();
                     applyColorTint();
+                    applyBlur();
                     applyDither();
                     requestAnimationFrame(step);
                 }
@@ -132,6 +134,15 @@ function applyDither() {
     ctx.putImageData(ditheredImageData, 0, 0);
 }
 
+function applyBlur() {
+    const currentBlur = BlurManager.getCurrentBlur();
+    if (!currentBlur) return;
+
+    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+    const blurredImageData = BlurManager.applyBlur(imageData, currentBlur);
+    ctx.putImageData(blurredImageData, 0, 0);
+}
+
 captureButton.addEventListener('click', () => {
     const currentColor = ColorManager.getCurrentColor();
     const borderWidth = 4;
@@ -179,4 +190,6 @@ if ('serviceWorker' in navigator) {
             console.log('ServiceWorker registration failed: ', err);
         });
     });
-}
\ No newline at end of file
+}
+
+ColorManager._setupEventListeners();
\ No newline at end of file