diff options
author | elioat <elioat@tilde.institute> | 2025-03-29 22:41:49 -0400 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2025-03-29 22:41:49 -0400 |
commit | 5eeed0ebe09bc27e0cb4c1e5d71d5fad6946fa50 (patch) | |
tree | 49a7fff32626f6611b1beed826bc3b77e06c278b /js/leibovitz/dither.js | |
parent | 0a82d97f1c97439cdc778fe4daeec5bcef42bbda (diff) | |
download | tour-5eeed0ebe09bc27e0cb4c1e5d71d5fad6946fa50.tar.gz |
*
Diffstat (limited to 'js/leibovitz/dither.js')
-rw-r--r-- | js/leibovitz/dither.js | 178 |
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 |