Dithering Playing with implementations of the various ways to do this. Note about technique: I load the image data once, and then modify it a bunch of times. To avoid just mutably modifying the same image data over and over again, I clone it, like this: let clone = new ImageData(new Uint8ClampedArray(data.data.data), data.width, data.height);
{ let ctx = DOM.canvas(180 * 4 * 2, 215 + 20).getContext('2d'); ctx.canvas.style.imageRendering = 'pixelated'; ctx.font = '12px monospace'; ctx.fillText('Threshold', 5, 15); ctx.drawImage(thresholdDither, 0, 20); ctx.translate(180, 0); ctx.fillText('Random', 5, 15); ctx.drawImage(randomDither, 0, 20); ctx.translate(180, 0); ctx.fillText('Dumb Error', 5, 15); ctx.drawImage(dumbError, 0, 20); ctx.translate(180, 0); ctx.fillText('Floyd Steinberg', 5, 15); ctx.drawImage(floydSteinberg, 0, 20); return ctx.canvas; }
Threshold Slide the slider to adjust the... threshold.
viewof threshold = DOM.range(0, 255 * 3)
thresholdDither = { let ctx = DOM.canvas(data.width, data.height).getContext('2d'); ctx.canvas.style.width = `${data.width * 2}px`; ctx.canvas.style.imageRendering = 'pixelated'; let clone = new ImageData(new Uint8ClampedArray(data.data.data), data.width, data.height); for (let x = 0; x < clone.data.length; x += 4) clone.data[x] = clone.data[x + 1] = clone.data[x + 2] = (clone.data[x] + clone.data[x + 1] + clone.data[x + 2]) > threshold ? 255 : 0; ctx.putImageData(clone, 0, 0); return ctx.canvas; }
Random dithering Popular in the 50s, reportedly. No longer popular for pretty clear reasons.
randomDither = { let ctx = DOM.canvas(data.width, data.height).getContext('2d'); ctx.canvas.style.width = `${data.width * 2}px`; ctx.canvas.style.imageRendering = 'pixelated'; let clone = new ImageData(new Uint8ClampedArray(data.data.data), data.width, data.height); for (let x = 0; x < clone.data.length; x += 4) clone.data[x] = clone.data[x + 1] = clone.data[x + 2] = (clone.data[x] + clone.data[x + 1] + clone.data[x + 2]) > (Math.random() * 255 * 3) ? 255 : 0; ctx.putImageData(clone, 0, 0); return ctx.canvas; }
Looks neat if you do it constantly, though.
{ now; let ctx = DOM.canvas(data.width, data.height).getContext('2d'); ctx.canvas.style.width = `${data.width * 2}px`; ctx.canvas.style.imageRendering = 'pixelated'; let clone = new ImageData(new Uint8ClampedArray(data.data.data), data.width, data.height); for (let x = 0; x < clone.data.length; x += 4) clone.data[x] = clone.data[x + 1] = clone.data[x + 2] = (clone.data[x] + clone.data[x + 1] + clone.data[x + 2]) > (Math.random() * 255 * 3) ? 255 : 0; ctx.putImageData(clone, 0, 0); return ctx.canvas; }
Dumb error diffusion Really not very practical, but kind of a good learning example. Cribbed from Image Dithering (the rest of these are written from the idea).
dumbError = { let ctx = DOM.canvas(data.width, data.height).getContext('2d'); ctx.canvas.style.width = `${data.width * 2}px`; ctx.canvas.style.imageRendering = 'pixelated'; let clone = new ImageData(new Uint8ClampedArray(data.data.data), data.width, data.height); function px(x, y) { return (x * 4) + (y * data.width * 4); } for (let y = 0; y < data.height; y++) { let error = 0; for (let x = 0; x < data.width; x++) { let oldPixel = clone.data[px(x, y)] + error; let newPixel = oldPixel > 125 ? 255 : 0; // how far off was this guess? add it to the next calculation. error= oldPixel - newPixel; clone.data[px(x, y)] = clone.data[px(x, y) + 1] = clone.data[px(x, y) + 2] = newPixel; } } ctx.putImageData(clone, 0, 0); yield ctx.canvas; }
Dumb errors on a different axis How about that from the top down? Kind of neat.
dumbErrorY = { let ctx = DOM.canvas(data.width, data.height).getContext('2d'); ctx.canvas.style.width = `${data.width * 2}px`; ctx.canvas.style.imageRendering = 'pixelated'; let clone = new ImageData(new Uint8ClampedArray(data.data.data), data.width, data.height); function px(x, y) { return (x * 4) + (y * data.width * 4); } for (let x = 0; x < data.width; x++) { let error = 0; for (let y = 0; y < data.height; y++) { let oldPixel = clone.data[px(x, y)] + error; let newPixel = oldPixel > 125 ? 255 : 0; // how far off was this guess? add it to the next calculation. error= oldPixel - newPixel; clone.data[px(x, y)] = clone.data[px(x, y) + 1] = clone.data[px(x, y) + 2] = newPixel; } } ctx.putImageData(clone, 0, 0); yield ctx.canvas; }
Floyd-Steinberg A totally decent totally fine dithering algorithm.
floydSteinberg = { let ctx = DOM.canvas(data.width, data.height).getContext('2d'); ctx.canvas.style.width = `${data.width * 2}px`; ctx.canvas.style.imageRendering = 'pixelated'; let clone = new ImageData(new Uint8ClampedArray(data.data.data), data.width, data.height); function px(x, y) { return (x * 4) + (y * data.width * 4); } for (let y = 0; y < data.height; y++) { for (let x = 0; x < data.width; x++) { let oldPixel = clone.data[px(x, y)]; let newPixel = oldPixel > 125 ? 255 : 0; clone.data[px(x, y)] = clone.data[px(x, y) + 1] = clone.data[px(x, y) + 2] = newPixel; let quantError = oldPixel - newPixel; clone.data[px(x + 1, y )] = clone.data[px(x + 1, y ) + 1] = clone.data[px(x + 1, y ) + 2] = clone.data[px(x + 1, y )] + quantError * 7 / 16 clone.data[px(x - 1, y + 1)] = clone.data[px(x - 1, y + 1) + 1] = clone.data[px(x - 1, y + 1) + 2] = clone.data[px(x - 1, y + 1)] + quantError * 3 / 16 clone.data[px(x , y + 1)] = clone.data[px(x , y + 1) + 1] = clone.data[px(x , y + 1) + 2] = clone.data[px(x , y + 1)] + quantError * 5 / 16 clone.data[px(x + 1, y + 1)] = clone.data[px(x + 1, y + 1) + 1] = clone.data[px(x + 1, y + 1) + 2] = clone.data[px(x + 1, y + 1)] + quantError * 1 / 16 } } ctx.putImageData(clone, 0, 0); yield ctx.canvas; }
Customizable kernel Floyd-Steinberg dithering Tinkering around, really have some fun, don't dither.
viewof kernel = { let ui = html` <style> input[type=number] { width: 165px; padding:5px; font-size: 20px; } table { width: auto; border-collapse: collapse; } td { padding: 0; } </style> <table> <tr> <td><input type='number'></td> <td><input type='number'></td> </tr><tr> <td><input type='number'></td> <td><input type='number'></td> </tr></table>`; let inputs = ui.querySelectorAll('input'); function valid() { let sum = 0; for (let k of kernel) sum += k; console.log(sum); return sum === 16; } let kernel = [7, 3, 5, 1]; kernel.forEach((k, i) => { inputs[i].value = k; inputs[i].addEventListener('input', evt => { let change = kernel[i] - +evt.target.value; kernel[i] = +evt.target.value; for (let j = 0; j < 4; j++) { if (j !== i && (kernel[j] + change) > 0 && (kernel[j] + change) < 16) { kernel[j] += change; inputs[j].value = kernel[j]; break; } } ui.style.background = 'white'; ui.value = kernel; ui.dispatchEvent(new CustomEvent('change')); }); }); ui.value = kernel; ui.dispatchEvent(new CustomEvent('change')); return ui; }
floydDIY = { let ctx = DOM.canvas(data.width, data.height).getContext('2d'); ctx.canvas.style.width = `${data.width * 2}px`; ctx.canvas.style.imageRendering = 'pixelated'; let clone = new ImageData(new Uint8ClampedArray(data.data.data), data.width, data.height); function px(x, y) { return (x * 4) + (y * data.width * 4); } for (let y = 0; y < data.height; y++) { for (let x = 0; x < data.width; x++) { let oldPixel = clone.data[px(x, y)]; let newPixel = oldPixel > 125 ? 255 : 0; clone.data[px(x, y)] = clone.data[px(x, y) + 1] = clone.data[px(x, y) + 2] = newPixel; let quantError = oldPixel - newPixel; clone.data[px(x + 1, y )] = clone.data[px(x + 1, y ) + 1] = clone.data[px(x + 1, y ) + 2] = clone.data[px(x + 1, y )] + quantError * kernel[0] / 16 clone.data[px(x - 1, y + 1)] = clone.data[px(x - 1, y + 1) + 1] = clone.data[px(x - 1, y + 1) + 2] = clone.data[px(x - 1, y + 1)] + quantError * kernel[1] / 16 clone.data[px(x , y + 1)] = clone.data[px(x , y + 1) + 1] = clone.data[px(x , y + 1) + 2] = clone.data[px(x , y + 1)] + quantError * kernel[2] / 16 clone.data[px(x + 1, y + 1)] = clone.data[px(x + 1, y + 1) + 1] = clone.data[px(x + 1, y + 1) + 2] = clone.data[px(x + 1, y + 1)] + quantError * kernel[3] / 16 } } ctx.putImageData(clone, 0, 0); return ctx.canvas; }
data = { let img = new Image(); img.crossOrigin = '*'; img.src = 'https://file-hamhdaleym.now.sh/'; await new Promise(resolve => img.addEventListener('load', resolve)); let ctx = DOM.canvas(img.width, img.height).getContext('2d'); ctx.drawImage(img, 0, 0); return { data: ctx.getImageData(0, 0, img.width, img.height), width: img.width, height: img.height }; }