javascriptcolorssassphotoshopcolor-blending

How to calculate soft light blended value of two colors with an alpha input?


I need to write a JavaScript function and a SASS mixin to calculate Soft Light blended value of two colors with an alpha input.

The function / mixin will take three parameters as follows:

and will return the blended color.

Ideally the function will look like:

function soft_light(bgcol, fgcol, alpha) {
    return soft_light_blended_color;
}

Here is a reference about the Soft Light blend mode from Wikipedia. I am aiming the formula used by Photoshop.

I can't understand how the formula will be calculated. Why the value of b is in between 0 and 1 instead of a color code? How the gamma correction thing work? How I will perform basic operations like multiplication, subtraction used in the formula with the color values.

Guides to the solution are highly appreciated. Thanks in advance. :-)


Solution

  • Well, you are touching upon an area assumed to be simple by many, but colorimetry (or "the science and technology used to quantify and describe physically the human color perception.") is actually a very complex area and its entire scope is understood by relatively few.

    That may sound scary but, no worries, we won't need to understand its entirety for this task.

    Short on normalized values

    Lets start with why 0 to 1: this represents normalized values which is independent on bit-resolution (ie. 8-bit, 10-bit component values etc.). It is also needed for the linearizing process (e.g. gamma and color space) or we would end up with very wrong values. It also reduces the rounding errors when processed due to being floating point. The cons are of course a performance hit and some memory overhead.

    Short on gamma

    If you're using image sources you will need to know and use the gamma value for it. If the images are stored as sRGB you can assume the value to be 2.2. Although it isn't 100% accurate it's good enough for this purpose.

    Gamma is a logarithmic compensating of otherwise linear color values to match how luminance appears on the screen (CRT, LCD etc.) and is applied so that when you have, say, a gray-scale gradient it will actually be perceived as being evenly progressing from black to white by our human eye.

    However, all composition and blending require linear values (the middle value of the aforementioned gradient would actually be 127-128) to produce correct results, so we first need to normalize and apply inverse gamma on the values (in pseudo code - I'm using Col to represent all components, but each step has to be applied separately to each component):

    First steps (optional)

    You can skip these steps if accuracy isn't that important and performance matters more.

    var normCol = col / 255;    // 255 for 8-bit values
    

    Then apply inverse gamma (decoding gamma):

    var invGamma = 1 / 2.2;     // assumes sRGB
    var normColLin = Math.pow(normCol, invGamma);
    

    Now, we would normally use a LUT (look-up table) to do the gamma conversion but I'll keep it like this for clarity (you would in that case use it before the value is normalized, otherwise it isn't very useful).

    Blending and the Photoshop formula

    Now we can process the color. Blend is usually only applied to foreground (b or source) so we don't have to worry about background and alpha quite yet.

    The formula in the Wikipedia article is one version I have seen claiming to be the Photoshop formula.

    For example, the one I possess looks like this:

    Photoshop soft-light v1

    And the Wikipedia's look like this:

    https://wikimedia.org/api/rest_v1/media/math/render/svg/47d5e1e1712e03e28c4a858adc621385d3f5753e

    On top of that you have W3C version which is implemented in the browser (IIRC many of the blending formulas are "donated" by Adobe, so..).

        if(Cs <= 0.5)
            B(Cb, Cs) = Cb - (1 - 2 x Cs) x Cb x (1 - Cb)
        else
            B(Cb, Cs) = Cb + (2 x Cs - 1) x (D(Cb) - Cb)
    with
        if(Cb <= 0.25)
            D(Cb) = ((16 * Cb - 12) x Cb + 4) x Cb
        else
            D(Cb) = sqrt(Cb)
    

    But lets stick to the one you reference (you can always replace it with one of the other if you check against Photoshop and its result to see which matches most). Also see the discussion on the formula shown in the article.

    Still as pseudo (remember that b is foreground/top layer and a background/bottom layer in the Wikipedia article):

    // todo normalizing + inverse gamma (apply once to entire bitmaps)
    
    function soft_light(bgcol, fgcol, alpha) {
    
      var newCol;
      if (fgcol < 0.5) {
        newCol = 2 * bgcol * fgcol + bgcol * bgcol * (1 - 2 * fgcol);
      }
      else {
        newCol = 2 * bgcol * (1 - fgcol) + Math.sqrt(bgcol) * (2 * fgcol - 1);
      }
    
      // todo Composition
      // todo Alpha blending (maybe)
    }
    
    // todo gamma + scale
    

    Compositing

    Now we have our new blended foreground and it's time to composite it on the background using the alpha value. We will use the standard source-over method for this, as always with blending modes. We can use the W3C which uses standard Porter-Duff:

    Fa = 1; Fb = 1 – αs
    co = αs x Cs + αb x Cb x (1 – αs)
    αo = αs + αb x (1 – αs)
    

    We will perhaps need a alpha blending step at the end (check in your case).

    In the current pseudo function, that will translate into:

    function soft_light(bgcol, fgcol, alpha) {
      // Blending
      var newCol;
      if (fgcol < 0.5) {
        newCol = 2 * bgcol * fgcol + bgcol * bgcol * (1 - 2 * fgcol);
      }
      else {
        newCol = 2 * bgcol * (1 - fgcol) + Math.sqrt(bgcol) * (2 * fgcol - 1);
      }
    
      // Composition
      var as = alpha; // just for clarity. Value in range [0, 1]
      var ab = 1;     // assumes full opaque background here, otherwise use actual alpha of bg
      var Fb = 1 - as;
      newCol = as * newCol + ab * Fb * bgcol;
    
      // Alpha blending (you may not need this step)
      var a = as + ab * Fb;
      return a ? newCol / a : 0;
    }
    

    Gamma encoding and Normalizing

    And then we need to apply gamma again so we convert linear back to logarithmic for display (your browser may already do the gamma encoding for you for display purposes, but if you need to save it out..):

    var gamma = 2.2;
    var normCol = Math.pow(newCol, gamma);
    
    var finalCol = (normCol * 255)|0;  // scale and convert to integer+floor
    

    Summary

    So in summary, the steps are:

    *: Can be skipped if accuracy can be sacrificed, but not performance.

    The colors would be for example:

    var fgcol = {
      r: rn,
      g: gn,
      b: bn
    };
    

    So the blending code would actually look like this:

    newCol.r = fgcol.r < 0.5 ? 
      2 * bgcol.r * fgcol.r + bgcol.r * bgcol.r * (1 - 2 * fgcol.r) :
      2 * bgcol.r * (1 - fgcol.r) + Math.sqrt(bgcol.r) * (2 * fgcol.r - 1);
    
    newCol.b = fgcol.b < 0.5 ? 
      2 * bgcol.b * fgcol.b + bgcol.b * bgcol.b * (1 - 2 * fgcol.b) :
      2 * bgcol.b * (1 - fgcol.b) + Math.sqrt(bgcol.b) * (2 * fgcol.b - 1);
    
    // etc.
    

    Disclaimer: I did not test codes, formulas above for errors due to the large scope and I didn't intend to write-up all the code. Even if there are errors you should get the general idea. If someone finds errors, feel free to update the post directly or comment.