about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorelioat <elioat@tilde.institute>2025-03-29 22:57:41 -0400
committerelioat <elioat@tilde.institute>2025-03-29 22:57:41 -0400
commit771835d9e10686e0a89712af5cd1c3825c1cfc39 (patch)
treef4debb1b58b9010a86b5b171a3b9981121d8d0c8
parent5eeed0ebe09bc27e0cb4c1e5d71d5fad6946fa50 (diff)
downloadtour-771835d9e10686e0a89712af5cd1c3825c1cfc39.tar.gz
*
-rw-r--r--js/leibovitz/dither.js265
-rw-r--r--js/leibovitz/index.html12
2 files changed, 258 insertions, 19 deletions
diff --git a/js/leibovitz/dither.js b/js/leibovitz/dither.js
index 2a02834..90c4ae2 100644
--- a/js/leibovitz/dither.js
+++ b/js/leibovitz/dither.js
@@ -6,10 +6,13 @@ const DitherManager = {
     _currentMode: 'none',
     _observers: new Set(),
     _modeSelect: null,
+    _pixelSizeControl: null,
+    currentBlockSize: 4,
 
     // Initialize the dither manager
     init() {
         this._setupEventListeners();
+        this._pixelSizeControl = document.getElementById('pixel-size-control');
     },
 
     // Private methods
@@ -17,8 +20,23 @@ const DitherManager = {
         this._modeSelect = document.getElementById('dither-select');
         this._modeSelect.addEventListener('change', (e) => {
             this._currentMode = e.target.value;
+            // Show/hide pixel size control based on dithering mode
+            this._pixelSizeControl.style.display = 
+                this._currentMode === 'none' ? 'none' : 'flex';
             this._notifyObservers();
         });
+
+        // Only add the event listener if the element exists
+        const blockSizeSlider = document.getElementById('block-size-slider');
+        if (blockSizeSlider) {
+            blockSizeSlider.addEventListener('input', (e) => {
+                this.currentBlockSize = parseInt(e.target.value);
+                document.getElementById('block-size-value').textContent = 
+                    `${this.currentBlockSize}px`;
+                // Notify observers instead of directly calling processFrame
+                this._notifyObservers();
+            });
+        }
     },
 
     _notifyObservers() {
@@ -65,32 +83,56 @@ const DitherManager = {
     _floydSteinbergDither(data, width, height) {
         const newData = new Uint8ClampedArray(data);
         const threshold = 128;
-        const levels = 4; // Number of quantization levels
+        const levels = 4;
+        const blockSize = this.currentBlockSize;
 
-        for (let y = 0; y < height; y++) {
-            for (let x = 0; x < width; x++) {
-                const idx = (y * width + x) * 4;
+        // Process in blocks
+        for (let y = 0; y < height; y += blockSize) {
+            for (let x = 0; x < width; x += blockSize) {
+                // Calculate block average
+                let blockSum = [0, 0, 0];
+                let pixelCount = 0;
                 
-                // Process each color channel
+                for (let by = 0; by < blockSize && y + by < height; by++) {
+                    for (let bx = 0; bx < blockSize && x + bx < width; bx++) {
+                        const idx = ((y + by) * width + (x + bx)) * 4;
+                        for (let c = 0; c < 3; c++) {
+                            blockSum[c] += newData[idx + c];
+                        }
+                        pixelCount++;
+                    }
+                }
+
+                // Calculate block average
+                const blockAvg = blockSum.map(sum => sum / pixelCount);
+                
+                // Apply dithering to the block average
                 for (let c = 0; c < 3; c++) {
-                    const oldPixel = newData[idx + c];
+                    const oldPixel = blockAvg[c];
                     const quantizedPixel = this._quantize(oldPixel, levels);
                     const newPixel = quantizedPixel > threshold ? 255 : 0;
                     const error = oldPixel - newPixel;
-                    
-                    newData[idx + c] = newPixel;
-                    
-                    // Distribute error to neighboring pixels
-                    if (x + 1 < width) {
-                        newData[idx + 4 + c] += error * 7 / 16; // right
+
+                    // Fill the entire block with the new color
+                    for (let by = 0; by < blockSize && y + by < height; by++) {
+                        for (let bx = 0; bx < blockSize && x + bx < width; bx++) {
+                            const idx = ((y + by) * width + (x + bx)) * 4;
+                            newData[idx + c] = newPixel;
+                        }
                     }
-                    if (y + 1 === height) continue;
-                    if (x > 0) {
-                        newData[idx + width * 4 - 4 + c] += error * 3 / 16; // bottom left
+
+                    // Distribute error to neighboring blocks
+                    if (x + blockSize < width) {
+                        this._distributeBlockError(newData, x + blockSize, y, c, error * 7/16, width, blockSize, height);
                     }
-                    newData[idx + width * 4 + c] += error * 5 / 16; // bottom
-                    if (x + 1 < width) {
-                        newData[idx + width * 4 + 4 + c] += error * 1 / 16; // bottom right
+                    if (y + blockSize < height) {
+                        if (x - blockSize >= 0) {
+                            this._distributeBlockError(newData, x - blockSize, y + blockSize, c, error * 3/16, width, blockSize, height);
+                        }
+                        this._distributeBlockError(newData, x, y + blockSize, c, error * 5/16, width, blockSize, height);
+                        if (x + blockSize < width) {
+                            this._distributeBlockError(newData, x + blockSize, y + blockSize, c, error * 1/16, width, blockSize, height);
+                        }
                     }
                 }
             }
@@ -99,6 +141,16 @@ const DitherManager = {
         return new ImageData(newData, width, height);
     },
 
+    // Helper method to distribute error to blocks
+    _distributeBlockError(data, x, y, channel, error, width, blockSize, height) {
+        for (let by = 0; by < blockSize && y + by < height; by++) {
+            for (let bx = 0; bx < blockSize && x + bx < width; bx++) {
+                const idx = ((y + by) * width + (x + bx)) * 4;
+                data[idx + channel] = Math.max(0, Math.min(255, data[idx + channel] + error));
+            }
+        }
+    },
+
     // Ordered dithering (Bayer matrix)
     _orderedDither(data, width, height) {
         const newData = new Uint8ClampedArray(data);
@@ -175,4 +227,179 @@ const DitherManager = {
 
         return new ImageData(newData, width, height);
     }
-}; 
\ No newline at end of file
+};
+
+// Bayer dithering implementation using 8x8 Bayer matrix
+// Pattern: https://en.wikipedia.org/wiki/Ordered_dithering
+function bayerDither(imageData, width, blockSize) {
+    const data = new Uint8ClampedArray(imageData.data);
+    const height = imageData.data.length / 4 / width;
+    
+    // 8x8 Bayer matrix normalized to 0-1 range
+    const bayerMatrix = [
+        [ 0, 48, 12, 60,  3, 51, 15, 63],
+        [32, 16, 44, 28, 35, 19, 47, 31],
+        [ 8, 56,  4, 52, 11, 59,  7, 55],
+        [40, 24, 36, 20, 43, 27, 39, 23],
+        [ 2, 50, 14, 62,  1, 49, 13, 61],
+        [34, 18, 46, 30, 33, 17, 45, 29],
+        [10, 58,  6, 54,  9, 57,  5, 53],
+        [42, 26, 38, 22, 41, 25, 37, 21]
+    ].map(row => row.map(x => x / 64)); // Normalize to 0-1
+
+    for (let y = 0; y < height; y++) {
+        for (let x = 0; x < width; x++) {
+            const i = (y * width + x) * 4;
+            
+            // Get threshold from Bayer matrix (wrapping around with modulo)
+            const threshold = bayerMatrix[y % 8][x % 8];
+            
+            // Apply threshold to each color channel
+            for (let c = 0; c < 3; c++) {
+                const normalizedPixel = data[i + c] / 255;
+                data[i + c] = normalizedPixel > threshold ? 255 : 0;
+            }
+        }
+    }
+    
+    return data;
+}
+
+function applyDithering(imageData, width) {
+    const method = document.getElementById('dither-select').value;
+    const blockSize = DitherManager.currentBlockSize;
+    
+    switch (method) {
+        case 'floyd-steinberg':
+            return DitherManager._floydSteinbergDither(imageData.data, width, imageData.height);
+        case 'bayer':
+            return bayerDither(imageData, width);
+        case 'atkinson':
+            return DitherManager._atkinsonDither(imageData.data, width, imageData.height);
+        case 'ordered':
+            return DitherManager._orderedDither(imageData.data, width, imageData.height);
+        default:
+            return new Uint8ClampedArray(imageData.data);
+    }
+}
+
+function floydSteinbergDither(imageData, width, blockSize) {
+    const { data } = imageData;
+    const height = imageData.data.length / 4 / width;
+
+    const newData = new Uint8ClampedArray(data);
+    const threshold = 128;
+    const levels = 4; // Number of quantization levels
+
+    for (let y = 0; y < height; y++) {
+        for (let x = 0; x < width; x++) {
+            const idx = (y * width + x) * 4;
+            
+            // Process each color channel
+            for (let c = 0; c < 3; c++) {
+                const oldPixel = data[idx + c];
+                const quantizedPixel = DitherManager._quantize(oldPixel, levels);
+                const newPixel = quantizedPixel > threshold ? 255 : 0;
+                const error = oldPixel - newPixel;
+                
+                newData[idx + c] = newPixel;
+                
+                // Distribute error to neighboring pixels
+                if (x + 1 < width) {
+                    newData[idx + 4 + c] += error * 7 / 16; // right
+                }
+                if (y + 1 === height) continue;
+                if (x > 0) {
+                    newData[idx + width * 4 - 4 + c] += error * 3 / 16; // bottom left
+                }
+                newData[idx + width * 4 + c] += error * 5 / 16; // bottom
+                if (x + 1 < width) {
+                    newData[idx + width * 4 + 4 + c] += error * 1 / 16; // bottom right
+                }
+            }
+        }
+    }
+
+    return new ImageData(newData, width, height);
+}
+
+function atkinsonDither(imageData, width, blockSize) {
+    const { data } = imageData;
+    const height = imageData.data.length / 4 / width;
+
+    const newData = new Uint8ClampedArray(data);
+    const threshold = 128;
+    const levels = 4; // Number of quantization levels
+
+    for (let y = 0; y < height; y++) {
+        for (let x = 0; x < width; x++) {
+            const idx = (y * width + x) * 4;
+            
+            // Process each color channel
+            for (let c = 0; c < 3; c++) {
+                const oldPixel = data[idx + c];
+                const quantizedPixel = DitherManager._quantize(oldPixel, levels);
+                const newPixel = quantizedPixel > threshold ? 255 : 0;
+                const error = oldPixel - newPixel;
+                
+                newData[idx + c] = newPixel;
+                
+                // Distribute error to neighboring pixels (Atkinson's algorithm)
+                if (x + 1 < width) {
+                    newData[idx + 4 + c] += error / 8; // right
+                }
+                if (x + 2 < width) {
+                    newData[idx + 8 + c] += error / 8; // right + 1
+                }
+                if (y + 1 === height) continue;
+                if (x > 0) {
+                    newData[idx + width * 4 - 4 + c] += error / 8; // bottom left
+                }
+                newData[idx + width * 4 + c] += error / 8; // bottom
+                if (x + 1 < width) {
+                    newData[idx + width * 4 + 4 + c] += error / 8; // bottom right
+                }
+                if (y + 2 === height) continue;
+                if (x + 1 < width) {
+                    newData[idx + width * 8 + 4 + c] += error / 8; // bottom + 1 right
+                }
+            }
+        }
+    }
+
+    return new ImageData(newData, width, height);
+}
+
+function orderedDither(imageData, width, blockSize) {
+    const { data } = imageData;
+    const height = imageData.data.length / 4 / width;
+    
+    const newData = new Uint8ClampedArray(data);
+    const matrix = [
+        [0, 8, 2, 10],
+        [12, 4, 14, 6],
+        [3, 11, 1, 9],
+        [15, 7, 13, 5]
+    ];
+    const matrixSize = 4;
+    const threshold = 128;
+    const levels = 4; // Number of quantization levels
+
+    for (let y = 0; y < height; y++) {
+        for (let x = 0; x < width; x++) {
+            const idx = (y * width + x) * 4;
+            const matrixX = x % matrixSize;
+            const matrixY = y % matrixSize;
+            const matrixValue = matrix[matrixY][matrixX] * 16;
+
+            // Process each color channel
+            for (let c = 0; c < 3; c++) {
+                const pixel = data[idx + c];
+                const quantizedPixel = DitherManager._quantize(pixel, levels);
+                newData[idx + c] = quantizedPixel + matrixValue > threshold ? 255 : 0;
+            }
+        }
+    }
+
+    return new ImageData(newData, width, height);
+} 
\ No newline at end of file
diff --git a/js/leibovitz/index.html b/js/leibovitz/index.html
index 8af8f4f..acc592a 100644
--- a/js/leibovitz/index.html
+++ b/js/leibovitz/index.html
@@ -64,6 +64,7 @@
             padding: 10px;
             border-radius: 8px;
             box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+            min-width: 200px;
         }
         .top-controls {
             display: flex;
@@ -130,6 +131,11 @@
             width: 100%;
         }
 
+        #block-size-value {
+            font-size: 12px;
+            color: #666;
+        }
+
     </style>
 </head>
 <body>
@@ -143,6 +149,7 @@
             <option value="floyd-steinberg">Floyd-Steinberg</option>
             <option value="ordered">Ordered</option>
             <option value="atkinson">Atkinson</option>
+            <option value="bayer">Bayer</option>
         </select>
         <div class="color-controls">
             <input type="color" id="color-tint" title="Color Tint">
@@ -153,6 +160,11 @@
         <label for="contrast-slider">Contrast</label>
         <input type="range" id="contrast-slider" min="-255" max="255" value="0" step="1">
     </div>
+    <div class="contrast-control" id="pixel-size-control" style="display: none;">
+        <label for="block-size-slider">Pixel Size</label>
+        <input type="range" id="block-size-slider" min="1" max="12" value="4" step="1">
+        <span id="block-size-value">4px</span>
+    </div>
 </div>
 
 <div id="controls">