// 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, _pixelSizeControl: null, currentBlockSize: 4, // Initialize the dither manager init() { this._setupEventListeners(); this._pixelSizeControl = document.getElementById('pixel-size-control'); }, // Private methods _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 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)); }, // 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); case 'bayer': return this._bayerDither(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; 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 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); }, // 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); 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); }, // Atkinson dithering with block-based processing _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); // right if (x + blockSize * 2 < width) { this._distributeBlockError(newData, x + blockSize * 2, y, c, error / 8, width, blockSize, height); // right + 1 } } if (y + blockSize < height) { if (x - blockSize >= 0) { this._distributeBlockError(newData, x - blockSize, y + blockSize, c, error / 8, width, blockSize, height); // bottom left } this._distributeBlockError(newData, x, y + blockSize, c, error / 8, width, blockSize, height); // bottom if (x + blockSize < width) { this._distributeBlockError(newData, x + blockSize, y + blockSize, c, error / 8, width, blockSize, height); // bottom right } } if (y + blockSize * 2 < height && x + blockSize < width) { this._distributeBlockError(newData, x + blockSize, y + blockSize * 2, c, error / 8, width, blockSize, height); // bottom + 1 } } } } return new ImageData(newData, width, height); }, // Add this as a method in DitherManager _bayerDither(data, width, height) { const newData = new Uint8ClampedArray(data); const blockSize = this.currentBlockSize; // 4x4 Bayer matrix (simpler and more visible pattern than 8x8) const bayerMatrix = [ [ 0, 8, 2, 10], [12, 4, 14, 6 ], [ 3, 11, 1, 9 ], [15, 7, 13, 5 ] ]; // Scale factor to make the pattern more pronounced const scaleFactor = 16; // Increase this value to make pattern more visible // 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++) { // Normalize pixel value and apply threshold 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); } }; // Update the Bayer dithering to use blocks 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; // 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); }