Search code examples
javascriptcolorscolor-space

selecting alpha for a color to be layered for largest gamut


a is the alpha value for a grayscale color: rgba(0,0,0,a)

I want to stack this color up to z times on a white rgb background.

Given z, how do I determine what a will create the widest gamut of resulting grayscale values?

I naively thought that I could solve with 255/z, but this results in my darkest grayscale values nowhere near 255. Here are three layers (z = 3), where I set a = 85. Alas! The darkest it gets is 180, so I could have increased a and ended up with a wider gamut. But how?

three layered rectangles

https://codepen.io/jedierikb/pen/JjRebjB?editors=1010

const $c = document.getElementById( 'c' );
$c.width = 200;
$c.height = 200;

const ctx = $c.getContext( '2d' );
const numLayers = 3;

const alpha = Math.round(255/numLayers);
ctx.fillStyle = '#000000' + intToHex(  alpha );
for (var i=0; i<numLayers; ++i) {
  ctx.fillRect( i*10, i*10, 100, 100 );
}

//let's get a pixel
var id = ctx.getImageData(numLayers*10, numLayers*10, 1, 1).data;
console.log( 'for ' + numLayers + ' layers, using alpha ' + alpha );
console.log( 'resulting in darkest alpha being', id[3] );

Solution

  • In your example (both the image and the CodePen) the alpha is 0.33.

    The lightest color is 170, which is 67% of 255. So the first layer of black removed a measure of brightness equal to its alpha (0.33 = 33%).

    Now, when the second layer is applied, the brigthness is substracted not from white but from the first layer. It had a brightness of 67%, so the second layer has a brightness of 67% * (1 - 0.33) = ~44%, which in RGB is 255 * 44% = ~112, equal to the color of the second layer.

    The third layer will have a brightness of 44% * (1 - 0.33) = ~30%, which in RGB is 255 * 30% = ~75, equal to the color of the third layer.

    Therefore, if you're applying black with alpha a to a white background z times, the z-th color will be

    255 * (1 - a)^z

    The range of values can be calculated using:

    range = 255 * (1 - a) - 255 * (1 - a)^z

    first color - last color

    By graphing this function you can tell that, for example, for z = 3 the local maximum is at 0.4226. You can also solve the polynomial or just cheat, calculate the value for a in increments of 0.05 and pick the highest result.

    function greatestGamutAlpha(layers, precision = 5) {
        let maxRangeAlpha,
            maxRange = 0;
        for (let a = 0; a < 100; a += precision) {
            const alpha = a / 100,
                range = 255 * (1 - alpha) - 255 * (1 - alpha) ** layers;
            if (range > maxRange) {
                maxRange = range;
                maxRangeAlpha = alpha;
            }
        }
        return maxRangeAlpha;
    }