Search code examples
javascriptcanvaslinear-gradientscolor-pickerpixel-manipulation

Draw saturation/brightness gradient


I'm trying to draw the following gradient image in canvas, but there's a problem in the right bottom.

Desired effect:

enter image description here

Current output:

enter image description here

I'm probably missing something really simple here.

function color(r, g, b) {
  var args = Array.prototype.slice.call(arguments);
  if (args.length == 1) {
    args.push(args[0]);
    args.push(args[0]);
  } else if (args.length != 3 && args.length != 4) {
    return;
  }
  return "rgb(" + args.join() + ")";
}

function drawPixel(x, y, fill) {
  var fill = fill || "black";
  context.beginPath();
  context.rect(x, y, 1, 1);
  context.fillStyle = fill;
  context.fill();
  context.closePath();
}

var canvas = document.getElementById("primary");
var context = canvas.getContext("2d");

canvas.width = 256;
canvas.height = 256;

for (var x = 0; x < canvas.width; x++) {
  for (var y = 0; y < canvas.height; y++) {
    var r = 255 - y;
    var g = 255 - x - y;
    var b = 255 - x - y;
    drawPixel(x, y, color(r, g, b));
  }
}
#primary {
    display: block;
    border: 1px solid gray;
}
<canvas id="primary"></canvas>

JSFiddle


Solution

  • Using gradients.

    You can get the GPU to do most of the processing for you.The 2D composite operation multiply effectively multiplies two colours for each pixel. So for each channel and each pixel colChanDest = Math.floor(colChanDest * (colChanSrc / 255)) is done via the massively parallel processing power of the GPU, rather than a lowly shared thread running on a single core (JavaScript execution context).

    The two gradients

    1. One is the background White to black from top to bottom

      var gradB = ctx.createLinearGradient(0,0,0,255); gradB.addColorStop(0,"white"); gradB.addColorStop(1,"black");

    2. The other is the Hue that fades from transparent to opaque from left to right

      var swatchHue var col = "rgba(0,0,0,0)" var gradC = ctx.createLinearGradient(0,0,255,0); gradC.addColorStop(0,``hsla(${hueValue},100%,50%,0)``); gradC.addColorStop(1,``hsla(${hueValue},100%,50%,1)``);

    Note the above strings quote are not rendering correctly on SO so I just doubled them to show, use a single quote as done in the demo snippet.

    Rendering

    Then layer the two, background (gray scale) first, then with composite operation "multiply"

    ctx.fillStyle = gradB;
    ctx.fillRect(0,0,255,255);
    ctx.fillStyle = gradC;
    ctx.globalCompositeOperation = "multiply";
    ctx.fillRect(0,0,255,255);
    ctx.globalCompositeOperation = "source-over";
    

    Only works for Hue

    It is important that the color (hue) is a pure colour value, you can not use a random rgb value. If you have a selected rgb value you need to extract the hue value from the rgb.

    The following function will convert a RGB value to a HSL colour

    function rgbToLSH(red, green, blue, result = {}){
        value hue, sat, lum, min, max, dif, r, g, b;
        r = red/255;
        g = green/255;
        b = blue/255;
        min = Math.min(r,g,b);
        max = Math.max(r,g,b);
        lum = (min+max)/2;
        if(min === max){
            hue = 0;
            sat = 0;
        }else{
            dif = max - min;
            sat = lum > 0.5 ? dif / (2 - max - min) : dif / (max + min);
            switch (max) {
            case r:
                hue = (g - b) / dif;
                break;
            case g:
                hue = 2 + ((b - r) / dif);
                break;
            case b:
                hue = 4 + ((r - g) / dif);
                break;
            }
            hue *= 60;
            if (hue < 0) {
                hue += 360;
            }        
        }
        result.lum = lum * 255;
        result.sat = sat * 255;
        result.hue = hue;
        return result;
    }
    

    Put it all together

    The example renders a swatch for a random red, green, blue value every 3 second.

    Note that this example uses Balel so that it will work on IE

    var canvas = document.createElement("canvas");
    canvas.width = canvas.height = 255;
    var ctx = canvas.getContext("2d");
    document.body.appendChild(canvas);
    
    function drawSwatch(r, g, b) {
      var col = rgbToLSH(r, g, b);
      var gradB = ctx.createLinearGradient(0, 0, 0, 255);
      gradB.addColorStop(0, "white");
      gradB.addColorStop(1, "black");
      var gradC = ctx.createLinearGradient(0, 0, 255, 0);
      gradC.addColorStop(0, `hsla(${Math.floor(col.hue)},100%,50%,0)`);
      gradC.addColorStop(1, `hsla(${Math.floor(col.hue)},100%,50%,1)`);
    
      ctx.fillStyle = gradB;
      ctx.fillRect(0, 0, 255, 255);
      ctx.fillStyle = gradC;
      ctx.globalCompositeOperation = "multiply";
      ctx.fillRect(0, 0, 255, 255);
      ctx.globalCompositeOperation = "source-over";
    }
    
    function rgbToLSH(red, green, blue, result = {}) {
      var hue, sat, lum, min, max, dif, r, g, b;
      r = red / 255;
      g = green / 255;
      b = blue / 255;
      min = Math.min(r, g, b);
      max = Math.max(r, g, b);
      lum = (min + max) / 2;
      if (min === max) {
        hue = 0;
        sat = 0;
      } else {
        dif = max - min;
        sat = lum > 0.5 ? dif / (2 - max - min) : dif / (max + min);
        switch (max) {
          case r:
            hue = (g - b) / dif;
            break;
          case g:
            hue = 2 + ((b - r) / dif);
            break;
          case b:
            hue = 4 + ((r - g) / dif);
            break;
        }
        hue *= 60;
        if (hue < 0) {
          hue += 360;
        }
      }
      result.lum = lum * 255;
      result.sat = sat * 255;
      result.hue = hue;
      return result;
    }
    
    function drawRandomSwatch() {
      drawSwatch(Math.random() * 255, Math.random() * 255, Math.random() * 255);
      setTimeout(drawRandomSwatch, 3000);
    }
    drawRandomSwatch();

    To calculate the colour from the x and y coordinates you need the calculated Hue then the saturation and value to get the hsv colour (NOTE hsl and hsv are different colour models)

    // saturation and value are clamped to prevent rounding errors creating wrong colour
    var rgbArray = hsv_to_rgb(
        hue, // as used to create the swatch
        Math.max(0, Math.min(1, x / 255)),   
        Math.max(0, Math.min(1, 1 - y / 255))
    );
    

    Function to get r,g,b values for h,s,v colour.

    /* Function taken from datGUI.js 
       Web site https://workshop.chromeexperiments.com/examples/gui/#1--Basic-Usage
       // h 0-360, s 0-1, and v 0-1
    */
    
    function hsv_to_rgb(h, s, v) {
      var hi = Math.floor(h / 60) % 6;
      var f = h / 60 - Math.floor(h / 60);
      var p = v * (1.0 - s);
      var q = v * (1.0 - f * s);
      var t = v * (1.0 - (1.0 - f) * s);
      var c = [
        [v, t, p],
        [q, v, p],
        [p, v, t],
        [p, q, v],
        [t, p, v],
        [v, p, q]
      ][hi];
      return {
        r: c[0] * 255,
        g: c[1] * 255,
        b: c[2] * 255
      };
    }