diff options
author | elioat <elioat@tilde.institute> | 2025-03-29 23:02:17 -0400 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2025-03-29 23:02:17 -0400 |
commit | f185a42a9f1f9431e6208bed6516737d92efefbd (patch) | |
tree | d853fa92f79d5211f5381eed36458aed65a5da6d | |
parent | 771835d9e10686e0a89712af5cd1c3825c1cfc39 (diff) | |
download | tour-f185a42a9f1f9431e6208bed6516737d92efefbd.tar.gz |
*
-rw-r--r-- | js/leibovitz/dither.js | 169 |
1 files changed, 122 insertions, 47 deletions
diff --git a/js/leibovitz/dither.js b/js/leibovitz/dither.js index 90c4ae2..9e1ffb1 100644 --- a/js/leibovitz/dither.js +++ b/js/leibovitz/dither.js @@ -162,20 +162,46 @@ const DitherManager = { ]; const matrixSize = 4; const threshold = 128; - const levels = 4; // Number of quantization levels + const levels = 4; + const blockSize = this.currentBlockSize; - 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; + // 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; - // Process each color channel + // Apply dithering to the block 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; + 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; + } + } } } } @@ -183,43 +209,66 @@ const DitherManager = { return new ImageData(newData, width, height); }, - // Atkinson dithering + // Atkinson dithering with block-based processing _atkinsonDither(data, width, height) { const newData = new Uint8ClampedArray(data); const threshold = 128; - const levels = 4; // Number of quantization levels + 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++; + } + } - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const idx = (y * width + x) * 4; + // Calculate block average + const blockAvg = blockSum.map(sum => sum / pixelCount); - // Process each color channel + // Apply dithering to the block average for (let c = 0; c < 3; c++) { - const oldPixel = newData[idx + c]; + const oldPixel = blockAvg[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 + + // 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; + } } - if (y + 1 === height) continue; - if (x > 0) { - newData[idx + width * 4 - 4 + c] += error / 8; // bottom left + + // 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 + } } - newData[idx + width * 4 + c] += error / 8; // bottom - if (x + 1 < width) { - newData[idx + width * 4 + 4 + c] += error / 8; // bottom right + 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 + 2 === height) continue; - if (x + 1 < width) { - newData[idx + width * 8 + 4 + c] += error / 8; // bottom + 1 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 } } } @@ -229,11 +278,11 @@ const DitherManager = { } }; -// Bayer dithering implementation using 8x8 Bayer matrix -// Pattern: https://en.wikipedia.org/wiki/Ordered_dithering -function bayerDither(imageData, width, blockSize) { +// 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 = [ @@ -245,19 +294,45 @@ function bayerDither(imageData, width, blockSize) { [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)); // Normalize to 0-1 + ].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++; + } + } - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const i = (y * width + x) * 4; + // Calculate block average + const blockAvg = blockSum.map(sum => sum / pixelCount); - // Get threshold from Bayer matrix (wrapping around with modulo) - const threshold = bayerMatrix[y % 8][x % 8]; + // 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 each color channel + // Apply threshold to the block for (let c = 0; c < 3; c++) { - const normalizedPixel = data[i + c] / 255; - data[i + c] = normalizedPixel > threshold ? 255 : 0; + 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; + } + } } } } |