p5.jscolor-blending

P5.js: how to programatically calculate BURN blendMode between 2 colors?


I am doing some blendMode(BURN) to paint some shapes. And other shapes I need to painted directly in the resulting color of the previous shapes, so I need to generate the blended resulting color my self.

I am looking for something like:

let blendedColor = blendColorsBurn(color1, color2);

Solution

  • There's no built in function for this. In 2d mode p5.js leverages the canvas's globalCompositeOperation property, which when set to color-burn performs the following color blending operation:

    Divides the inverted bottom layer by the top layer, and then inverts the result.

    This sounds simple enough, but I wanted to verify it means what it sounds like it means, so I decided to try a quick test implementation. Press the shift and control keys to see the built in BURN blendMode vs my calculations respectively.

    let existingContent;
    let newContent;
    
    let manualBurn;
    
    let scaleFactor = 1;
    
    function preload() {
      existingContent = loadImage("https://www.paulwheeler.us/files/existing-content.png");
      newContent = loadImage("https://www.paulwheeler.us/files/new-content.png");
    }
    
    function setup() {
      createCanvas(windowWidth, windowHeight);
      noLoop();
    
      scaleFactor = height / newContent.height;
      manualBurn = createImage(newContent.width, newContent.height);
    
      // Divides the inverted bottom layer by the top layer, and then inverts the result.
      for (let x = 0; x < newContent.width; x++) {
        for (let y = 0; y < newContent.height; y++) {
          const c1 = existingContent.get(x, y);
          const c2 = newContent.get(x, y);
    
          if (alpha(c2) > 0) {
            let a = alpha(c1) / 255;
            // Inverted bottom layer
            let [inv_r, inv_g, inv_b, inv_a] = [
              // Subtracting from alpha instead of 1 is to deal with pre-multiplied alpha
              a - red(c1) / 255,
              a - green(c1) / 255,
              a - blue(c1) / 255,
              1 - alpha(c1) / 255,
            ];
    
            // divided by the top layer
            let [div_r, div_g, div_b, div_a] = [
              inv_r / (red(c2) / 255),
              inv_g / (green(c2) / 255),
              inv_b / (blue(c2) / 255),
              inv_a / (alpha(c2) / 255),
            ];
    
            // inverted
            let out = [255 * (1 - div_r), 255 * (1 - div_g), 255 * (1 - div_b), max(alpha(c2), 255 * (1 - div_a))];
    
            manualBurn.set(x, y, out);
          } else {
            manualBurn.set(x, y, c1);
          }
        }
      }
    
      manualBurn.updatePixels();
    }
    
    function keyPressed() {
      redraw();
    }
    
    function keyReleased() {
      redraw();
    }
    
    function draw() {
      clear();
      scale(scaleFactor);
      if (keyIsDown(CONTROL)) {
        blendMode(BLEND);
        image(manualBurn, 0, 0);
      } else {
        image(
          existingContent,
          0,
          0
        );
        if (keyIsDown(SHIFT)) {
          blendMode(BURN);
          image(newContent, 0, 0);
        }
      }
    }
    html, body {
      margin: 0;
      padding: 0;
    }
    body {
      background-image: url("https://www.paulwheeler.us/files/checkerboard.jpeg");
      background-repeat: repeat;
      background-size: 200px;
    }
    canvas {
      display: block;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>

    There were definitely some gotchas:

    1. It may be obvious but this math has to be done with values from 0 to 1, not 0 to 255.
    2. colorMode(RGB, 1, 1, 1, 1) does not work because the red(), green(), and blue() function round to integers 🤦‍♂️
    3. When inverting a color, you need to start from the alpha value rather than 1, because of premultiplied alpha.
    4. It's not obvious from the description, but it would appear that the burn images alpha takes precedence.