about summary refs log tree commit diff stats
path: root/js/leibovitz/dither.js
diff options
context:
space:
mode:
authorelioat <elioat@tilde.institute>2025-03-29 22:41:49 -0400
committerelioat <elioat@tilde.institute>2025-03-29 22:41:49 -0400
commit5eeed0ebe09bc27e0cb4c1e5d71d5fad6946fa50 (patch)
tree49a7fff32626f6611b1beed826bc3b77e06c278b /js/leibovitz/dither.js
parent0a82d97f1c97439cdc778fe4daeec5bcef42bbda (diff)
downloadtour-5eeed0ebe09bc27e0cb4c1e5d71d5fad6946fa50.tar.gz
*
Diffstat (limited to 'js/leibovitz/dither.js')
-rw-r--r--js/leibovitz/dither.js178
1 files changed, 178 insertions, 0 deletions
diff --git a/js/leibovitz/dither.js b/js/leibovitz/dither.js
new file mode 100644
index 0000000..2a02834
--- /dev/null
+++ b/js/leibovitz/dither.js
@@ -0,0 +1,178 @@
+// Dithering management module
+// Uses the Observer pattern to notify the main camera module of dithering changes
+
+const DitherManager = {
+    // Private state
+    _currentMode: 'none',
+    _observers: new Set(),
+    _modeSelect: null,
+
+    // Initialize the dither manager
+    init() {
+        this._setupEventListeners();
+    },
+
+    // Private methods
+    _setupEventListeners() {
+        this._modeSelect = document.getElementById('dither-select');
+        this._modeSelect.addEventListener('change', (e) => {
+            this._currentMode = e.target.value;
+            this._notifyObservers();
+        });
+    },
+
+    _notifyObservers() {
+        this._observers.forEach(observer => observer(this._currentMode));
+    },
+
+    // Public methods
+    subscribe(observer) {
+        this._observers.add(observer);
+        return () => this._observers.delete(observer);
+    },
+
+    getCurrentMode() {
+        return this._currentMode;
+    },
+
+    // Apply dithering to an image
+    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);
+            default:
+                return imageData;
+        }
+    },
+
+    // Quantize a value to create chunkier output
+    _quantize(value, levels = 4) {
+        const step = 255 / (levels - 1);
+        return Math.round(value / step) * step;
+    },
+
+    // Floyd-Steinberg dithering
+    _floydSteinbergDither(data, width, height) {
+        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 = newData[idx + 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
+                    }
+                    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);
+    },
+
+    // Ordered dithering (Bayer matrix)
+    _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; // 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 = newData[idx + c];
+                    const quantizedPixel = this._quantize(pixel, levels);
+                    newData[idx + c] = quantizedPixel + matrixValue > threshold ? 255 : 0;
+                }
+            }
+        }
+
+        return new ImageData(newData, width, height);
+    },
+
+    // Atkinson dithering
+    _atkinsonDither(data, width, height) {
+        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 = newData[idx + 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 (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);
+    }
+}; 
\ No newline at end of file