# How to Fix Banding in Gradients

Photoshop’s gradient algorithm is quite disappointing. It is notorious for creating gradients with banding. Here is an example, attempting to create a gradient from `#222` to `#333`: Eeeww!

Can you see it? If you look closely, there are vertical “lines” in the image where the color changes. This is incredibly easy to fix algorithmically - but very difficult to fix any other way.

Gradients are basically linear interpolation algorithms:

``````function lerp(a, b, amount){
return a + (b - a) * amount;
}

function lerpColor(color1, color2, amount){
return {
r: Math.round(lerp(color1.r, color2.r, amount)),
g: Math.round(lerp(color1.g, color2.g, amount)),
b: Math.round(lerp(color1.b, color2.b, amount))
};
}
``````

Where does this fail?

Actually, the linear interpolation (`lerp`) is perfectly correct. It’s the `Math.round` where things go askew.

Most color channels are represented in 8-bits, which means they range from 0 to 255. This is good enough most of the time, and humans have a really hard time detecting the difference between colors that are 1 unit apart (i.e., `#445599` and `#445699`). However - for certain colors, under certain conditions, humans can actually see the difference. Gradients are one instance where humans can detect minute changes.

So it seems we are at a loss - how can we fix gradient banding if we actually need better hardware that supports more than 8-bits per channel? It turns out this problem was already solved, back when we had even worse hardware restrictions. How do you display a 24-bit image on a 8-bit display?

In fact, searching through Google’s results on how to fix banding results in a bunch of non-algorithmic attempts at faking dithering. Let’s settle this once and for all, and just perform true dithering on a higher-than-8-bit channel image in memory.

The following implementation is using roughly 64-bits per channel, and dithers back to 8-bit channels to display the target image.

``````var color1 = {
// color1 (default: #222222)
r: 0x22 / 0xFF,
g: 0x22 / 0xFF,
b: 0x22 / 0xFF
};
var color2 = {
// color2 (default: #333333)
r: 0x33 / 0xFF,
g: 0x33 / 0xFF,
b: 0x33 / 0xFF
};
// output image size (default: [320, 240])
var imageSize = [320, 240];
// enable or disable dithering (default: true)
var dithering = true;
// resolution of output colors (default: 256)
var maxVal = 256;

var cnv = document.createElement('canvas');
cnv.width = imageSize;
cnv.height = imageSize;
document.body.appendChild(cnv);
var ctx = cnv.getContext('2d');
var imd = ctx.createImageData(imageSize, imageSize);

var ditherError = [];
function getError(x, y, chan){
if (!dithering)
return 0;
var k = (x + y * imageSize) * 3 + chan;
if (typeof ditherError[k] == 'undefined')
return 0;
return ditherError[k];
}

var k = (x + y * imageSize) * 3 + chan;
ditherError[k] = getError(x, y, chan) + val;
}

function lerp(a, b, amount){
return a + (b - a) * amount;
}

function doLine(y){
for (var x = 0; x < imageSize; x++){
for (var chan = 0; chan < 3; chan++){
var amount = x / (imageSize - 1);
var cmp = chan == 0 ? 'r' : (chan == 1 ? 'g' : 'b');
var target = lerp(color1[cmp], color2[cmp], amount) +
getError(x, y, chan);
var actual = Math.floor(target * maxVal);
if (actual < 0)
actual = 0;
if (actual >= maxVal)
actual = maxVal - 1;
var err = target - actual / (maxVal - 1);
addError(x + 1, y + 0, chan, err * 7 / 16);
addError(x - 1, y + 1, chan, err * 3 / 16);
addError(x + 0, y + 1, chan, err * 5 / 16);
addError(x + 1, y + 1, chan, err * 1 / 16);
imd.data[(x + y * imageSize) * 4 + chan] =
Math.floor(actual * 255 / (maxVal - 1));
}
imd.data[(x + y * imageSize) * 4 + 3] = 255; // alpha
}
}

var y = 0;
function tick(){
doLine(y);
y++;
ctx.putImageData(imd, 0, 0);
if (y < imageSize)
setTimeout(tick, 1);
}

tick();
``````

Sample rendering: posted 3 Jun 2013 by Sean
tags: photoshop, banding, gradients, javascript, dithering