about summary refs log tree commit diff stats
path: root/js/leibovitz/dither.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/leibovitz/dither.js')
-rw-r--r--js/leibovitz/dither.js613
1 files changed, 613 insertions, 0 deletions
diff --git a/js/leibovitz/dither.js b/js/leibovitz/dither.js
new file mode 100644
index 0000000..e74f1be
--- /dev/null
+++ b/js/leibovitz/dither.js
@@ -0,0 +1,613 @@
+/**
+ * Dithering management module implementing multiple dithering algorithms.
+ * 
+ * Implements a couple dithering algorithms with block-based processing.
+ * Block-based processing is faster, and has better performance.
+ * Supports multiple dithering patterns with configurable block sizes.
+ * 
+ * Implements the following design patterns:
+ * - Observer Pattern: state management and effect application
+ * - Factory Pattern: UI initialization
+ * - Strategy Pattern: dithering algorithm selection
+ * - Command Pattern: state reset operations
+ * 
+ * Supported dithering algorithms:
+ * - Floyd-Steinberg: Error diffusion with standard distribution pattern
+ * - Ordered: Matrix-based threshold dithering
+ * - Atkinson: Error diffusion with 1/8 error distribution
+ * - Bayer: Pattern-based threshold dithering
+ * 
+ * Each color channel (Red, Green, Blue) has 4 possible values:
+ * - 0   -> Black/None
+ * - 85  -> Low
+ * - 170 -> Medium
+ * - 255 -> Full
+ * 
+ * Features:
+ * - Block-based processing for performance
+ * - Multiple dithering algorithms
+ * - Configurable block sizes
+ * - Error diffusion patterns
+ */
+
+const DitherManager = {
+    // Private state
+    _currentMode: 'none',
+    _observers: new Set(),
+    _modeSelect: null,
+    _pixelSizeControl: null,
+    currentBlockSize: 4,
+
+    /**
+     * Initializes the dither manager and sets up UI controls
+     */
+    init() {
+        this._setupEventListeners();
+        this._pixelSizeControl = document.getElementById('pixel-size-control');
+    },
+
+    _setupEventListeners() {
+        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 actually 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() {
+        this._observers.forEach(observer => observer(this._currentMode));
+    },
+
+    /**
+     * Subscribes to dithering state changes
+     * @param {Function} observer - Callback function for state changes
+     * @returns {Function} Unsubscribe function
+     */
+    subscribe(observer) {
+        this._observers.add(observer);
+        return () => this._observers.delete(observer);
+    },
+
+    getCurrentMode() {
+        return this._currentMode;
+    },
+
+    /**
+     * Applies selected dithering algorithm to image data
+     * @param {ImageData} imageData - Source image data
+     * @param {string} mode - Selected dithering algorithm
+     * @returns {ImageData} Processed image data
+     */
+    applyDither(imageData, mode) {
+        if (!mode || mode === 'none') return imageData;
+
+        const { data } = imageData;
+        const width = imageData.width;
+        const height = imageData.height;
+
+        switch (mode) {
+            case 'floyd-steinberg':
+                return this._floydSteinbergDither(data, width, height);
+            case 'ordered':
+                return this._orderedDither(data, width, height);
+            case 'atkinson':
+                return this._atkinsonDither(data, width, height);
+            case 'bayer':
+                return this._bayerDither(data, width, height);
+            default:
+                return imageData;
+        }
+    },
+
+    /**
+     * Quantizes a value to create chunkier output
+     * @param {number} value - Input value
+     * @param {number} levels - Number of quantization levels
+     * @returns {number} Quantized value
+     */
+    _quantize(value, levels = 4) {
+        const step = 255 / (levels - 1);
+        return Math.round(value / step) * step;
+    },
+
+    /**
+     * Applies Floyd-Steinberg dithering algorithm
+     * Uses a 4x4 error distribution pattern for smoother results
+     * @param {Uint8ClampedArray} data - Image data
+     * @param {number} width - Image width
+     * @param {number} height - Image height
+     * @returns {ImageData} Dithered image data
+     */
+    _floydSteinbergDither(data, width, height) {
+        const newData = new Uint8ClampedArray(data);
+        const threshold = 128;
+        const levels = 4;
+        const blockSize = this.currentBlockSize;
+
+        // Process in blocks, block by block
+        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;
+                
+                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 = blockAvg[c];
+                    const quantizedPixel = this._quantize(oldPixel, levels);
+                    const newPixel = quantizedPixel > threshold ? 255 : 0;
+                    const error = oldPixel - newPixel;
+
+                    // 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;
+                        }
+                    }
+
+                    // Distribute error to neighboring blocks
+                    if (x + blockSize < width) {
+                        this._distributeBlockError(newData, x + blockSize, y, c, error * 7/16, width, blockSize, height);
+                    }
+                    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);
+                        }
+                    }
+                }
+            }
+        }
+
+        return new ImageData(newData, width, height);
+    },
+
+    /**
+     * Distributes error to neighboring blocks
+     * @param {Uint8ClampedArray} data - Image data
+     * @param {number} x - X coordinate
+     * @param {number} y - Y coordinate
+     * @param {number} channel - Color channel
+     * @param {number} error - Error value to distribute
+     * @param {number} width - Image width
+     * @param {number} blockSize - Size of processing blocks
+     * @param {number} height - Image height
+     */
+    _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));
+            }
+        }
+    },
+
+    /**
+     * Applies ordered dithering using a Bayer matrix
+     * And implements threshold-based dithering with block processing
+     * Also uses a 4x4 Bayer matrix pattern for structured dithering
+     * @param {Uint8ClampedArray} data - Image data
+     * @param {number} width - Image width
+     * @param {number} height - Image height
+     * @returns {ImageData} Dithered image data
+     */
+    _orderedDither(data, width, height) {
+        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;
+        const blockSize = this.currentBlockSize;
+
+        // 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;
+                
+                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);
+                
+                // Get matrix value for this block
+                const matrixX = Math.floor(x / blockSize) % matrixSize;
+                const matrixY = Math.floor(y / blockSize) % matrixSize;
+                const matrixValue = matrix[matrixY][matrixX] * 16;
+
+                // Apply dithering to the block
+                for (let c = 0; c < 3; c++) {
+                    const quantizedPixel = this._quantize(blockAvg[c], levels);
+                    const newPixel = quantizedPixel + matrixValue > threshold ? 255 : 0;
+
+                    // 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;
+                        }
+                    }
+                }
+            }
+        }
+
+        return new ImageData(newData, width, height);
+    },
+
+    /**
+     * Applies Atkinson dithering algorithm
+     * @param {Uint8ClampedArray} data - Image data
+     * @param {number} width - Image width
+     * @param {number} height - Image height
+     * @returns {ImageData} Dithered image data
+     */
+    _atkinsonDither(data, width, height) {
+        const newData = new Uint8ClampedArray(data);
+        const threshold = 128;
+        const levels = 4;
+        const blockSize = this.currentBlockSize;
+
+        // 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;
+                
+                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 = blockAvg[c];
+                    const quantizedPixel = this._quantize(oldPixel, levels);
+                    const newPixel = quantizedPixel > threshold ? 255 : 0;
+                    const error = oldPixel - newPixel;
+
+                    // 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;
+                        }
+                    }
+
+                    // Distribute error to neighboring blocks (Atkinson pattern)
+                    if (x + blockSize < width) {
+                        this._distributeBlockError(newData, x + blockSize, y, c, error / 8, width, blockSize, height);
+                        if (x + blockSize * 2 < width) {
+                            this._distributeBlockError(newData, x + blockSize * 2, y, c, error / 8, width, blockSize, height);
+                        }
+                    }
+                    if (y + blockSize < height) {
+                        if (x - blockSize >= 0) {
+                            this._distributeBlockError(newData, x - blockSize, y + blockSize, c, error / 8, width, blockSize, height);
+                        }
+                        this._distributeBlockError(newData, x, y + blockSize, c, error / 8, width, blockSize, height);
+                        if (x + blockSize < width) {
+                            this._distributeBlockError(newData, x + blockSize, y + blockSize, c, error / 8, width, blockSize, height);
+                        }
+                    }
+                    if (y + blockSize * 2 < height && x + blockSize < width) {
+                        this._distributeBlockError(newData, x + blockSize, y + blockSize * 2, c, error / 8, width, blockSize, height);
+                    }
+                }
+            }
+        }
+
+        return new ImageData(newData, width, height);
+    },
+
+    /**
+     * Applies Bayer dithering algorithm
+     * @param {Uint8ClampedArray} data - Image data
+     * @param {number} width - Image width
+     * @param {number} height - Image height
+     * @returns {ImageData} Dithered image data
+     */
+    _bayerDither(data, width, height) {
+        const newData = new Uint8ClampedArray(data);
+        const blockSize = this.currentBlockSize;
+        
+        const bayerMatrix = [
+            [ 0, 8, 2, 10],
+            [12, 4, 14, 6 ],
+            [ 3, 11, 1, 9 ],
+            [15, 7, 13, 5 ]
+        ];
+        
+        const scaleFactor = 16;
+        
+        // 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;
+                
+                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);
+                
+                // Get matrix value for this block position
+                const matrixX = Math.floor(x / blockSize) % 4;
+                const matrixY = Math.floor(y / blockSize) % 4;
+                const threshold = (bayerMatrix[matrixY][matrixX] * scaleFactor);
+                
+                // Apply dithering to the block
+                for (let c = 0; c < 3; c++) {
+                    const normalizedPixel = blockAvg[c];
+                    const newPixel = normalizedPixel > threshold ? 255 : 0;
+                    
+                    // 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;
+                        }
+                    }
+                }
+            }
+        }
+
+        // Preserve alpha channel
+        for (let i = 3; i < newData.length; i += 4) {
+            newData[i] = data[i];
+        }
+        
+        return new ImageData(newData, width, height);
+    }
+};
+
+// Legacy functions for backward compatibility
+function bayerDither(imageData, width) {
+    const data = new Uint8ClampedArray(imageData.data);
+    const height = imageData.data.length / 4 / width;
+    const blockSize = DitherManager.currentBlockSize;
+    
+    // 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));
+
+    // 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;
+            
+            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] += data[idx + c];
+                    }
+                    pixelCount++;
+                }
+            }
+
+            // Calculate block average
+            const blockAvg = blockSum.map(sum => sum / pixelCount);
+            
+            // Get threshold from Bayer matrix for this block
+            const matrixX = Math.floor(x / blockSize) % 8;
+            const matrixY = Math.floor(y / blockSize) % 8;
+            const threshold = bayerMatrix[matrixY][matrixX];
+            
+            // Apply threshold to the block
+            for (let c = 0; c < 3; c++) {
+                const normalizedPixel = blockAvg[c] / 255;
+                const newPixel = normalizedPixel > threshold ? 255 : 0;
+                
+                // 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;
+                        data[idx + c] = newPixel;
+                    }
+                }
+            }
+        }
+    }
+    
+    // Preserve alpha channel
+    for (let i = 3; i < data.length; i += 4) {
+        data[i] = imageData.data[i];
+    }
+    
+    return data;
+}
+
+function applyDithering(imageData, width) {
+    const method = document.getElementById('dither-select').value;
+    return DitherManager.applyDither(imageData, method);
+}
+
+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;
+
+    for (let y = 0; y < height; y++) {
+        for (let x = 0; x < width; x++) {
+            const idx = (y * width + x) * 4;
+            
+            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;
+                
+                if (x + 1 < width) {
+                    newData[idx + 4 + c] += error * 7 / 16;
+                }
+                if (y + 1 === height) continue;
+                if (x > 0) {
+                    newData[idx + width * 4 - 4 + c] += error * 3 / 16;
+                }
+                newData[idx + width * 4 + c] += error * 5 / 16;
+                if (x + 1 < width) {
+                    newData[idx + width * 4 + 4 + c] += error * 1 / 16;
+                }
+            }
+        }
+    }
+
+    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;
+
+    for (let y = 0; y < height; y++) {
+        for (let x = 0; x < width; x++) {
+            const idx = (y * width + x) * 4;
+            
+            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;
+                
+                if (x + 1 < width) {
+                    newData[idx + 4 + c] += error / 8;
+                }
+                if (x + 2 < width) {
+                    newData[idx + 8 + c] += error / 8;
+                }
+                if (y + 1 === height) continue;
+                if (x > 0) {
+                    newData[idx + width * 4 - 4 + c] += error / 8;
+                }
+                newData[idx + width * 4 + c] += error / 8;
+                if (x + 1 < width) {
+                    newData[idx + width * 4 + 4 + c] += error / 8;
+                }
+                if (y + 2 === height) continue;
+                if (x + 1 < width) {
+                    newData[idx + width * 8 + 4 + c] += error / 8;
+                }
+            }
+        }
+    }
+
+    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;
+
+    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;
+
+            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