diff options
Diffstat (limited to 'js/leibovitz/dither.js')
-rw-r--r-- | js/leibovitz/dither.js | 613 |
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 |